原文: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。

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

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

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

后面會說到,如果剛好把整個 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):

這個 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)這個類加載器被卸載的時候,這個 ClassLoaderData 和 ClassLoaderMetaspace 會被刪除。并且會將所有的這個加載器用到的 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 中的空間的生命周期。

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

對于一個空閑的 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)。

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

上面的內(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 相比,它可是巨大無比。

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
前面我們介紹過,MaxMetaspaceSize 和 CompressedClassSpaceSize 是控制 Metaspace 的兩個配置。
回顧一下:
-
MaxMetaspaceSize
最大允許 Metaspace 使用的內(nèi)存,包括 Class Space 和 Non-Class Space,默認(rèn)是不限制。
-
CompressedClassSpaceSize
在啟動的時候就限制 Class Space 的大小,默認(rèn)值是 1G,啟動后不可以修改。再說一遍,它是 reserved 不是 committed 的內(nèi)存。
下圖展示了它們是怎么工作的:

紅色部分是 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 空間,那么這些空間花在哪里了呢?看下圖:

深入 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 是一個很好的選擇。