JVM元空間Metaspace的記憶體結構
我們深入研究元空間的架構。我們描述了各個層和元件,以及它們是如何協同工作的。
這對那些想要破解hotspot
和Metaspace
或者至少真正理解記憶體的去向以及為什麼我們不能僅僅使用malloc
的人來說是很有趣的。
與大多數其他非平凡的分配器一樣,元空間是在層中實現的。
在底部,記憶體是在作業系統的大區域中分配的。在中間,我們將這些區域分割成不太大的塊,然後交給類裝入器。
在頂部,類裝入器將這些塊分割為呼叫程式程式碼。
元空間的底層:虛擬空間列表VirtualSpaceList
VirtualSpaceList:
在最底層(在最粗的粒度上),Metaspace的記憶體是保留的,並通過類似mmap(3)
的虛擬記憶體呼叫從作業系統按需提交記憶體。這種情況發生在2MB大小的區域(在64位平臺上)。
這些對映區域作為節點儲存在名為VirtualSpaceList的全域性連結列表中。
每個節點管理一個高水位線,將已提交的空間與仍然未提交的空間分開。當分配達到最高水位線時,將按需提交新頁面。為了避免過於頻繁地呼叫作業系統,保留了一點空間。
直到節點完全用完為止。然後,分配一個新節點並將其新增到列表中。舊節點正在“失效”。
記憶體是從名為MetaChunk
的塊節點分配的。它們有三種尺寸,分別命名為specialized
small
和medium
—命名具有歷史意義—通常為1K/4K/64K
VirtualSpaceList
及其節點是全域性結構,而Metachunk
由一個類裝入器擁有。因此,VirtualSpaceList
中的單個節點可能包含來自不同類裝入器的塊:
當一個類裝入器及其所有相關的類被解除安裝時,用於儲存其類元資料的元空間將被釋放。所有現在可用的塊都新增到全域性可用列表(ChunkManager
):
這些塊被重用:如果另一個類裝入器開始載入類並分配元空間,則可能會給它一個空閒塊,而不是分配一個新的塊:
Metaspace中間層:Metachunk
類裝入器從Metaspace請求記憶體以獲取一段元資料(通常是少量的,大約幾十或幾百個位元組),比如200個位元組。它將得到一個Metachunk——一塊通常比請求的記憶體大得多的記憶體。
為什麼?因為直接從全域性VirtualSpaceList分配記憶體非常昂貴。VirtualSpaceList是一個全域性結構,需要鎖定。我們不想經常這樣做,所以會給載入器一塊更大的記憶體——這個Metachunk——載入程式將使用它更快地滿足將來的分配,同時不鎖定其他載入程式。只有當塊用完時,載入程式才會再次困擾全域性VirtualSpaceList。
元空間分配器如何決定要交給載入器的塊有多大?好吧,都是猜測:
- 新啟動的標準載入程式將獲得小的4K塊,直到達到任意閾值(4),在該閾值時,元空間分配器明顯地失去了耐心,並開始給載入程式提供更大的64K塊。
- 引導類載入器被稱為載入程式,它傾向於載入許多類。所以分配器從一開始就給它一個巨大的塊(4M)。這可以通過
InitialBootClassLoaderMetaspaceSize
進行調整。 - 反射類載入器(
jdk.internal.reflect.DelegatingClassLoader
)和匿名類的類裝入器3已知只能載入一個類。因此,他們從一開始就得到非常小的(1K)塊,因為假設他們很快就不再需要元空間,再給他們任何東西都是浪費。
請注意,整個優化——在假定載入程式很快就會需要它的情況下,為它提供比當前需要更多的空間——是對該載入程式未來分配行為的賭注,可能是正確的,也可能是不正確的。一旦分配器給它們一大塊,它們就可能停止載入。
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
中,我們有第二個類裝入器本地分配器。它將元塊分割成小的分配單元。這些單元稱為元塊,是傳遞給呼叫者的實際單元(例如,元塊包含一個InstanceKlass
)。
此類裝入器本地分配器可以是原始的,因此速度很快:
類元資料的生存期被繫結到類載入器上,當類裝入器死亡時,它將被批量釋放。因此,JVM不需要關心釋放隨機元塊4。與一般用途的malloc(3)
分配器不同。
讓我們來檢查一下Metachunk:
當它出生時,它只包含頭。隨後的分配只是在頂部分配。由於整塊元資料都可以被釋放,所以不能再依賴於整塊的分配。
注意當前塊的“未使用”部分:由於塊屬於一個類裝入器,所以該部分只能由同一個裝入器使用。如果載入程式停止載入類,那麼這個空間實際上是浪費了。
ClassloaderData和ClassLoaderMetaspace
類裝入器將其本機表示形式儲存在名為ClassLoaderData
的本機結構中。
該結構引用了一個ClassLoaderMetaspace
結構,該結構儲存了該載入程式使用的所有元塊的列表。
當載入程式被解除安裝時,關聯的ClassLoaderData
及其ClassLoaderMetaspace
將被刪除。這會將類裝入器使用的所有塊釋放到元空間空閒列表中。如果條件正確,可能會或不會導致記憶體釋放到作業系統,具體案例和排查可參考這篇:http://javakk.com/160.html
匿名類
類載入器資料 != ClassLoaderMetaspace
注意我們一直在說“元空間記憶體由它的類載入器擁有”——但這裡我們有點撒謊,這是一種簡化。隨著匿名類的增加,情況變得更加複雜:
這些是為動態語言支援而生成的構造。當裝入器載入匿名類時,該類將獲得自己的獨立ClassLoaderData
,其生存期與匿名類的生存期耦合,而不是宿主類裝入器(因此,可以在收集housing loader
之前收集它及其關聯的元資料)。這意味著類裝入器對所有正常載入的類都有一個主類裝入器資料,而每個匿名類都有一個輔助類裝入器資料結構。
這種分離的目的是為了不必要地延長Lambdas
和方法控制代碼之類的元空間分配的壽命。
那麼,再說一次:記憶體何時返回作業系統?
讓我們再看看記憶體何時返回作業系統。我們現在可以比第1部分末尾更詳細地回答這個問題:
當一個VirtualSpaceListNode
中的所有塊碰巧是空閒的時,該節點本身將被移除。該節點將從VirtualSpaceList
中刪除。它的空閒塊從Metaspace
空閒列表中移除。節點被取消對映,其記憶體返回給作業系統。節點被“清除”。
為了使一個節點中的所有塊都是空閒的,擁有這些塊的所有類裝入器都必須已經死亡。
這是否可能在很大程度上取決於碎片化:
一個節點的大小是2MB;塊的大小從1K-64K不等;通常每個節點的負載是150-200塊。如果這些塊都是由一個類裝入器分配的,那麼收集該裝入器將釋放節點並將其記憶體釋放給作業系統。
但是,如果這些塊由具有不同生命週期的不同類裝入器擁有,則不會釋放任何內容。當我們處理許多小類裝入器(例如匿名類的裝入器或反射委託器)時,可能會出現這種情況。
另外,請注意,部分Metaspace(壓縮類空間)將永遠不會釋放回作業系統。
- 記憶體由作業系統在2MB大小的區域中保留,並儲存在全域性連結列表中。這些地區承諾按需提供服務。
- 這些區域被分割成塊,然後交給類裝入器。塊屬於一個類裝入器。
- 塊被進一步分割成微小的分配,稱為塊。這些是分發給呼叫者的分配單元。
- 當一個全域性塊被重新使用時,它擁有一個全域性塊。部分記憶體可能會被釋放到作業系統中,但這在很大程度上取決於碎片化和運氣。
文章來源:http://javakk.com/395.html
也歡迎大家關注我的公眾號【Java老K】獲取更多幹貨