天天看點

JVM源碼分析之Metaspace解密

metaspace,顧名思義,中繼資料空間,專門用來存中繼資料的,它是jdk8裡特有的資料結構用來替代perm,這塊空間很有自己的特點,前段時間公司這塊的問題太多了,主要是因為更新了中間件所緻,看到大家讨論來讨論去,看得出很多人對metaspace還是模棱兩可,不是很了解它,是以我覺得有必要寫篇文章來介紹一下它,解開它神秘的面紗,當我們再次碰到它的相關問題的時候不會再感到束手無策。

通過這篇文章,你将可以了解到

為什麼會有metaspace

metaspace的組成

metaspace的vm參數

jstat裡我們應該關注metaspace的哪些值

metaspace的由來民間已有很多傳說,不過我這裡隻談我自己的了解,因為我不是oracle參與這塊的開發者,是以對其真正的由來不怎麼了解。

我們都知道jdk8之前有perm這一整塊記憶體來存klass等資訊,我們的參數裡也必不可少地會配置-xx:permsize以及-xx:maxpermsize來控制這塊記憶體的大小,jvm在啟動的時候會根據這些配置來配置設定一塊連續的記憶體塊,但是随着動态類加載的情況越來越多,這塊記憶體我們變得不太可控,到底設定多大合适是每個開發者要考慮的問題,如果設定太小了,系統運作過程中就容易出現記憶體溢出,設定大了又總感覺浪費,盡管不會實質配置設定這麼大的實體記憶體。基于這麼一個可能的原因,于是metaspace出現了,希望記憶體的管理不再受到限制,也不要怎麼關注中繼資料這塊的oom問題,雖然到目前來看,也并沒有完美地解決這個問題。

或許從jvm代碼裡也能看出一些端倪來,比如<code>maxmetaspacesize</code>預設值很大,<code>compressedclassspacesize</code>預設也有1g,從這些參數我們能猜到metaspace的作者不希望出現它相關的oom問題。

metaspace其實由兩大部分組成

klass metaspace

noklass metaspace

klass metaspace就是用來存klass的,klass是我們熟知的class檔案在jvm裡的運作時資料結構,不過有點要提的是我們看到的類似a.class其實是存在heap裡的,是java.lang.class的一個對象執行個體。這塊記憶體是緊接着heap的,和我們之前的perm一樣,這塊記憶體大小可通過<code>-xx:compressedclassspacesize</code>參數來控制,這個參數前面提到了預設是1g,但是這塊記憶體也可以沒有,假如沒有開啟壓縮指針就不會有這塊記憶體,這種情況下klass都會存在noklass metaspace裡,另外如果我們把-xmx設定大于32g的話,其實也是沒有這塊記憶體的,因為會這麼大記憶體會關閉壓縮指針開關。還有就是這塊記憶體最多隻會存在一塊。

noklass metaspace專門來存klass相關的其他的内容,比如method,constantpool等,這塊記憶體是由多塊記憶體組合起來的,是以可以認為是不連續的記憶體塊組成的。這塊記憶體是必須的,雖然叫做noklass metaspace,但是也其實可以存klass的内容,上面已經提到了對應場景。

klass metaspace和noklass mestaspace都是所有classloader共享的,是以類加載器們要配置設定記憶體,但是每個類加載器都有一個spacemanager,來管理屬于這個類加載的記憶體小塊。如果klass metaspace用完了,那就會oom了,不過一般情況下不會,noklass mestaspace是由一塊塊記憶體慢慢組合起來的,在沒有達到限制條件的情況下,會不斷加長這條鍊,讓它可以持續工作。

如果我們要改變metaspace的一些行為,我們一般會對其相關的一些參數做調整,因為metaspace的參數本身不是很多,是以我這裡将涉及到的所有參數都做一個介紹,也許好些參數大家都是有誤解的

uselargepagesinmetaspace

initialbootclassloadermetaspacesize

metaspacesize

maxmetaspacesize

compressedclassspacesize

minmetaspaceexpansion

maxmetaspaceexpansion

minmetaspacefreeratio

maxmetaspacefreeratio

預設false,這個參數是說是否在metaspace裡使用largepage,一般情況下我們使用4kb的page size,這個參數依賴于uselargepages這個參數開啟,不過這個參數我們一般不開。

64位下預設4m,32位下預設2200k,metasapce前面已經提到主要分了兩大塊,klass metaspace以及noklass metaspace,而noklass metaspace是由一塊塊記憶體組合起來的,這個參數決定了noklass metaspace的第一個記憶體block的大小,即2*initialbootclassloadermetaspacesize,同時為bootstrapclassloader的第一塊記憶體chunk配置設定了initialbootclassloadermetaspacesize的大小

預設20.8m左右(x86下開啟c2模式),主要是控制metaspacegc發生的初始門檻值,也是最小門檻值,但是觸發metaspacegc的門檻值是不斷變化的,與之對比的主要是指klass metaspace與noklass metaspace兩塊committed的記憶體和。

預設基本是無窮大,但是我還是建議大家設定這個參數,因為很可能會因為沒有限制而導緻metaspace被無止境使用(一般是記憶體洩漏)而被os kill。這個參數會限制metaspace(包括了klass metaspace以及noklass metaspace)被committed的記憶體大小,會保證committed的記憶體不會超過這個值,一旦超過就會觸發gc,這裡要注意和maxpermsize的差別,maxmetaspacesize并不會在jvm啟動的時候配置設定一塊這麼大的記憶體出來,而maxpermsize是會配置設定一塊這麼大的記憶體的。

預設1g,這個參數主要是設定klass metaspace的大小,不過這個參數設定了也不一定起作用,前提是能開啟壓縮指針,假如-xmx超過了32g,壓縮指針是開啟不來的。如果有klass metaspace,那這塊記憶體是和heap連着的。

minmetaspaceexpansion和maxmetaspaceexpansion這兩個參數或許和大家認識的并不一樣,也許很多人會認為這兩個參數不就是記憶體不夠的時候,然後擴容的最小大小嗎?其實不然

這兩個參數和擴容其實并沒有直接的關系,也就是并不是為了增大committed的記憶體,而是為了增大觸發metaspace gc的門檻值

這兩個參數主要是在比較特殊的場景下救急使用,比如gclocker或者<code>should_concurrent_collect</code>的一些場景,因為這些場景下接下來會做一次gc,相信在接下來的gc中可能會釋放一些metaspace的記憶體,于是先臨時擴大下metaspace觸發gc的門檻值,而有些記憶體配置設定失敗其實正好是因為這個門檻值觸頂導緻的,于是可以通過增大門檻值暫時繞過去

預設332.8k,增大觸發metaspace gc門檻值的最小要求。假如我們要救急配置設定的記憶體很小,沒有達到minmetaspaceexpansion,但是我們會将這次觸發metaspace gc的門檻值提升minmetaspaceexpansion,之是以要大于這次要配置設定的記憶體大小主要是為了防止别的線程也有類似的請求而頻繁觸發相關的操作,不過如果要配置設定的記憶體超過了maxmetaspaceexpansion,那minmetaspaceexpansion将會是要配置設定的記憶體大小基礎上的一個增量

預設5.2m,增大觸發metaspace gc門檻值的最大要求。假如說我們要配置設定的記憶體超過了minmetaspaceexpansion但是低于maxmetaspaceexpansion,那增量是maxmetaspaceexpansion,如果超過了maxmetaspaceexpansion,那增量是minmetaspaceexpansion加上要配置設定的記憶體大小

注:每次配置設定隻會給對應的線程一次擴充觸發metaspace gc門檻值的機會,如果擴充了,但是還不能配置設定,那就隻能等着做gc了

minmetaspacefreeratio和下面的maxmetaspacefreeratio,主要是影響觸發metaspacegc的門檻值

預設40,表示每次gc完之後,假設我們允許接下來metaspace可以繼續被commit的記憶體占到了被commit之後總共committed的記憶體量的minmetaspacefreeratio%,如果這個總共被committed的量比目前觸發metaspacegc的門檻值要大,那麼将嘗試做擴容,也就是增大觸發metaspacegc的門檻值,不過這個增量至少是minmetaspaceexpansion才會做,不然不會增加這個門檻值

這個參數主要是為了避免觸發metaspacegc的門檻值和gc之後committed的記憶體的量比較接近,于是将這個門檻值進行擴大

一般情況下在gc完之後,如果被committed的量還是比較大的時候,換個說法就是離觸發metaspacegc的門檻值比較接近的時候,這個調整會比較明顯

注:這裡不用gc之後used的量來算,主要是擔心可能出現committed的量超過了觸發metaspacegc的門檻值,這種情況一旦發生會很危險,會不斷做gc,這應該是jdk8在某個版本之後才修複的bug

預設70,這個參數和上面的參數基本是相反的,是為了避免觸發metaspacegc的門檻值過大,而想對這個值進行縮小。這個參數在gc之後committed的記憶體比較小的時候并且離觸發metaspacegc的門檻值比較遠的時候,調整會比較明顯

我們看gc是否異常,除了通過gc日志來做分析之外,我們還可以通過jstat這樣的工具展示的資料來分析,前面我公衆号裡有篇文章介紹了jstat這塊的實作,有興趣的可以到我的公衆号<code>你假笨</code>裡去翻閱下jstat的這篇文章。

我們通過jstat可以看到metaspace相關的這麼一些名額,分别是<code>m</code>,<code>ccs</code>,<code>mc</code>,<code>mu</code>,<code>ccsc</code>,<code>ccsu</code>,<code>mcmn</code>,<code>mcmx</code>,<code>ccsmn</code>,<code>ccsmx</code>

它們的定義如下:

我這裡對這些字段分類介紹下

mc表示klass metaspace以及noklass metaspace兩者總共committed的記憶體大小,機關是kb,雖然從上面的定義裡我們看到了是capacity,但是實質上計算的時候并不是capacity,而是committed,這個是要注意的

mu這個無可厚非,說的就是klass metaspace以及noklass metaspace兩者已經使用了的記憶體大小

ccsc表示的是klass metaspace的已經被commit的記憶體大小,機關也是kb

ccsu表示klass metaspace的已經被使用的記憶體大小

m表示的是klass metaspace以及noklass metaspace兩者總共的使用率,其實可以根據上面的四個名額算出來,即(ccsu+mu)/(ccsc+mc)

ccs表示的是noklass metaspace的使用率,也就是ccsu/ccsc算出來的

ps:是以我們有時候看到m的值達到了90%以上,其實這個并不一定說明metaspace用了很多了,因為記憶體是慢慢commit的,是以我們的分母是慢慢變大的,不過當我們committed到一定量的時候就不會再增長了

mcmn和ccsmn這兩個值大家可以忽略,一直都是0

mcmx表示klass metaspace以及noklass metaspace兩者總共的reserved的記憶體大小,比如預設情況下klass metaspace是通過compressedclassspacesize這個參數來reserved 1g的記憶體,noklass metaspace預設reserved的記憶體大小是2* initialbootclassloadermetaspacesize

ccsmx表示klass metaspace reserved的記憶體大小

綜上所述,其實看metaspace最主要的還是看<code>mc</code>,<code>mu</code>,<code>ccsc</code>,<code>ccsu</code>這幾個具體的大小來判斷metaspace到底用了多少更靠譜

本來還想寫metaspace記憶體配置設定和gc的内容,不過那塊說起來又是一個比較大的話題,因為那塊大家看起來可能會比較枯燥,有機會再寫