1. 程式人生 > 實用技巧 >JVM-永久代與元空間

JVM-永久代與元空間

永久代

在自定義類載入器還不是很常見的時候,類大多是static的,很少被解除安裝或收集,因此被成為“永久的(Permanent)”。

同時,由於類class是JVM實現的一部分,並不是由應用建立的,所以又被認為是“非堆(Non-Heap)”記憶體。

  • 在JDK8之前的HotSpot JVM,存放這些“永久的”區域叫做“永久代(permanent generation)”。
  • 永久代是一片連續的堆空間,在JVM啟動之前通過在命令列設定引數-XX:MaxPermSize
    -XX:MaxPermSize:永久代的最大可分配記憶體空間。預設大小64M(64位JVM由於質真膨脹,預設是85M)。當JVM載入的類資訊容量超過了引數設定的值時,應用將會報OOM的錯誤
  • 永久代的垃圾收集和老年代(old generation)捆綁在一起,因此無論哪個滿了,都會出發永久代和老年代的垃圾收集。
  • java.lang.OutOfMemoryError:PremGen space
    這裡的“PermGen space”其實指的就是方法區。不過方法區和“PermGen space”又有著本質的區別。前者是JVM的規範,而後者則是JVM規範的一種實現。並且只有HotSpot才有“PermGen space”,而對於其他型別的虛擬機器,如JRockit(Oracle)、J9(IBM)並沒有“PermGen space”。
  • 由於方法區主要儲存類的相關資訊,所以對於動態生成類的情況比較容易出現永久代的記憶體溢位。並且JDK 1.8中引數PermSize和MaxPermSize已經失效

移除永久代的工作從JDK1.7就開始了。JDK1.7中,儲存在永久代的部分資料就已經轉移到了Java Heap或者是 Native Heap。但永久代仍存在於JDK1.7中,並沒完全移除

注意:永久代的移除並不代表自定義的類載入器洩露問題就解決了。因此,你還必須監控你的記憶體消耗情況,因為一旦發生洩漏,會佔用你的大量本地記憶體,並且還可能導致交換區交換更加糟糕。

元空間(metaspace)

JDK 8的HotSpot JVM現在使用的是本地記憶體來表示類的元資料,這個區域就叫做元空間。

元空間這個東西,是在JDK8以後才存在的,JDK7及以前,只有永久代
元空間的儲存位置是在計算機的記憶體當中,而永久的儲存們置是在JVM的地堆中

每一個類載入器的儲存區域都稱作一個元空間所有的元空間合在一起就是我們一直說的元空間

只有class元資料才在元空間。

持久代的空間被徹底地刪除了,它被一個叫元空間的區域所替代了。持久代刪除了之後,很明顯,JVM會忽略PermSize和MaxPermSize這兩個引數,還有就是你再也看不到java.lang.OutOfMemoryError: PermGen error的異常了。原來類的靜態變數和Interned Strings 都被轉移到了java堆區,

元空間記憶體管理

元空間的記憶體管理由元空間虛擬機器來完成。先前,對於類的元資料我們需要不同的垃圾回收器進行處理,現在只需要執行元空間虛擬機器的C++程式碼即可完成。在元空間中,類和其元資料的生命週期和其對應的類載入器是相同的。話句話說,只要類載入器存活,其載入的類的元資料也是存活的,因而不會被回收掉。

JVM用元空間代替永久代的原因:
1隨著作業系統的發展,計算機支援的記憶體從32位的最大2^32位元組,變為64位的最大
2^48位元組。

2隨著Java 在Web領域的發展,java程式變的得越來越大,需要載入的內容也越來越多,如果
使用永久代實現方法區,那麼需要手動擴大堆的大小,而使用元空間之後,就可以直接存
儲在記憶體當中,不用手動雲修改堆的大小。

主要原因:

  • 字串存在永久代中,容易出現效能問題和記憶體溢位, 由於永久代記憶體經常不夠用或發生記憶體洩露,爆出異常 java.lang.OutOfMemoryError: PermGen
  • 類及方法的資訊等比較難確定其大小,因此對於永久代的大小指定比較困難,太小容易出現永久代溢位,太大則容易導致老年代溢位。
  • 永久代會為 GC 帶來不必要的複雜度,並且回收效率偏低。
  • Oracle 可能會將HotSpot 與 JRockit 合二為一。
  • 永久代空間大小很難確定,太小容易GC/OOM異常,太大佔用記憶體(元空間並不在虛擬機器中、而是使用本地記憶體,大小僅受本地記憶體限制)
  • 永久代調優困難
  • 垃圾回收頻率低

移除永久代是為融合HotSpot JVM與 JRockit VM而做出的努力,因為JRockit沒有永久代,不需要配置永久代

元空間的特點:

  • 充分利用了Java語言規範中的好處:類及相關的元資料的生命週期與類載入器的一致。
  • 每個載入器有專門的儲存空間
  • 只進行線性分配
  • 不會單獨回收某個類
  • 省掉了GC掃描及壓縮的時間
  • 元空間裡的物件的位置是固定的
  • 如果GC發現某個類載入器不再存活了,會把相關的空間整個回收掉

元空間的記憶體分配模型

  • 絕大多數的類元資料的空間都從本地記憶體中分配
  • 用來描述類元資料的類也被刪除了
  • 分元資料分配了多個虛擬記憶體空間
  • 給每個類載入器分配一個記憶體塊的列表。塊的大小取決於類載入器的型別; sun/反射/代理對應的類載入器的塊會小一些
  • 歸還記憶體塊,釋放記憶體塊列表
  • 一旦元空間的資料被清空了,虛擬記憶體的空間會被回收掉
  • 減少碎片的策略

元空間的調優

使用-XX:MaxMetaspaceSize引數可以設定元空間的最大值,預設是沒有上限的,也就是說你的系統記憶體上限是多少它就是多少。-XX:MetaspaceSize選項指定的是元空間的初始大小,如果沒有指定的話,元空間會根據應用程式執行時的需要動態地調整大小。

MaxMetaspaceSize的調優

  • -XX:MaxMetaspaceSize={unlimited}
  • 元空間的大小受限於你機器的記憶體
  • 限制類的元資料使用的記憶體大小,以免出現虛擬記憶體切換以及本地記憶體分配失敗。如果懷疑有類載入器出現洩露,應當使用這個引數;32位機器上,如果地址空間可能會被耗盡,也應當設定這個引數。
  • 元空間的初始大小是21M——這是GC的初始的高水位線,超過這個大小會進行Full GC來進行類的回收。
  • 如果啟動後GC過於頻繁,請將該值設定得大一些
  • 可以設定成和持久代一樣的大小,以便推遲GC的執行時間

CompressedClassSpaceSize的調優

  • 只有當-XX:+UseCompressedClassPointers開啟了才有效
  • -XX:CompressedClassSpaceSize=1G
  • 由於這個大小在啟動的時候就固定了的,因此最好設定得大點。
  • 沒有使用到的話不要進行設定
  • JVM後續可能會讓這個區可以動態的增長。不需要是連續的區域,只要從基地址可達就行;可能會將更多的類元資訊放回到元空間中;未來會基於PredictedLoadedClassCount的值來自動的設定該空間的大小

元空間的一些工具

  • jmap -permstat改成了jmap -clstats。它用來列印Java堆的類載入器的統計資料。對每一個類載入器,會輸出它的名字,是否存活,地址,父類載入器,以及它已經載入的類的數量及大小。除此之外,駐留的字串(intern)的數量及大小也會打印出來。
  • jstat -gc,這個命令輸出的是元空間的資訊而非持久代的
  • jcmd GC.class_stats提供類元資料大小的詳細資訊。使用這個功能啟動程式時需要加上-XX:+UnlockDiagnosticVMOptions選項。

提高GC的效能

如果你理解了元空間的概念,很容易發現GC的效能得到了提升。

  • Full GC中,元資料指向元資料的那些指標都不用再掃描了。很多複雜的元資料掃描的程式碼(尤其是CMS裡面的那些)都刪除了。
  • 元空間只有少量的指標指向Java堆。這包括:類的元資料中指向java/lang/Class例項的指標;陣列類的元資料中,指向java/lang/Class集合的指標。
  • 沒有元資料壓縮的開銷
  • 減少了根物件的掃描(不再掃描虛擬機器裡面的已載入類的字典以及其它的內部雜湊表)
  • 減少了Full GC的時間
  • G1回收器中,併發標記階段完成後可以進行類的解除安裝

總結

    • Hotspot中的元資料現在儲存到了元空間裡。mmap中的記憶體塊的生命週期與類載入器的一致。
    • 類指標壓縮空間(Compressed class pointer space)目前仍然是固定大小的,但它的空間較大
    • 可以進行引數的調優,不過這不是必需的。
    • 未來可能會增加其它的優化及新特性。比如, 應用程式類資料共享;新生代GC優化,G1回收器進行類的回收;減少元資料的大小,以及JVM內部物件的記憶體佔用量。