1. 程式人生 > >Java PermGen 去哪裡了?

Java PermGen 去哪裡了?

原文連結:原文作者:Monica Beckwith  以下為本人翻譯,僅用於交流學習,版權歸原作者和InfoQ所有,轉載註明出處,請不要用於商業用途

在Java虛擬機器(JVM)內部,class檔案中包括類的版本、欄位、方法、介面等描述資訊,還有執行時常量池,用於存放編譯器生成的各種字面量和符號引用。

在過去(自定義類載入器還不是很常見的時候),類大多是”static”的,很少被解除安裝或收集,因此被稱為“永久的(Permanent)”。同時,由於類class是JVM實現的一部分,並不是由應用建立的,所以又被認為是“非堆(non-heap)”記憶體。

在JDK8之前的HotSpot JVM,存放這些”永久的”的區域叫做“永久代(permanent generation)”。永久代是一片連續的堆空間,在JVM啟動之前通過在命令列設定引數-XX:MaxPermSize來設定永久代最大可分配的記憶體空間,預設大小是64M(64位JVM由於指標膨脹,預設是85M)。永久代的垃圾收集是和老年代(old generation)捆綁在一起的,因此無論誰滿了,都會觸發永久代和老年代的垃圾收集。不過,一個明顯的問題是,當JVM載入的類資訊容量超過了引數-XX:MaxPermSize設定的值時,應用將會報OOM的錯誤(對於這句話,譯者的理解是:32位的JVM預設MaxPermSize是64M,而JDK8裡的Metaspace,也可以通過引數-XX:MetaspaceSize 和-XX:MaxMetaspaceSize設定大小,但如果不指定MaxMetaspaceSize的話,Metaspace的大小僅受限於native memory的剩餘大小。也就是說永久代的最大空間一定得有個指定值,而如果MaxPermSize指定不當,就會OOM)。

注:在JDK7之前的版本,對於HopSpot JVM,interned-strings儲存在永久代(又名PermGen),會導致大量的效能問題和OOM錯誤。從PermGen移除interned strings的更多資訊檢視這裡

譯者注:從JDK7開始永久代的移除工作,貯存在永久代的一部分資料已經轉移到了Java Heap或者是Native Heap。但永久代仍然存在於JDK7,並沒有完全的移除:符號引用(Symbols)轉移到了native heap;字面量(interned strings)轉移到了java heap;類的靜態變數(class statics)轉移到了java heap。

在JDK7 update 4即隨後的版本中,提供了完整的支援對於Garbage-First(G1)垃圾收集器,以取代在JDK5中釋出的CMS收集器。使用G1,PermGen僅僅在FullGC(stop-the-word,STW)時才會被收集。G1僅僅在PermGen滿了或者應用分配記憶體的速度比G1併發垃圾收集速度快的時候才觸發FullGC。

而對於CMS收集器,通過開啟布林引數-XX:+CMSClassUnloadingEnabled來併發對PermGen進行收集。對於G1沒有類似的選項,G1只能通過FullGC,stop the world,來對PermGen進行收集。

永久代在JDK8中被完全的移除了。所以永久代的引數-XX:PermSize和-XX:MaxPermSize也被移除了。

在JDK8中,classe metadata(the virtual machines internal presentation of Java class),被儲存在叫做Metaspace的native memory。一些新的flags被加入:
-XX:MetaspaceSize,class metadata的初始空間配額,以bytes為單位,達到該值就會觸發垃圾收集進行型別解除安裝,同時GC會對該值進行調整:如果釋放了大量的空間,就適當的降低該值;如果釋放了很少的空間,那麼在不超過MaxMetaspaceSize(如果設定了的話),適當的提高該值。
-XX:MaxMetaspaceSize,可以為class metadata分配的最大空間。預設是沒有限制的。
-XX:MinMetaspaceFreeRatio,在GC之後,最小的Metaspace剩餘空間容量的百分比,減少為class metadata分配空間導致的垃圾收集
-XX:MaxMetaspaceFreeRatio,在GC之後,最大的Metaspace剩餘空間容量的百分比,減少為class metadata釋放空間導致的垃圾收集

預設情況下,class metadata的分配僅受限於可用的native memory總量。可以使用MaxMetaspaceSize來限制可為class metadata分配的最大記憶體。當class metadata的使用的記憶體達到MetaspaceSize(32位clientVM預設12Mbytes,32位ServerVM預設是16Mbytes)時就會對死亡的類載入器和類進行垃圾收集。設定MetaspaceSize為一個較高的值可以推遲垃圾收集的發生。

Native Heap,就是C-Heap。對於32位的JVM,C-Heap的容量=4G-Java Heap-PermGen;對於64位的JVM,C-Heap的容量=物理伺服器的總RAM+虛擬記憶體-Java Heap-PermGen

這裡科普下,在Windows下稱為虛擬記憶體(virtual memory),在Linux下稱為交換空間(swap space),用於當系統需要更多的記憶體資源而實體記憶體已經滿了的情況下,將實體記憶體中不活躍的頁轉移到磁碟上的交換空間中。

在JDK8,Native Memory,包括Metaspace和C-Heap。

IBM的J9和Oracle的JRockit(收購BEA公司的JVM)都沒有永久代的概念,而Oracle移除HotSpot中的永久代的原因之一是為了與JRockit合併,以充分利用各自的特點。

再見,再見PermGen,你好Metaspace

隨著JDK8的到來,JVM不再有PermGen。但類的元資料資訊(metadata)還在,只不過不再是儲存在連續的堆空間上,而是移動到叫做“Metaspace”的本地記憶體(Native memory)中。

類的元資料資訊轉移到Metaspace的原因是PermGen很難調整。PermGen中類的元資料資訊在每次FullGC的時候可能會被收集,但成績很難令人滿意。而且應該為PermGen分配多大的空間很難確定,因為PermSize的大小依賴於很多因素,比如JVM載入的class的總數,常量池的大小,方法的大小等。

此外,在HotSpot中的每個垃圾收集器需要專門的程式碼來處理儲存在PermGen中的類的元資料資訊。從PermGen分離類的元資料資訊到Metaspace,由於Metaspace的分配具有和Java Heap相同的地址空間,因此Metaspace和Java Heap可以無縫的管理,而且簡化了FullGC的過程,以至將來可以並行的對元資料資訊進行垃圾收集,而沒有GC暫停。

永久代的移除對終端使用者意味著什麼?

由於類的元資料可以在本地記憶體(native memory)之外分配,所以其最大可利用空間是整個系統記憶體的可用空間。這樣,你將不再會遇到OOM錯誤,溢位的記憶體會湧入到交換空間。終端使用者可以為類元資料指定最大可利用的本地記憶體空間,JVM也可以增加本地記憶體空間來滿足類元資料資訊的儲存。

注:永久代的移除並不意味者類載入器洩露的問題就沒有了。因此,你仍然需要監控你的消費和計劃,因為記憶體洩露會耗盡整個本地記憶體,導致記憶體交換(swapping),這樣只會變得更糟。

移動到Metaspace和它的記憶體分配

Metaspace VM利用記憶體管理技術來管理Metaspace。這使得由不同的垃圾收集器來處理類元資料的工作,現在僅僅由Metaspace VM在Metaspace中通過C++來進行管理。Metaspace背後的一個思想是,類和它的元資料的生命週期是和它的類載入器的生命週期一致的。也就是說,只要類的類載入器是存活的,在Metaspace中的類元資料也是存活的,不能被釋放。

之前我們不嚴格的使用這個術語“Metaspace”。更正式的,每個類載入器儲存區叫做“a metaspace”。這些metaspaces一起總體稱為”the Metaspace”。僅僅當類載入器不在存活,被垃圾收集器宣告死亡後,該類載入器對應的metaspace空間才可以回收。Metaspace空間沒有遷移和壓縮。但是元資料會被掃描是否存在Java引用。

Metaspace VM使用一個塊分配器(chunking allocator)來管理Metaspace空間的記憶體分配。塊的大小依賴於類載入器的型別。其中有一個全域性的可使用的塊列表(a global free list of chunks)。當類載入器需要一個塊的時候,類載入器從全域性塊列表中取出一個塊,新增到它自己維護的塊列表中。當類載入器死亡,它的塊將會被釋放,歸還給全域性的塊列表。塊(chunk)會進一步被劃分成blocks,每個block儲存一個元資料單元(a unit of metadata)。Chunk中Blocks的分配線性的(pointer bump)。這些chunks被分配在記憶體對映空間(memory mapped(mmapped) spaces)之外。在一個全域性的虛擬記憶體對映空間(global virtual mmapped spaces)的連結串列,當任何虛擬空間變為空時,就將該虛擬空間歸還回作業系統。

上面這幅圖展示了Metaspace使用metachunks在mmapeded virual spaces分配的情形。類載入器1和3描述的是反射或匿名類載入器,使用“特定的”chunk尺寸。類載入器2和4使用小還是中等的chunk尺寸取決於載入的類數量。

Metaspace大小的調整和可以使用的工具

正如前面提到了,Metaspace VM管理Metaspace空間的增長。但有時你會想通過在命令列顯示的設定引數-XX:MaxMetaspaceSize來限制Metaspace空間的增長。預設情況下,-XX:MaxMetaspaceSize並沒有限制,因此,在技術上,Metaspace的尺寸可以增長到交換空間,而你的本地記憶體分配將會失敗。

對於64位的伺服器端JVM,-XX:MetaspaceSize的預設大小是21M。這是初始的限制值(the initial high watermark)。一旦達到這個限制值,FullGC將會被觸發進行類解除安裝(當這些類的類載入器不再存活時),然後這個high watermark被重置。新的high watermark的值依賴於空餘Metaspace的容量。如果沒有足夠的空間被釋放,high watermark的值將會上升;如果釋放了大量的空間,那麼high watermark的值將會下降。如果初始的watermark設定的太低,這個過程將會進行多次。你可以通過垃圾收集日誌來顯示的檢視這個垃圾收集的過程。所以,一般建議在命令列設定一個較大的值給XX:MetaspaceSize來避免初始時的垃圾收集。

每次垃圾收集之後,Metaspace VM會自動的調整high watermark,推遲下一次對Metaspace的垃圾收集。

這兩個引數,-XX:MinMetaspaceFreeRatio和-XX:MaxMetaspaceFreeRatio,類似於GC的FreeRatio引數,可以放在命令列。

針對Metaspace,JDK自帶的一些工具做了修改來展示Metaspace的資訊:

  • jmap -clstats :列印類載入器的統計資訊(取代了在JDK8之前列印類載入器資訊的permstat)。一個例子的輸出當執行DaCapo’s Avrora基準測試:
$ jmap -clstats <PID>
Attaching to process ID 6476, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.5-b02
finding class loader instances ..done.
computing per loader stat ..done.
please wait.. computing liveness.liveness analysis may be inaccurate ...
class_loader classes bytes parent_loader alive? type 
<bootstrap\> 655 1222734 null live <internal> 
0x000000074004a6c0000x000000074004a708dead java/util/ResourceBundle$RBClassLoader@0x00000007c0053e20
0x000000074004a76000 null dead sun/misc/Launcher$ExtClassLoader@0x00000007c002d248 0x00000007401189c8 1 1471
0x00000007400752f8dead sun/reflect/[email protected]0x00000007c0009870 0x000000074004a708116 3160530x000000074004a760 dead sun/misc/Launcher$AppClassLoader@0x00000007c0038190 
0x00000007400752f8538 7738540x000000074004a708 dead org/dacapo/harness/[email protected]0x00000007c00638b0 
total = 6 1310 2314112 N/A alive=1, dead=5 N/A
  • jstat -gc :Metaspace的資訊也會被打印出來,如下面的例子所示:
  • jcmd GC.class_stats:這是一個新的診斷命令,可以使使用者連線到存活的JVM,轉儲Java類元資料的詳細統計。

注:在JDK8 build 13下,需要開啟引數-XX:+UnlockDiagnosticVMOptions

$ jcmd <PID> help GC.class_stats
9522:
GC.class_stats 
Provide statistics about Java class meta data. Requires -XX:+UnlockDiagnosticVMOptions. 
Impact: High: Depends on Java heap size and content. 
Syntax : GC.class_stats [options] [<columns>] 
Arguments: 
  columns : [optional] Comma-separated list of all the columns to show. If not specified, the following columns are shown: InstBytes,KlassBytes,CpAll,annotations,MethodCount,Bytecodes,MethodAll,ROAll,RWAll,Total (STRING, no default value) 
Options: (options must be specified using the <key> or <key>=<value> syntax) 
  -all : [optional] Show all columns (BOOLEAN, false) 
  -csv : [optional] Print in CSV (comma-separated values) format for spreadsheets (BOOLEAN, false) 
  -help : [optional] Show meaning of all the columns (BOOLEAN, false)

注:對於列的更多資訊,請檢視這裡
一個輸出列子:

$ jcmd <PID> GC.class_stats 
7140:
Index Super InstBytes KlassBytes annotations CpAll MethodCount Bytecodes MethodAll ROAll RWAll Total ClassName 
1 -1 426416 480 0 0 0 0 0 24 576 600 [C 
2 -1 290136 480 0 0 0 0 0 40 576 616 [Lavrora.arch.legacy.LegacyInstr; 
3 -1 269840 480 0 0 0 0 0 24 576 600 [B 
4 43 137856 648 0 19248 129 4886 25288 16368 30568 46936 java.lang.Class 
5 43 136968 624 0 8760 94 4570 33616 12072 32000 44072 java.lang.String 
6 43 75872 560 0 1296 7 149 1400 880 2680 3560 java.util.HashMap$Node 
7 836 57408 608 0 720 3 69 1480 528 2488 3016 avrora.sim.util.MulticastFSMProbe 
8 43 55488 504 0 680 1 31 440 280 1536 1816 avrora.sim.FiniteStateMachine$State 
9 -1 53712 480 0 0 0 0 0 24 576 600 [Ljava.lang.Object; 
10 -1 49424 480 0 0 0 0 0 24 576 600 [I 
11 -1 49248 480 0 0 0 0 0 24 576 600 [Lavrora.sim.platform.ExternalFlash$Page; 
12 -1 24400 480 0 0 0 0 0 32 576 608 [Ljava.util.HashMap$Node; 
13 394 21408 520 0 600 3 33 1216 432 2080 2512 avrora.sim.AtmelInterpreter$IORegBehavior 
14 727 19800 672 0 968 4 71 1240 664 2472 3136 avrora.arch.legacy.LegacyInstr$MOVW 
…<snipped> 
…<snipped> 
1299 1300 0 608 0 256 1 5 152 104 1024 1128 sun.util.resources.LocaleNamesBundle 
1300 1098 0 608 0 1744 10 290 1808 1176 3208 4384 sun.util.resources.OpenListResourceBundle 
1301 1098 0 616 0 2184 12 395 2200 1480 3800 5280 sun.util.resources.ParallelListResourceBundle 
        2244312 794288 2024 2260976 12801 561882 3135144 1906688 4684704 6591392 Total 
        34.0% 12.1% 0.0% 34.3% - 8.5% 47.6% 28.9% 71.1% 100.0% 
Index Super InstBytes KlassBytes annotations CpAll MethodCount Bytecodes MethodAll ROAll RWAll Total ClassName

當前的問題

先前提到的,Metaspace VM使用塊分配器(chunking allocator)。chunk的大小取決於類載入器的型別。由於類class並沒有一個固定的尺寸,這就存在這樣一種可能:可分配的chunk的尺寸和需要的chunk的尺寸不相等,這就會導致記憶體碎片。Metaspace VM還沒有使用壓縮技術,所以記憶體碎片是現在的一個主要關注的問題。