天天看點

阿裡雲二面:JVM 方法區和元空間什麼關系?為什麼要将永久代替換為元空間?

昨天,我花了很長時間完善了一下 JavaGuide 上 JVM 部分方法區的相關介紹。

阿裡雲二面:JVM 方法區和元空間什麼關系?為什麼要将永久代替換為元空間?

多提一嘴,為了完善方法區這部分内容的介紹,我看了很多文檔,還特意去扒了一下《深入了解 Java 虛拟機(第 3 版)》勘誤的 issues,簡直看到的腦殼疼。。。

阿裡雲二面:JVM 方法區和元空間什麼關系?為什麼要将永久代替換為元空間?

講真,深挖下來的話細節太多,也沒太大意義(卷不動了)。

這個問題在 Java 面試中還是挺常見的(通常會在面試官問你 JVM 運作時記憶體的時候被提到),但是,面試的時候也不會問的特别細緻,我下面講到的這些基本就夠面試使用了。

我前幾天看好幾個群友分享的面經裡面就有方法區相關的面試題。

這篇文章我就從面試的角度,通過 7 個常見的知識點/面試題來帶你了解方法區:

  1. 什麼是方法區
  2. 方法區和永久代以及元空間有什麼關系?
  3. 方法區常用參數有哪些?
  4. 為什麼要将永久代 (PermGen) 替換為元空間 (MetaSpace) 呢?
  5. 什麼是運作時常量池?
  6. 字元串常量池有什麼作用?
  7. JDK 1.7 為什麼要将字元串常量池移動到堆中?

下面是正文。

什麼是方法區?

方法區屬于是 JVM 運作時資料區域的一塊邏輯區域,是各個線程共享的記憶體區域。

《Java 虛拟機規範》隻是規定了有方法區這麼個概念和它的作用,方法區到底要如何實作那就是虛拟機自己要考慮的事情了。也就是說,在不同的虛拟機實作上,方法區的實作是不同的。

當虛拟機要使用一個類時,它需要讀取并解析 Class 檔案擷取相關資訊,再将資訊存入到方法區。方法區會存儲已被虛拟機加載的 類資訊、字段資訊、方法資訊、常量、靜态變量、即時編譯器編譯後的代碼緩存等資料。

方法區和永久代以及元空間有什麼關系?

方法區和永久代以及元空間的關系很像 Java 中接口和類的關系,類實作了接口,這裡的類就可以看作是永久代和元空間,接口可以看作是方法區,也就是說永久代以及元空間是 HotSpot 虛拟機對虛拟機規範中方法區的兩種實作方式。

并且,永久代是 JDK 1.8 之前的方法區實作,JDK 1.8 及以後方法區的實作便成為元空間。

阿裡雲二面:JVM 方法區和元空間什麼關系?為什麼要将永久代替換為元空間?

方法區常用參數有哪些?

JDK 1.8 之前永久代還沒被徹底移除的時候通常通過下面這些參數來調節方法區大小。

-XX:PermSize=N //方法區 (永久代) 初始大小
-XX:MaxPermSize=N //方法區 (永久代) 最大大小,超過這個值将會抛出 OutOfMemoryError 異常:java.lang.OutOfMemoryError: PermGen
           

複制

相對而言,垃圾收集行為在這個區域是比較少出現的,但并非資料進入方法區後就“永久存在”了。

JDK 1.8 的時候,方法區(HotSpot 的永久代)被徹底移除了(JDK1.7 就已經開始了),取而代之是元空間,元空間使用的是直接記憶體。下面是一些常用參數:

-XX:MetaspaceSize=N //設定 Metaspace 的初始(和最小大小)
-XX:MaxMetaspaceSize=N //設定 Metaspace 的最大大小
           

複制

與永久代很大的不同就是,如果不指定大小的話,随着更多類的建立,虛拟機會耗盡所有可用的系統記憶體。

為什麼要将永久代 (PermGen) 替換為元空間 (MetaSpace) 呢?

下圖來自《深入了解 Java 虛拟機》第 3 版 2.2.5

阿裡雲二面:JVM 方法區和元空間什麼關系?為什麼要将永久代替換為元空間?

1、整個永久代有一個 JVM 本身設定的固定大小上限,無法進行調整,而元空間使用的是直接記憶體,受本機可用記憶體的限制,雖然元空間仍舊可能溢出,但是比原來出現的幾率會更小。

當元空間溢出時會得到如下錯誤:

java.lang.OutOfMemoryError: MetaSpace

你可以使用

-XX:MaxMetaspaceSize

标志設定最大元空間大小,預設值為 unlimited,這意味着它隻受系統記憶體的限制。

-XX:MetaspaceSize

調整标志定義元空間的初始大小如果未指定此标志,則 Metaspace 将根據運作時的應用程式需求動态地重新調整大小。

2、元空間裡面存放的是類的中繼資料,這樣加載多少類的中繼資料就不由

MaxPermSize

控制了, 而由系統的實際可用空間來控制,這樣能加載的類就更多了。

3、在 JDK8,合并 HotSpot 和 JRockit 的代碼時, JRockit 從來沒有一個叫永久代的東西, 合并之後就沒有必要額外的設定這麼一個永久代的地方了。

什麼是運作時常量池?

Class 檔案中除了有類的版本、字段、方法、接口等描述資訊外,還有用于存放編譯期生成的各種字面量(Literal)和符号引用(Symbolic Reference)的常量池表(Constant Pool Table)。常量池表會在類加載後存放到方法區的運作時常量池中。

字面量是源代碼中的固定值的表示法,即通過字面我們就能知道其值的含義。字面量包括整數、浮點數和字元串字面量,符号引用包括類符号引用、字段符号引用、方法符号引用和接口方法符号引用。

運作時常量池的功能類似于傳統程式設計語言的符号表,盡管它包含了比典型符号表更廣泛的資料。

既然運作時常量池是方法區的一部分,自然受到方法區記憶體的限制,當常量池無法再申請到記憶體時會抛出

OutOfMemoryError

錯誤。

JDK1.7 及之後版本的 JVM 已經将運作時常量池從方法區中移了出來,在 Java 堆(Heap)中開辟了一塊區域存放運作時常量池。

🐛 修正(參見:issue747,reference) :
  1. JDK1.7 之前,運作時常量池包含的字元串常量池和靜态變量存放在方法區, 此時 HotSpot 虛拟機對方法區的實作為永久代。
  2. JDK1.7 字元串常量池和靜态變量被從方法區拿到了堆中, 這裡沒有提到運作時常量池,也就是說字元串常量池被單獨拿到堆,運作時常量池剩下的東西還在方法區, 也就是 HotSpot 中的永久代 。
  3. JDK1.8 HotSpot 移除了永久代用元空間(Metaspace)取而代之, 這時候字元串常量池和靜态變量還在堆, 運作時常量池還在方法區, 隻不過方法區的實作從永久代變成了元空間(Metaspace)

字元串常量池有什麼作用?

字元串常量池 是 JVM 為了提升性能和減少記憶體消耗針對字元串(String 類)專門開辟的一塊區域,主要目的是為了避免字元串的重複建立。

String aa = "ab"; // 放在常量池中
String bb = "ab"; // 從常量池中查找
System.out.println(aa==bb);// true
           

複制

JDK1.7 之前運作時常量池邏輯包含字元串常量池存放在方法區。JDK1.7 的時候,字元串常量池被從方法區拿到了堆中。

這裡的字元串其實就是我們前面提到的字元串字面量。在聲明一個字元串字面量時,如果字元串常量池中能夠找到該字元串字面量,則直接傳回該引用。如果找不到的話,則在常量池中建立該字元串字面量的對象并傳回其引用。

相關問題:JVM 常量池中存儲的是對象還是引用呢?- RednaxelaFX - 知乎

JDK 1.7 為什麼要将字元串常量池移動到堆中?

主要是因為永久代(方法區實作)的 GC 回收效率太低,隻有在整堆收集 (Full GC)的時候才會被執行 GC。Java 程式中通常會有大量的被建立的字元串等待回收,将字元串常量池放到堆中,能夠更高效及時地回收字元串記憶體。

總結

一張圖檔帶你看看 JDK1.6 到 JDK1.8 方法區的變化。

阿裡雲二面:JVM 方法區和元空間什麼關系?為什麼要将永久代替換為元空間?

參考資料

[1]issue747: https://github.com/Snailclimb/JavaGuide/issues/747

[2]reference: https://blog.csdn.net/q5706503/article/details/84640762

[3]JVM 常量池中存儲的是對象還是引用呢?- RednaxelaFX - 知乎: https://www.zhihu.com/question/57109429/answer/151717241

·········· END ··············