Meta Space

原文:https://www.javadoop.com/post/metaspace

永久代主要存放以下數(shù)據(jù):

  • JVM internal representation of classes and their metadata //類及其元數(shù)據(jù)的JVM內(nèi)部表示
  • Class statics //類的靜態(tài)
  • Interned strings //實際字符串,說的就是常量池吧

從 JDK7 開始,JDK 開發(fā)者們就有消滅永久代的打算了。有部分?jǐn)?shù)據(jù)移到永久代之外了:

  • Symbols => native memory // 符號引用 >本機內(nèi)存
  • Interned strings => Java Heap // Interned string => Java堆
  • Class statics => Java Heap //類statics => Java堆

到了 JDK8,這個工作終于完成了,徹底廢棄了 PermGen,Metaspace 取而代之。

方法區(qū)都存了些什么

  • JVM中類的元數(shù)據(jù)在Java堆中的存儲區(qū)域。
  • Java類對應(yīng)的HotSpot虛擬機中的內(nèi)部表示也存儲在這里。
  • 類的層級信息,字段,名字。
  • 方法的編譯信息及字節(jié)碼。
  • 變量
  • 常量池和符號解析

持久代的大小

  • 它的上限是MaxPermSize,默認(rèn)是64M
  • Java堆中的連續(xù)區(qū)域 : 如果存儲在非連續(xù)的堆空間中的話,要定位出持久代到新對象的引用非常復(fù)雜并且耗時??ū恚╟ard table),是一種記憶集(Remembered Set),它用來記錄某個內(nèi)存代中普通對象指針(oops)的修改。
  • 持久代用完后,會拋出OutOfMemoryError "PermGen space"異常。解決方案:應(yīng)用程序清理引用來觸發(fā)類卸載;增加MaxPermSize的大小。
  • 需要多大的持久代空間取決于類的數(shù)量,方法的大小,以及常量池的大小。

為什么移除持久代

  • 它的大小是在啟動時固定好的——很難進(jìn)行調(diào)優(yōu)。-XX:MaxPermSize,設(shè)置成多少好呢?
  • HotSpot的內(nèi)部類型也是Java對象:它可能會在Full GC中被移動,同時它對應(yīng)用不透明,且是非強類型的,難以跟蹤調(diào)試,還需要存儲元數(shù)據(jù)的元數(shù)據(jù)信息(meta-metadata)。
  • 簡化Full GC:每一個回收器有專門的元數(shù)據(jù)迭代器。
  • 可以在GC不進(jìn)行暫停的情況下并發(fā)地釋放類數(shù)據(jù)。
  • 使得原來受限于持久代的一些改進(jìn)未來有可能實現(xiàn)

根據(jù)上面的各種原因,永久代最終被移除,方法區(qū)移至Metaspace,字符串常量移至Java Heap(待測試確認(rèn))。

什么是 Metaspace

Metaspace 區(qū)域位于堆外,所以它的最大內(nèi)存大小取決于系統(tǒng)內(nèi)存,而不是堆大小,我們可以指定 MaxMetaspaceSize 參數(shù)來限定它的最大內(nèi)存。
Metaspace 是用來存放 class metadata 的,class metadata 用于記錄一個 Java 類在 JVM 中的信息,包括但不限于 JVM class file format 的運行時數(shù)據(jù):
1、Klass 結(jié)構(gòu),這個非常重要,把它理解為一個 Java 類在虛擬機內(nèi)部的表示吧;
2、method metadata,包括方法的字節(jié)碼、局部變量表、異常表、參數(shù)信息等;
3、常量池;
4、注解;
5、方法計數(shù)器,記錄方法被執(zhí)行的次數(shù),用來輔助 JIT 決策
6、 其他
雖然每個 Java 類都關(guān)聯(lián)了一個 java.lang.Class 的實例,而且它是一個貯存在堆中的 Java 對象。但是類的 class metadata 不是一個 Java 對象,它不在堆中,而是在 Metaspace 中。

什么時候分配 Metaspace 空間

當(dāng)一個類被加載時,它的類加載器會負(fù)責(zé)在 Metaspace 中分配空間用于存放這個類的元數(shù)據(jù)

什么時候回收 Metaspace 空間

分配給一個類的空間,是歸屬于這個類的類加載器的,只有當(dāng)這個類加載器卸載的時候,這個空間才會被釋放。
所以,只有當(dāng)這個類加載器加載的所有類都沒有存活的對象,并且沒有到達(dá)這些類和類加載器的引用時,相應(yīng)的 Metaspace 空間才會被 GC 釋放。

配置 Metaspace 空間

  • -XX:MaxMetaspaceSize:Metaspace 總空間的最大允許使用內(nèi)存,默認(rèn)是不限制。

  • -XX:CompressedClassSpaceSize:Metaspace 中的 Compressed Class Space 的最大允許內(nèi)存,默認(rèn)值是 1G,這部分會在 JVM 啟動的時候向操作系統(tǒng)申請 1G 的虛擬地址映射,但不是真的就用了操作系統(tǒng)的 1G 內(nèi)存。

Metaspace 和 GC

MetaSpace 使用的宿主機的本地native內(nèi)存。MetaSpace的空間在什么時候會被回收呢?

首先可以確定的就是,metaspace肯定會發(fā)生GC。
metaspace發(fā)生GC的時機:
1)metaspace在沒有更多的內(nèi)存空間的時候,比如加載新的類的時候;
2)JVM內(nèi)部又一個叫做_capacity_until_GC的變量,一旦metaspace使用的空間超過這個變量的值,就會對metaspace進(jìn)行回收。這個變量的初始值為MetaspaceSize,但是會自動的在 MetaspaceSize和MaxMetaspaceSize之間進(jìn)行調(diào)整。
3)FGC時會對metaspace進(jìn)行回收。
metaspace回收用的是什么垃圾回收器:G1和CMS都能很好的對metaspace進(jìn)行回收。

Metaspace 只在 GC 運行并且卸載類加載器的時候才會釋放空間。當(dāng)然,在某些時候,需要主動觸發(fā) GC 來回收一些沒用的 class metadata,即使這個時候?qū)τ诙芽臻g來說,還達(dá)不到 GC 的條件。

Metaspace 可能在兩種情況下觸發(fā) GC:
1、分配空間時:虛擬機維護(hù)了一個閾值,如果 Metaspace 的空間大小超過了這個閾值,那么在新的空間分配申請時,虛擬機首先會通過收集可以卸載的類加載器來達(dá)到復(fù)用空間的目的,而不是擴大 Metaspace 的空間,這個時候會觸發(fā) GC。這個閾值會上下調(diào)整,和 Metaspace 已經(jīng)占用的操作系統(tǒng)內(nèi)存保持一個距離。
2、碰到 Metaspace OOM:Metaspace 的總使用空間達(dá)到了 MaxMetaspaceSize 設(shè)置的閾值,或者 Compressed Class Space 被使用光了,如果這次 GC 真的通過卸載類加載器騰出了很多的空間,這很好,否則的話,我們會進(jìn)入一個糟糕的 GC 周期,即使我們有足夠的堆內(nèi)存。

2、Metaspace 的架構(gòu)

這一節(jié)將深入到 Metaspace 的架構(gòu)實現(xiàn),將描述它的每一層和每一個組件,以及它們是怎么工作的。

對于開發(fā)者來說,這一定是非常有趣的一件事情,我們大部分開發(fā)者都不可能去開發(fā) JDK,但是了解這些總是充滿著樂趣。

Metaspace 在實現(xiàn)上分為多層。最底層,負(fù)責(zé)向操作系統(tǒng)申請大塊的內(nèi)存;中間的一層,負(fù)責(zé)分出一小塊一小塊給每個類加載器;最頂層,類加載器負(fù)責(zé)把這些申請到的內(nèi)存塊用來存放 class metadata。

最底層:the space list

在最底層,JVM 通過 mmap(3) 接口向操作系統(tǒng)申請內(nèi)存映射,在 64 位平臺上,每次申請 2MB 空間。

當(dāng)然,這里的 2MB 不是真的就消耗了主存的 2MB,只有之后在使用的時候才會真的消耗內(nèi)存。這里是虛擬內(nèi)存映射。

每次申請過來的內(nèi)存區(qū)域,放到一個鏈表中 VirtualSpaceList,作為其中的一個 Node??聪聢D。

一個 Node 是 2MB 的空間,前面說了在使用的時候再向操作系統(tǒng)申請實際的內(nèi)存,但是頻繁的系統(tǒng)調(diào)用會降低性能,所以 Node 內(nèi)部需要維護(hù)一個水位線,當(dāng) Node 內(nèi)已使用內(nèi)存快達(dá)到水位線的時候,向操作系統(tǒng)要新的內(nèi)存頁。并且相應(yīng)地提高水位線。

直到一個 Node 被完全用完,會分配一個新的 Node,并且將其加入到鏈表中,老的 Node 就 “退休” 了。下圖中,前面的三個 Node 就是退休狀態(tài)了。

從一個 Node 中分配內(nèi)存,每一塊稱為 MetaChunk,chunk 有三種規(guī)格,在 64 位系統(tǒng)中分別為 1K、4K、64K。

image.png

鏈表 VirtualSpaceList 和每個節(jié)點 Node 是全局的,而 Node 內(nèi)部的一個個 MetaChunk 是分配給每個類加載器的。所以一個 Node 通常由分配給多個類加載器的 chunks 組成。

image.png

當(dāng)一個類加載器和它加載的所有的類都卸載的時候,它占用的 chunks 就會加入到一個全局的空閑列表中:ChunkManager,看下圖:

image.png

這些 chunks 會被復(fù)用:如果其他的類加載器加載新的類,它可能就會得到一個空閑列表中的 chunk,而不是去 Node 中申請一個新的 chunk。

image.png

后面會說到,如果剛好把整個 Node 都清空了,那么這整個 Node 的內(nèi)存會直接還給操作系統(tǒng)。

當(dāng)然,由這個 Node 進(jìn)入到空閑列表的節(jié)點也要刪除。

中間層:Metachunk

通常一個類加載器在申請 Metaspace 空間用來存放 metadata 的時候,也就需要幾十到幾百個字節(jié),但是它會得到一個 Metachunk,一個比要求的內(nèi)存大得多的內(nèi)存塊。

為什么?因為前面說了,要從全局的 VirtualSpaceList 鏈表的 Node 中分配內(nèi)存是昂貴的操作,需要加鎖。我們不希望這個操作太頻繁,所以一次性給一個大的 MetaChunk,以便于這個類加載器之后加載其他的類,這樣就可以做到多個類加載器并發(fā)分配了。只有當(dāng)這個 chunk 用完了,類加載器才需要又去 VirtualSpaceList 申請新的 chunk。

前面說了,chunk 有三種規(guī)格,那 Metaspace 的分配器怎么知道一個類加載器每次要多大的 chunk 呢?這當(dāng)然是基于猜測的:

  • 通常,一個標(biāo)準(zhǔn)的類加載器在第一次申請空間時,會得到一個 4K 的 chunk,直到它達(dá)到了一個隨意設(shè)置的閾值(4),此時分配器失去了耐心,之后會一次性給它一個 64K 的大 chunk。
  • bootstrap classloader 是一個公認(rèn)的會加載大量的類的加載器,所以分配器會給它一個巨大的 chunk,一開始就會給它 4M??梢酝ㄟ^ InitialBootClassLoaderMetaspaceSize 進(jìn)行調(diào)優(yōu)。
  • 反射類類加載器 (jdk.internal.reflect.DelegatingClassLoader) 和匿名類類加載器只會加載一個類,所以一開始只會給它們一個非常小的 chunk(1K),因為給它們太多就是一種浪費。

類加載器申請空間的時候,每次都給類加載器一個 chunk,這種優(yōu)化,是建立在假設(shè)它們立馬就會需要新的空間的基礎(chǔ)上的。這種假設(shè)可能正確也可能錯誤,可能在拿到一個很大的 chunk 后,這個類加載器恰巧就不再需要加載新的類了。

對于這部分可能的空間浪費,可以在后面介紹的系統(tǒng)工具中觀察到。

最頂層:Metablock

在 Metachunk 上,我們有一個二級分配器(class-loader-local allocator),它將一個 Metachunk 分割成一個個小的單元,這些小的單元稱為 Metablock,它們是實際分配給每個調(diào)用者的。

這個二級分配器非常原始,它的速度也非??欤?/p>

前面說過,class metadata 的生命周期是和類加載器綁定的,所以在類加載器卸載的時候,JVM 可以大塊大塊地釋放這些空間。

下面展示一個 Metachunk 的結(jié)構(gòu):

image.png

這個 chunk 誕生的時候,它只有一個 header,之后的分配都只要在頂部進(jìn)行分配就行。

由于這個 chunk 是歸屬于一個類加載器的,所以如果它不再加載新的類,那么 unused 空間就將真的浪費掉。

ClassloaderData and ClassLoaderMetaspace

在 JVM 內(nèi)部,一個類加載器以一個 ClassLoaderData 結(jié)構(gòu)標(biāo)識,這個結(jié)構(gòu)引用了一個 ClassLoaderMetaspace 結(jié)構(gòu),它維護(hù)了該加載器使用的所有的 Metachunk。

當(dāng)這個類加載器被卸載的時候,這個 ClassLoaderDataClassLoaderMetaspace 會被刪除。并且會將所有的這個加載器用到的 chunks 歸還到空閑列表中。這部分內(nèi)存是否可以直接歸還給操作系統(tǒng)取決于是否滿足其他條件,后面會介紹。

就是前面提過的,如果恰好把整個 Node 都清空了,那么這個 Node 的內(nèi)存直接還給操作系統(tǒng)

匿名類

ClassloaderData != ClassLoaderMetaspace

注意,我們前面說,“Metaspace 內(nèi)存是屬于類加載器的”,但是,這里其實撒了一個小謊,如果將匿名類考慮進(jìn)去,那就更加復(fù)雜了:

當(dāng)類加載器加載一個匿名類時,這個類有自己獨立的 ClassLoaderData,它的生命周期是跟隨著這個匿名類的,而不是這個類加載器(所以,和它相關(guān)的空間可以在類加載器卸載前得到釋放)。所以,一個類加載器有一個主要的 ClassLoaderData 結(jié)構(gòu)用來服務(wù)所有的正常的類,對于每一個匿名類,還有一個二級的 ClassLoaderData 結(jié)構(gòu)來維護(hù)。

這樣做的目的之一,其實就是沒有必要擴大大量的 Lambdas 和 method handlers 在 Metaspace 中的空間的生命周期。

image.png

內(nèi)存什么時候會還給操作系統(tǒng)

當(dāng)一個 VirtualSpaceListNode 中的所有 chunk 都是空閑的時候,這個 Node 就會從鏈表 VirtualSpaceList 中移除,它的 chunks 也會從空閑列表中移除,這個 Node 就沒有被使用了,會將其內(nèi)存歸還給操作系統(tǒng)。

image.png

對于一個空閑的 Node 來說,擁有其上面的 chunks 的所有的類加載器必然都是被卸載了的。

至于這個情況是否可能發(fā)生,主要就是取決于碎片化:

一個 Node 是 2M,chunks 的大小為 1K, 4K 或 64K,所以通常一個 Node 上有約 150-200 個 chunks,如果這些 chunks 全部由同一個類加載器擁有,回收這個類加載器就可以一次性回收這個 Node,并且把它的空間還給操作系統(tǒng)。

但是,如果這些 chunks 分配給不同的類加載器,每個類加載器都有不同的生命周期,那么什么都不會被釋放。這也許就是在告訴我們,要小心對待大量的小的類加載器,如那些負(fù)責(zé)加載匿名類或反射類的加載器。

同時也要清楚,Metaspace 中的 Compressed Class Space 是永遠(yuǎn)不會將內(nèi)存還給操作系統(tǒng)的。我們馬上就要介紹這部分內(nèi)容了。

本節(jié)小結(jié)

  • 每次向操作系統(tǒng)申請 2M 的虛擬空間映射,放置到全局鏈表中,待需要使用的時候申請內(nèi)存。
  • 一個 Node 會分割為一個個的 chunks,分配給類加載器,一個 chunk 屬于一個類加載器。
  • chunk 再細(xì)分為一個個 Metablock,這是分配給調(diào)用者的最小單元。
  • 當(dāng)一個類加載器被卸載,它占有的 chunks 會進(jìn)入到空閑列表,以便復(fù)用,如果運氣好的話,有可能會直接把內(nèi)存歸還給操作系統(tǒng)。

3、什么是 Compressed Class Space

在 64 位平臺上,HotSpot 使用了兩個壓縮優(yōu)化技術(shù),Compressed Object Pointers (“CompressedOops”) 和 Compressed Class Pointers。

壓縮指針,指的是在 64 位的機器上,使用 32 位的指針來訪問數(shù)據(jù)(堆中的對象或 Metaspace 中的元數(shù)據(jù))的一種方式。

這樣有很多的好處,比如 32 位的指針占用更小的內(nèi)存,可以更好地使用緩存,在有些平臺,還可以使用到更多的寄存器。

當(dāng)然,在 64 位的機器中,最終還是需要一個 64 位的地址來訪問數(shù)據(jù)的,所以這個 32 位的值是相對于一個基準(zhǔn)地址的值。

CompressedOops 說的是對象引用的壓縮,它不在本文的討論范圍內(nèi)。

在 64 位平臺上,本質(zhì)上還是需要使用 64 位地址來引用每一個對象的,但是這項技術(shù)使得可以只使用 32 位地址來實現(xiàn)引用。大家可以參考一下評論區(qū)的討論,這里就不展開了。

由于本文在描述的是 Metaspace,所以我們這里不關(guān)心 Compressed Object Pointers,下面將描述 Compressed Class Pointers

每個 Java 對象,在它的頭部,有一個引用指向 Metaspace 中的 Klass 結(jié)構(gòu)。

image.png

當(dāng)使用了 compressed class pointers,這個引用是 32 位的值,為了找到真正的 64 位地址,需要加上一個 base 值:

image.png

上面的內(nèi)容應(yīng)該很好理解,這項技術(shù)對 Klass 的分配帶來的問題是:由于 32 位地址只能訪問到 4G 的空間,所以最大只允許 4G 的 Klass 地址。這項限制也意味著,JVM 需要向 Metaspace 分配一個連續(xù)的地址空間。

當(dāng)從系統(tǒng)申請內(nèi)存時,通過調(diào)用系統(tǒng)接口 malloc(3) 或 mmap(3),操作系統(tǒng)可能返回任意一個地址值,所以在 64位系統(tǒng)中,它并不能保證在 4G 的范圍內(nèi)。

所以,我們只能用一個 mmap() 來申請一個區(qū)域單獨用來存放 Klass 對象。我們需要提前知道這個區(qū)域的大小,而且不能超過 4G。顯然,這種方式是不能擴展的,因為這個地址后面的內(nèi)存可能是被占用的。

只有 Klass 結(jié)構(gòu)有這個限制,對于其他的 class metadata 沒有這個必要: 因為只有 Klass 實例是通過 Java 對象 header 中的壓縮指針訪問的。其他的 metadata 都是通過 64 位的地址進(jìn)行訪問的,所以它們可以被放到任意的地址上。

所以,我們決定將 Metaspace 分為兩個區(qū)域:non-class part 和 class part。

  • class part:存放 Klass 對象,需要一個連續(xù)的不超過 4G 的內(nèi)存
  • non-class part:包含其他的所有 metadata

class part 被稱作 Compressed Class Space,這個名字會有點怪,因為 Klass 本身其實沒有使用壓縮技術(shù),而是引用它們的指針被壓縮了。

compressed class space 空間的大小,是通過 -XX:CompressedClassSpaceSize 指定的。

我們需要提前知道自己需要多少內(nèi)存,它的默認(rèn)值是 1G。當(dāng)然這個 1G 并不是真的使用了操作系統(tǒng)的 1G,而是虛擬地址映射。

實現(xiàn)

為了復(fù)用已有的 Metaspace 空間,使用了一個小技巧:

在 Class Space 和 Non-Class Space 中,分別都有 VirtualSpaceList 和 ChunkManager 兩個結(jié)構(gòu)。

但是對于 Class Space,既然我們需要一個連續(xù)的空間我們不能使用一個鏈表來存放所有的 Node,所以這個鏈表退化為只有一個節(jié)點,并且不能擴展。這個 Node 就是 compressed class space,和 Non-Class Space 中的 Node 相比,它可是巨大無比。

image.png

ClassLoaderMetaspace(記錄當(dāng)前類加載器持有哪些 chunks)需要兩個鏈表,一個用于記錄 Class Space 中的 chunks,一個用于記錄 Non-Class Space 中的 chunks。

到這里應(yīng)該也很好理解,就是對于一個類加載器來說,它需要知道自己使用了 non-class part 中的哪些 chunks 和 class part 中的哪些 chunks。

開關(guān): UseCompressedClassPointers, UseCompressedOops

-XX:+UseCompressedOops 允許對象指針壓縮。

-XX:+UseCompressedClassPointers 允許類指針壓縮。

它們默認(rèn)都是開啟的,可以手動關(guān)閉它們。

如果不允許類指針壓縮,那么將沒有 compressed class space 這個空間,并且-XX:CompressedClassSpaceSize 這個參數(shù)無效。

-XX:-UseCompressedClassPointers 需要搭配 -XX:+UseCompressedOops,但是反過來不是: 我們可以只壓縮對象指針,不壓縮類指針。

這里面為什么這么規(guī)定我也不懂,但是從直覺上來說,壓縮對象指針顯然是比較重要的,能獲得較大的收益。也許就是基于這種考量吧:你連對象指針都不壓縮,類指針壓縮不壓縮又有什么關(guān)系呢?

注意,對象指針壓縮要求堆小于 32G,所以如果堆大于等于 32G,那么對象指針壓縮和類指針壓縮都會被關(guān)閉。

32G 可不是一個掐指一算隨便指定的數(shù)字,看下評論區(qū)就知道原因了。

4、度量 Metaspace

前面我們介紹過,MaxMetaspaceSizeCompressedClassSpaceSize 是控制 Metaspace 的兩個配置。

回顧一下:

  • MaxMetaspaceSize

    最大允許 Metaspace 使用的內(nèi)存,包括 Class Space 和 Non-Class Space,默認(rèn)是不限制。

  • CompressedClassSpaceSize

    在啟動的時候就限制 Class Space 的大小,默認(rèn)值是 1G,啟動后不可以修改。再說一遍,它是 reserved 不是 committed 的內(nèi)存。

下圖展示了它們是怎么工作的:

image.png

紅色部分是 Metaspace 中已使用的系統(tǒng)內(nèi)存,包括 Non-Class Space 鏈表中的紅色部分和 Class Space 中大 Node 的紅色部分。這個總和受到 -XX:MaxMetaspaceSize 的限制,超出將拋出 OutOfMemoryError(“Metaspace”)。

-XX:CompressedClassSpaceSize 限制了下方的 Class Space 中,這個大 Node 的大小,包括了紅色已使用的內(nèi)存和藍(lán)色未使用的內(nèi)存。如果這個 Node 被用完了,會拋出 OutOfMemoryError(“Compressed Class Space”)。

所以這意味著什么?

當(dāng)一個 Java 類被加載后,它需要 Non-Class Space 和 Class Space 的空間,而且后者通常都是被限制的(默認(rèn) 1G),所以我們總是有那么一個上限存在,即使 -XX:MaxMetaspaceSize 沒有配置。

所以,是否會觸及到這個上限,取決于 Non-Class Space 和 Class Space 的使用比例。

對于每個類,我們假設(shè)這個比例是 1: 5 (class:non-class) 。

這意味著,對于 -XX:CompressedClassSpaceSize 的 1G 的默認(rèn)值,我們的上限約 6G,1G 的 Class Space 再加約 5G 的 Non-Class Space。

一個類大概需要多大的 Metaspace 空間

對于一個被加載到虛擬機中的類,Metaspace 需要分配 class 和 non-class 空間,那么這些空間花在哪里了呢?看下圖:

image.png

深入 Class Space:

最大的一部分是 Klass 結(jié)構(gòu),它是固定大小的。

然后緊跟著兩個可變大小的 vtable 和 itable,前者由類中方法的數(shù)量決定,后者由這個類所實現(xiàn)接口的方法數(shù)量決定。

隨后是一個 map,記錄了類中引用的 Java 對象的地址,盡管該結(jié)構(gòu)一般都很小,不過也是可變的。

vtable 和 itable 通常也很小,但是對于一些巨大的類,它們也可以很大,一個有 30000 個方法的類,vtable 的大小會達(dá)到 240k,如果類派生自一個擁有 30000 個方法的接口,也是同理。但是這些都是測試案例,除了自動生成代碼,你從來不會看到這樣的類。

深入 Non-Class Space

這個區(qū)域有很多的東西,下面這些占用了最多的空間:

  • 常量池,可變大??;

  • 每個成員方法的 metadata:ConstMethod 結(jié)構(gòu),包含了好幾個可變大小的內(nèi)部結(jié)構(gòu),如方法字節(jié)碼、局部變量表、異常表、參數(shù)信息、方法簽名等;

  • 運行時數(shù)據(jù),用來控制 JIT 的行為;

  • 注解

Metaspace 中的結(jié)構(gòu)都繼承自 MetaspaceObj,所以查看它的類繼承結(jié)構(gòu)能了解更詳細(xì)的信息。

Class space 和 Non-Class Space 比例

下面看一下在一些典型的應(yīng)用中,它們之間的大小比例數(shù)據(jù)。

下面是 WildFly 應(yīng)用服務(wù)器,16.0.0,運行在 SAPMachine 11 平臺上,沒有加載任何應(yīng)用。我們檢查下總共需要多少 Metaspace 空間,然后計算平均每個類所需要的空間。我們使用 jcmd VM.metaspace 進(jìn)行度量。

loader #classes non-class space (avg per class) class space (/avg per class) ratio non-class/class
all 11503 60381k (5.25k) 9957k (0.86k) 6.0 : 1
bootstrap 2819 16720k (5.93k) 1768k (0.62k) 9.5 : 1
app 185 1320k (7.13k) 136k (0.74k) 9.7 : 1
anonymous 869 1013k (1.16k) 475k (0.55k) 2.1 : 1

這個表告訴我們:

  • 對于正常的類(我們假設(shè)通過 bootstrap 和 app 加載的類是正常的),我可以得到平均每個類需要約 5-7k 的 Non-Class Space 和 600-900 bytes 的 Class Space。
  • 匿名類要小得多,但是也有一個有趣的事情,Class 和 Non-Class Space 之間的比例,相對的,我們需要更多的 Class Space。這也不奇怪,因為諸如 Lambda 類都是很小的,但是它的 Klass 結(jié)構(gòu)不可能小于 sizeof(Klass)。所以,我們得到 1k Non-Class Space 和 0.5k Class Space。

注意,在我們的案例中,匿名類的數(shù)據(jù)可能沒有代表性,需要收集更多的匿名類,才能得到更準(zhǔn)確的數(shù)據(jù)。

Metaspace 默認(rèn)大小

如果我們完全不設(shè)置限制 Metaspace 的大小,那么 Metaspace 可以容納多少類呢?

MaxMetaspaceSize 默認(rèn)是沒有限制的,CompressedClassSpaceSize 默認(rèn)是 1G,所以我們唯一會觸碰到的是 Class Space 空間的上限。

使用上面的數(shù)據(jù),每個類約 5-7k 的 Non-Class Space 和 600-900 bytes 的 Class Space,我們可以估算出大約 1-1.5 百萬的類(假設(shè)沒有碎片、沒有浪費)以后會觸碰到 Class Space 的 OOM。這是一個很大的數(shù)值了。

限制 Metaspace 空間大小

免責(zé)聲明:不要盲目使用你在網(wǎng)絡(luò)上找到的規(guī)則,尤其是這些數(shù)據(jù)并非來自生產(chǎn)數(shù)據(jù)。

其實我們沒有什么選擇,你確實可以限制 Metaspace 的空間增長,但是如果你的程序需要更多的空間用來存放 class metadata,那么你就會碰到 OOM,除了讓你的代碼加載更少的類,否則,你幾乎是無能為力。

和堆進(jìn)行比較:你可以增加和減少堆的大小,而不必影響代碼功能,所以堆的配置是比較靈活的,而 Metaspace 不具備這個特性。

那么你為什么要限制 Metaspace 的大小呢?

  • 告警系統(tǒng)需要知道,為什么 Metaspace 空間以一個異常的速度在消耗,需要有人去看一下發(fā)生了什么。
  • 有時候需要限制虛擬內(nèi)存地址的大小。通常我們感興趣的是實際消耗內(nèi)存,但是虛擬內(nèi)存大小可能會導(dǎo)致虛擬機進(jìn)程達(dá)到系統(tǒng)限制。

注意:JDK 版本依賴:與 JDK 11或更高版本相比,JDK 8 中的元空間受到碎片的影響更大。所以在 JDK 8 環(huán)境下分配的時候,需要設(shè)置更多的緩沖。

如果要限制 Metaspace 大小使得系統(tǒng)更容易被監(jiān)控,同時不用在乎虛擬地址空間的大小,那么最好只設(shè)置 MaxMetaspaceSize 而不用設(shè)置 CompressedClassSpaceSize。如果要單獨設(shè)置,那么最好設(shè)置 CompressedClassSpaceSize 為 MaxMetaspaceSize 的 80% 左右。

除了 MaxMetaspaceSize 之外,減小 CompressedClassSpaceSize 的唯一原因是減小虛擬機進(jìn)程的虛擬內(nèi)存大小。 但是,如果將 CompressedClassSpaceSize 設(shè)置得太低,則可能在用完 MaxMetaspaceSize 之前先用完了 Compressed Class Space。 在大多數(shù)情況下,比率為1:2(CompressedClassSpaceSize = MaxMetaspaceSize / 2)應(yīng)該是安全的。

那么,你應(yīng)該將 MaxMetaspaceSize 設(shè)置為多大呢? 首先應(yīng)該是計算預(yù)期的 Metaspace 使用量。你可以使用上面給出的數(shù)字,然后給每個類約 1K 的 Class Space 和 3~8K 的 Non-Class Space 作為緩沖。

因此,如果你的應(yīng)用程序計劃加載10000個類,那么從理論上講,你只需要 10M 的 Class Space 和 80M Non-Class Space。

然后,你需要考慮安全系數(shù)。在大多數(shù)情況下,因子 2 是比較安全的。你當(dāng)然也可以碰運氣,設(shè)置低一點,但是要做好在碰到 OOM 后調(diào)大 Metaspace 空間的準(zhǔn)備。

如果設(shè)置安全因子為 2,那么需要 20M 的 Class Space 和 160M 的 Non-Class Space,也就是總大小為 180M。因此,在這里 -XX:MaxMetaspaceSize=180M 是一個很好的選擇。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • JVM的實現(xiàn)規(guī)范中要求必須實現(xiàn)五個部分,分別是堆、棧、PC計數(shù)器、本地方法棧和方法區(qū)。PermGen Space和...
    摸摸臉上的胡渣閱讀 876評論 0 0
  • 1.8之前 JVM分區(qū)可以分為線程共有——新生代、老年代、永久代,線程私有—虛擬機棧、本地方法棧、程序計數(shù)器,具體...
    維特?zé)o憂堡閱讀 1,966評論 0 0
  • 在過去(當(dāng)自定義類加載器使用不普遍的時候),類幾乎是“靜態(tài)的”并且很少被卸載和回收,因此類也可以被看成“永久的”。...
    云狗狗狗狗狗閱讀 4,451評論 0 8
  • 3:類加載-初始化 1. 加載過程 1. Loading 1. 雙親委派,主要出于安全來考慮 2. LazyLoa...
    Yuszha閱讀 570評論 0 0
  • 讓Java應(yīng)用程序運行是一回事,但讓他們跑得快就是另外一回事了。在面對對象的環(huán)境中,性能問題就像來勢兇猛的野獸。但...
    程序員技術(shù)圈閱讀 611評論 0 2

友情鏈接更多精彩內(nèi)容