我們深入研究元空間的架構(gòu)。我們描述了各個層和組件弓叛,以及它們是如何協(xié)同工作的毕籽。
這對那些想要破解hotspot
和Metaspace
或者至少真正理解內(nèi)存的去向以及為什么我們不能僅僅使用malloc
的人來說是很有趣的睡榆。
與大多數(shù)其他非平凡的分配器一樣,元空間是在層中實現(xiàn)的。
在底部锭硼,內(nèi)存是在操作系統(tǒng)的大區(qū)域中分配的。在中間婴削,我們將這些區(qū)域分割成不太大的塊虫溜,然后交給類裝入器寄雀。
在頂部急膀,類裝入器將這些塊分割為調(diào)用程序代碼餐禁。
元空間的底層:虛擬空間列表VirtualSpaceList
VirtualSpaceList:
http://hg.openjdk.java.net/jdk/jdk11/file/1ddf9a99e4ad/src/hotspot/share/memory/metaspace/virtualSpaceList.hpp#l39
在最底層(在最粗的粒度上),Metaspace的內(nèi)存是保留的,并通過類似mmap(3)的虛擬內(nèi)存調(diào)用從操作系統(tǒng)按需提交內(nèi)存隅忿。這種情況發(fā)生在2MB大小的區(qū)域(在64位平臺上)。
這些映射區(qū)域作為節(jié)點保存在名為VirtualSpaceList
的全局鏈接列表中弊仪。
每個節(jié)點管理一個高水位線颓鲜,將已提交的空間與仍然未提交的空間分開黍匾。當分配達到最高水位線時,將按需提交新頁面升薯。為了避免過于頻繁地調(diào)用操作系統(tǒng)莱褒,保留了一點空間。
直到節(jié)點完全用完為止涎劈。然后广凸,分配一個新節(jié)點并將其添加到列表中。舊節(jié)點正在“失效”蛛枚。
內(nèi)存是從名為MetaChunk
的塊節(jié)點分配的谅海。它們有三種尺寸,分別命名為specialized蹦浦、small和medium—命名具有歷史意義—通常為1K/4K/64K
VirtualSpaceList
及其節(jié)點是全局結(jié)構(gòu)扭吁,而Metachunk
由一個類裝入器擁有。因此盲镶,VirtualSpaceList
中的單個節(jié)點可能包含來自不同類裝入器的塊:
當一個類裝入器及其所有相關(guān)的類被卸載時侥袜,用于保存其類元數(shù)據(jù)的元空間將被釋放。所有現(xiàn)在可用的塊都添加到全局可用列表(ChunkManager):
這些塊被重用:如果另一個類裝入器開始加載類并分配元空間溉贿,則可能會給它一個空閑塊枫吧,而不是分配一個新的塊:
Metaspace中間層:Metachunk
類裝入器從Metaspace請求內(nèi)存以獲取一段元數(shù)據(jù)(通常是少量的,大約幾十或幾百個字節(jié))顽照,比如200個字節(jié)由蘑。它將得到一個Metachunk——一塊通常比請求的內(nèi)存大得多的內(nèi)存。
為什么代兵?因為直接從全局VirtualSpaceList分配內(nèi)存非常昂貴尼酿。VirtualSpaceList是一個全局結(jié)構(gòu),需要鎖定植影。我們不想經(jīng)常這樣做裳擎,所以會給加載器一塊更大的內(nèi)存——這個Metachunk——加載程序?qū)⑹褂盟斓貪M足將來的分配,同時不鎖定其他加載程序思币。只有當塊用完時鹿响,加載程序才會再次困擾全局VirtualSpaceList。
元空間分配器如何決定要交給加載器的塊有多大谷饿?好吧惶我,都是猜測:
- 新啟動的標準加載程序?qū)@得小的4K塊,直到達到任意閾值(4)博投,在該閾值時绸贡,元空間分配器明顯地失去了耐心,并開始給加載程序提供更大的64K塊。
- 引導類加載器被稱為加載程序听怕,它傾向于加載許多類捧挺。所以分配器從一開始就給它一個巨大的塊(4M)。這可以通過
InitialBootClassLoaderMetaspaceSize
進行調(diào)整尿瞭。 - 反射類加載器(
jdk.internal.reflect.DelegatingClassLoader
)和匿名類的類裝入器3已知只能加載一個類闽烙。因此,他們從一開始就得到非常小的(1K)塊声搁,因為假設(shè)他們很快就不再需要元空間黑竞,再給他們?nèi)魏螙|西都是浪費。
請注意酥艳,整個優(yōu)化——在假定加載程序很快就會需要它的情況下摊溶,為它提供比當前需要更多的空間——是對該加載程序未來分配行為的賭注,可能是正確的充石,也可能是不正確的莫换。一旦分配器給它們一大塊,它們就可能停止加載骤铃。
This is basically like feeding cats, or small children. The small ones you give a small amount of food on the plate, for the large ones you pile it on, and both cats and children may surprise you at any moment by dropping the spoon (the children, not the cats) and walking away, leaving half-eaten plates of memory behind. The penalty for guessing wrong is wasted memory.
Metaspace上層:元塊Metablock
在Metachunk
中拉岁,我們有第二個類裝入器本地分配器。它將元塊分割成小的分配單元惰爬。這些單元稱為元塊喊暖,是傳遞給調(diào)用者的實際單元(例如,元塊包含一個InstanceKlass
)撕瞧。
此類裝入器本地分配器可以是原始的陵叽,因此速度很快:
類元數(shù)據(jù)的生存期被綁定到類加載器上,當類裝入器死亡時丛版,它將被批量釋放巩掺。因此,JVM不需要關(guān)心釋放隨機元塊4页畦。與一般用途的malloc(3)分配器不同胖替。
讓我們來檢查一下Metachunk:
當它出生時,它只包含頭豫缨。隨后的分配只是在頂部分配独令。由于整塊元數(shù)據(jù)都可以被釋放,所以不能再依賴于整塊的分配好芭。
注意當前塊的“未使用”部分:由于塊屬于一個類裝入器燃箭,所以該部分只能由同一個裝入器使用。如果加載程序停止加載類舍败,那么這個空間實際上是浪費了招狸。
ClassloaderData和ClassLoaderMetaspace
類裝入器將其本機表示形式保存在名為ClassLoaderData
的本機結(jié)構(gòu)中碗硬。
該結(jié)構(gòu)引用了一個ClassLoaderMetaspace
結(jié)構(gòu),該結(jié)構(gòu)保存了該加載程序使用的所有元塊的列表瓢颅。
當加載程序被卸載時,關(guān)聯(lián)的ClassLoaderData
及其ClassLoaderMetaspace
將被刪除弛说。這會將類裝入器使用的所有塊釋放到元空間空閑列表中挽懦。如果條件正確,可能會或不會導致內(nèi)存釋放到操作系統(tǒng)木人,請參閱:http://javakk.com/160.html
匿名類
類加載器數(shù)據(jù) != ClassLoaderMetaspace
注意我們一直在說“元空間內(nèi)存由它的類加載器擁有”——但這里我們有點撒謊信柿,這是一種簡化。隨著匿名類的增加醒第,情況變得更加復雜:
這些是為動態(tài)語言支持而生成的構(gòu)造渔嚷。當裝入器加載匿名類時,該類將獲得自己的獨立ClassLoaderData
稠曼,其生存期與匿名類的生存期耦合形病,而不是宿主類裝入器(因此,可以在收集housing loader
之前收集它及其關(guān)聯(lián)的元數(shù)據(jù))霞幅。這意味著類裝入器對所有正常加載的類都有一個主類裝入器數(shù)據(jù)漠吻,而每個匿名類都有一個輔助類裝入器數(shù)據(jù)結(jié)構(gòu)。
這種分離的目的是為了不必要地延長Lambdas
和方法句柄之類的元空間分配的壽命司恳。
那么途乃,再說一次:內(nèi)存何時返回操作系統(tǒng)?
讓我們再看看內(nèi)存何時返回操作系統(tǒng)扔傅。我們現(xiàn)在可以比第1部分末尾更詳細地回答這個問題:
當一個VirtualSpaceListNode
中的所有塊碰巧是空閑的時耍共,該節(jié)點本身將被移除。該節(jié)點將從VirtualSpaceList
中刪除猎塞。它的空閑塊從Metaspace空閑列表中移除试读。節(jié)點被取消映射,其內(nèi)存返回給操作系統(tǒng)邢享。節(jié)點被“清除”鹏往。
為了使一個節(jié)點中的所有塊都是空閑的,擁有這些塊的所有類裝入器都必須已經(jīng)死亡骇塘。
這是否可能在很大程度上取決于碎片化:
一個節(jié)點的大小是2MB伊履;塊的大小從1K-64K不等;通常每個節(jié)點的負載是150-200塊款违。如果這些塊都是由一個類裝入器分配的唐瀑,那么收集該裝入器將釋放節(jié)點并將其內(nèi)存釋放給操作系統(tǒng)。
但是插爹,如果這些塊由具有不同生命周期的不同類裝入器擁有哄辣,則不會釋放任何內(nèi)容请梢。當我們處理許多小類裝入器(例如匿名類的裝入器或反射委托器)時,可能會出現(xiàn)這種情況力穗。
另外毅弧,請注意,部分Metaspace(壓縮類空間)將永遠不會釋放回操作系統(tǒng)当窗。
- 內(nèi)存由操作系統(tǒng)在2MB大小的區(qū)域中保留够坐,并保存在全局鏈接列表中。這些地區(qū)承諾按需提供服務崖面。
- 這些區(qū)域被分割成塊元咙,然后交給類裝入器。塊屬于一個類裝入器巫员。
- 塊被進一步分割成微小的分配庶香,稱為塊。這些是分發(fā)給呼叫者的分配單元简识。
- 當一個全局塊被重新使用時赶掖,它擁有一個全局塊。部分內(nèi)存可能會被釋放到操作系統(tǒng)中财异,但這在很大程度上取決于碎片化和運氣倘零。
文章來源:http://javakk.com/395.html
也歡迎大家關(guān)注我的公眾號【Java老K】獲取更多干貨