JVM原始碼分析之Metaspace解密

PerfMa發表於2020-07-15
本文來自: PerfMa技術社群

PerfMa(笨馬網路)官網

概述

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

通過這篇文章,你將可以瞭解到

  • 為什麼會有metaspace
  • metaspace的組成
  • metaspace的VM引數
  • jstat裡我們應該關注metaspace的哪些值

為什麼會有metaspace

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

我們都知道jdk8之前有perm這一整塊記憶體來存klass等資訊,我們的引數裡也必不可少地會配置-XX:PermSize以及-XX:MaxPermSize來控制這塊記憶體的大小,jvm在啟動的時候會根據這些配置來分配一塊連續的記憶體塊,但是隨著動態類載入的情況越來越多,這塊記憶體我們變得不太可控,到底設定多大合適是每個開發者要考慮的問題,如果設定太小了,系統執行過程中就容易出現記憶體溢位,設定大了又總感覺浪費,儘管不會實質分配這麼大的實體記憶體。基於這麼一個可能的原因,於是metaspace出現了,希望記憶體的管理不再受到限制,也不要怎麼關注後設資料這塊的OOM問題,雖然到目前來看,也並沒有完美地解決這個問題。

或許從JVM程式碼裡也能看出一些端倪來,比如MaxMetaspaceSize預設值很大,CompressedClassSpaceSize預設也有1G,從這些引數我們能猜到metaspace的作者不希望出現它相關的OOM問題。

metaspace的組成

metaspace其實由兩大部分組成

  • Klass Metaspace
  • NoKlass Metaspace

Klass Metaspace就是用來存klass的,klass是我們熟知的class檔案在jvm裡的執行時資料結構,不過有點要提的是我們看到的類似A.class其實是存在heap裡的,是java.lang.Class的一個物件例項。這塊記憶體是緊接著Heap的,和我們之前的perm一樣,這塊記憶體大小可通過-XX:CompressedClassSpaceSize引數來控制,這個引數前面提到了預設是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的一些行為,我們一般會對其相關的一些引數做調整,因為metaspace的引數本身不是很多,所以我這裡將涉及到的所有引數都做一個介紹,也許好些引數大家都是有誤解的

  • UseLargePagesInMetaspace
  • InitialBootClassLoaderMetaspaceSize
  • MetaspaceSize
  • MaxMetaspaceSize
  • CompressedClassSpaceSize
  • MinMetaspaceExpansion
  • MaxMetaspaceExpansion
  • MinMetaspaceFreeRatio
  • MaxMetaspaceFreeRatio

UseLargePagesInMetaspace

預設false,這個引數是說是否在metaspace裡使用LargePage,一般情況下我們使用4KB的page size,這個引數依賴於UseLargePages這個引數開啟,不過這個引數我們一般不開。

InitialBootClassLoaderMetaspaceSize

64位下預設4M,32位下預設2200K,metasapce前面已經提到主要分了兩大塊,Klass Metaspace以及NoKlass Metaspace,而NoKlass Metaspace是由一塊塊記憶體組合起來的,這個引數決定了NoKlass Metaspace的第一個記憶體Block的大小,即2*InitialBootClassLoaderMetaspaceSize,同時為bootstrapClassLoader的第一塊記憶體chunk分配了InitialBootClassLoaderMetaspaceSize的大小。

MetaspaceSize

預設20.8M左右(x86下開啟c2模式),主要是控制metaspaceGC發生的初始閾值,也是最小閾值,但是觸發metaspaceGC的閾值是不斷變化的,與之對比的主要是指Klass Metaspace與NoKlass Metaspace兩塊committed的記憶體和。

MaxMetaspaceSize

預設基本是無窮大,但是我還是建議大家設定這個引數,因為很可能會因為沒有限制而導致metaspace被無止境使用(一般是記憶體洩漏)而被OS Kill。這個引數會限制metaspace(包括了Klass Metaspace以及NoKlass Metaspace)被committed的記憶體大小,會保證committed的記憶體不會超過這個值,一旦超過就會觸發GC,這裡要注意和MaxPermSize的區別,MaxMetaspaceSize並不會在jvm啟動的時候分配一塊這麼大的記憶體出來,而MaxPermSize是會分配一塊這麼大的記憶體的。

CompressedClassSpaceSize

預設1G,這個引數主要是設定Klass Metaspace的大小,不過這個引數設定了也不一定起作用,前提是能開啟壓縮指標,假如-Xmx超過了32G,壓縮指標是開啟不來的。如果有Klass Metaspace,那這塊記憶體是和Heap連著的。

MinMetaspaceExpansion

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

這兩個引數和擴容其實並沒有直接的關係,也就是並不是為了增大committed的記憶體,而是為了增大觸發metaspace GC的閾值

這兩個引數主要是在比較特殊的場景下救急使用,比如gcLocker或者should_concurrent_collect的一些場景,因為這些場景下接下來會做一次GC,相信在接下來的GC中可能會釋放一些metaspace的記憶體,於是先臨時擴大下metaspace觸發GC的閾值,而有些記憶體分配失敗其實正好是因為這個閾值觸頂導致的,於是可以通過增大閾值暫時繞過去

預設332.8K,增大觸發metaspace GC閾值的最小要求。假如我們要救急分配的記憶體很小,沒有達到MinMetaspaceExpansion,但是我們會將這次觸發metaspace GC的閾值提升MinMetaspaceExpansion,之所以要大於這次要分配的記憶體大小主要是為了防止別的執行緒也有類似的請求而頻繁觸發相關的操作,不過如果要分配的記憶體超過了MaxMetaspaceExpansion,那MinMetaspaceExpansion將會是要分配的記憶體大小基礎上的一個增量。

MaxMetaspaceExpansion

預設5.2M,增大觸發metaspace GC閾值的最大要求。假如說我們要分配的記憶體超過了MinMetaspaceExpansion但是低於MaxMetaspaceExpansion,那增量是MaxMetaspaceExpansion,如果超過了MaxMetaspaceExpansion,那增量是MinMetaspaceExpansion加上要分配的記憶體大小

注:每次分配只會給對應的執行緒一次擴充套件觸發metaspace GC閾值的機會,如果擴充套件了,但是還不能分配,那就只能等著做GC了。

MinMetaspaceFreeRatio

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。

MaxMetaspaceFreeRatio

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

jstat裡的metaspace欄位

我們看GC是否異常,除了通過GC日誌來做分析之外,我們還可以通過jstat這樣的工具展示的資料來分析。

我們通過jstat可以看到metaspace相關的這麼一些指標,分別是MCCSMCMUCCSCCCSUMCMNMCMXCCSMNCCSMX

它們的定義如下:

column {
    header "^M^"  /* Metaspace - Percent Used */
    data (1-((sun.gc.metaspace.capacity - sun.gc.metaspace.used)/sun.gc.metaspace.capacity)) * 100
    align right
    width 6
    scale raw
    format "0.00"
  }
  column {
    header "^CCS^"    /* Compressed Class Space - Percent Used */
    data (1-((sun.gc.compressedclassspace.capacity - sun.gc.compressedclassspace.used)/sun.gc.compressedclassspace.capacity)) * 100
    align right
    width 6
    scale raw
    format "0.00"
  }

  column {
    header "^MC^" /* Metaspace Capacity - Current */
    data sun.gc.metaspace.capacity
    align center
    width 6
    scale K
    format "0.0"
  }
  column {
    header "^MU^" /* Metaspae Used */
    data sun.gc.metaspace.used
    align center
    width 6
    scale K
    format "0.0"
  }
   column {
    header "^CCSC^"   /* Compressed Class Space Capacity - Current */
    data sun.gc.compressedclassspace.capacity
    width 8
    align right
    scale K
    format "0.0"
  }
  column {
    header "^CCSU^"   /* Compressed Class Space Used */
    data sun.gc.compressedclassspace.used
    width 8
    align right
    scale K
    format "0.0"
  }
  column {
    header "^MCMN^"   /* Metaspace Capacity - Minimum */
    data sun.gc.metaspace.minCapacity
    scale K
    align right
    width 8
    format "0.0"
  }
  column {
    header "^MCMX^"   /* Metaspace Capacity - Maximum */
    data sun.gc.metaspace.maxCapacity
    scale K
    align right
    width 8
    format "0.0"
  }
  column {
    header "^CCSMN^"    /* Compressed Class Space Capacity - Minimum */
    data sun.gc.compressedclassspace.minCapacity
    scale K
    align right
    width 8
    format "0.0"
  }
  column {
    header "^CCSMX^"  /* Compressed Class Space Capacity - Maximum */
    data sun.gc.compressedclassspace.maxCapacity
    scale K
    align right
    width 8
    format "0.0"
  }

我這裡對這些欄位分類介紹下

MC & MU & CCSC & CCSU

  • MC表示Klass Metaspace以及NoKlass Metaspace兩者總共committed的記憶體大小,單位是KB,雖然從上面的定義裡我們看到了是capacity,但是實質上計算的時候並不是capacity,而是committed,這個是要注意的
  • MU這個無可厚非,說的就是Klass Metaspace以及NoKlass Metaspace兩者已經使用了的記憶體大小
  • CCSC表示的是Klass Metaspace的已經被commit的記憶體大小,單位也是KB
  • CCSU表示Klass Metaspace的已經被使用的記憶體大小

M & CCS

  • M表示的是Klass Metaspace以及NoKlass Metaspace兩者總共的使用率
  • CCS表示的是NoKlass Metaspace的使用率,也就是CCSU/CCSC算出來的

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

MCMN & MCMX & CCSMN & CCSMX

  • MCMN和CCSMN這兩個值大家可以忽略,一直都是0
  • MCMX表示Klass Metaspace以及NoKlass Metaspace兩者總共的reserved的記憶體大小,比如預設情況下Klass Metaspace是通過CompressedClassSpaceSize這個引數來reserved 1G的記憶體,NoKlass Metaspace預設reserved的記憶體大小是2* InitialBootClassLoaderMetaspaceSize
  • CCSMX表示Klass Metaspace reserved的記憶體大小

綜上所述,其實看metaspace最主要的還是看MC,MU,CCSC,CCSU這幾個具體的大小來判斷metaspace到底用了多少更靠譜
本來還想寫metaspace記憶體分配和GC的內容,不過那塊說起來又是一個比較大的話題,因為那塊大家看起來可能會比較枯燥,有機會再寫

一起來學習吧

PerfMa KO 系列課之 JVM 引數【Memory篇】

YGC問題排查,又讓我漲姿勢了!

相關文章