天天看點

【JVM】靈性一問——為什麼用元空間替換永久代?

前言

首先需要明确的是,以下我們讨論的HotSpot虛拟機,其他類型的虛拟機,例如JRockit與J9等,壓根就沒有永久代的概念。是以,下面所說的“虛拟機”都是HotSpot版本的。

要想了解這種變化的原因,需要先了解方法區、永久代與元空間的概念與之間的關系。

方法區與永久代,元空間之間的關系

方法區是一種規範,不同的虛拟機廠商可以基于規範做出不同的實作,永久代和元空間就是出于不同jdk版本的實作。

說白了,方法區就像是一個接口,永久代與元空間分别是兩個不同的實作類而已。隻不過永久代是這個接口最初的實作類,後來這個接口一直進行變更,直到最後徹底廢棄這個實作類,由新實作類——元空間進行替代。

方法區

借用《深入了解Java虛拟機——JVM進階特性與最佳實踐》中介紹方法區的段落

方法區和堆一樣,是各個線程共享的記憶體區域,它用于存儲已被虛拟機加載的類資訊、常量、靜态變量、即時編譯後的代碼等資料。

Java7及以前版本的永久代的結構

在Java7及以前的版本,是存在永久代的。在Java7版本時,永久代已經發生了悄悄的變化。等到Java8時,徹底廢棄了永久代,由元空間替換。

永久代與堆的構造如下:

【JVM】靈性一問——為什麼用元空間替換永久代?

(關于堆中的Eden、from與to區域,可以先參考我的另外一篇文章​​【JVM】說說java中的堆區​​)

從上圖中可以看到,永久代與堆中的老年代是連續的,這裡的連續指的是實體位址連續,永久代本身并不在堆中。是以,老年代與永久代其中一個滿了,都會觸發Full GC。

我們可以使用以下的指令,來顯示指定永久代的大小:

  • -XX:PremSize:設定永久代的初始大小
  • -XX:MaxPermSize: 設定永久代的最大值

由于方法區主要存儲類的相關資訊,是以對于動态生成類的情況比較容易出現永久代的記憶體溢出。最典型的場景就是,在 jsp 頁面比較多的情況,容易出現永久代記憶體溢出,會報出"java.lang.OutOfMemoryError: PermGen space "異常。

Java7時,永久代的變化

在Java7時,仍然有永久代,永久代也與堆中的老年代連續,但永久代中存儲的部分資料已經開始轉移到Java Heap或Native Memory中了,比如:

  • 符号引用(Symbols)轉移到了Native Memory
  • 字元串常量池(interned strings)轉移到了Java Heap
  • 類的靜态變量(class statics)轉移到了Java Heap

現在分别在Java6、7、8環境中循環調用String.intern()方法(關于此方法,可以先移步到我的另外一篇文章中​​【JAVA】String源碼淺談​​,裡面有對此方法的介紹與實驗),那麼分别會報出以下區域的記憶體溢出異常

  • Java6時,記憶體溢出區域為永久代。因為在Java6及之前,字元串常量池在永久代中
  • Java7時,記憶體溢出區域為堆中。因為在Java7時,字元串常量池被移到堆中了。
  • Java8時,記憶體溢出區域仍然為堆中,不過此時已經沒有永久代了。

Java8開始,永久代就已經消失了,由元空間取而代之。

元空間

元空間(Metaspace),不再與堆連續,而是直接存在于本地記憶體中,也就是機器的記憶體。理論上機器記憶體有多大,元空間的野心就有多大。但可以通過以下的參數來設定元空間的大小:

  • -XX:MetaspaceSize,初始空間大小,達到該值就會觸發垃圾收集進行類型解除安裝,同時GC會對該值進行調整:如果釋放了大量的空間,就适當降低該值;如果釋放了很少的空間,那麼在不超過MaxMetaspaceSize時,适當提高該值。
  • -XX:MaxMetaspaceSize,最大空間,預設是沒有限制的。

除了上面兩個指定大小的選項以外,還有兩個與 GC 相關的屬性:

  • -XX:MinMetaspaceFreeRatio,在GC之後,最小的Metaspace剩餘空間容量的百分比,減少為配置設定空間所導緻的垃圾收集
  • -XX:MaxMetaspaceFreeRatio,在GC之後,最大的Metaspace剩餘空間容量的百分比,減少為釋放空間所導緻的垃圾收集

在使用-XX:MaxMetaspaceSize顯示指定元空間的大小為一個比較小的值,接着在循環中動态加載過多的類,那麼會報出"java.lang.OutOfMemoryError: Metaspace"異常。

Java8中,使用元空間替換永久代的原因