天天看點

北上廣深杭30K試題:JVM記憶體模型如何配置設定的?

01 JVM記憶體模型的劃分

由于我們生産環境使用的虛拟機HotSpot 居多,是以下面的描述都是基于HotSpot 虛拟機而言的,對于其他類型的虛拟機,如 JRockit(Oracle)、J9(IBM) 可能并不太一樣

根據虛拟機規範,JVM的記憶體分為 堆、虛拟機棧、本地方法棧、程式計數器、本地方法棧5部分

JDK 1.8 同 JDK 1.7 比,最大的差别就是:中繼資料區取代了永久代。元空間的本質和永久代類似,都是對 JVM 規範中方法區的實作。不過元空間與永久代之間最大的差別在于:中繼資料空間并不在虛拟機中,而是使用本地記憶體

北上廣深杭30K試題:JVM記憶體模型如何配置設定的?

image.png

推薦:NB大廠連環問,JVM到骨髓(基礎-面試-調優),瞬間漲薪3K不成問題!

1.1 虛拟機棧

每個線程有一個私有的棧,随着線程的建立而建立。棧裡面存着的是一種叫“棧幀”的東西,每個方法會建立一個棧幀,棧幀中存放了局部變量表(基本資料類型和對象引用)、操作數棧、方法出口等資訊。棧的大小可以固定也可以動态擴充。當棧調用深度大于JVM所允許的範圍,會抛出StackOverflowError的錯誤

虛拟機棧的特點

  • 局部變量表随着棧幀的建立而建立,它的大小在編譯時确定,建立時隻需配置設定事先規定的大小即可。在方法運作過程中,局部變量表的大小不會發生改變
  • Java 虛拟機棧也是線程私有,随着線程建立而建立,随着線程的結束而銷毀
  • Java 虛拟機棧會出現兩種異常:StackOverFlowError (若 Java 虛拟機棧的大小不允許動态擴充,那麼當線程請求棧的深度超過目前 Java 虛拟機棧的最大深度時,抛出 StackOverFlowError 異常)和 OutOfMemoryError(若允許動态擴充,那麼當線程請求棧時記憶體用完了,無法再動态擴充時,抛出 OutOfMemoryError 異常)
北上廣深杭30K試題:JVM記憶體模型如何配置設定的?

image.png

1.2 本地方法棧

本地方法棧是為 JVM 運作 Native 方法準備的空間,由于很多 Native 方法都是用 C 語言實作的,是以它通常又叫 C 棧。它與 Java 虛拟機棧實作的功能類似,隻不過本地方法棧是描述本地方法運作過程的記憶體模型。本地方法被執行時,在本地方法棧也會建立一塊棧幀,用于存放該方法的局部變量表、操作數棧、動态連結、方法出口資訊等。方法執行結束後,相應的棧幀也會出棧,并釋放記憶體空間。也會抛出 StackOverFlowError 和 OutOfMemoryError 異常。

1.3 PC 寄存器計數器

PC 寄存器,也叫程式計數器。JVM支援多個線程同時運作,每個線程都有自己的程式計數器。倘若目前執行的是 JVM 的方法,則該寄存器中儲存目前執行指令的位址;倘若執行的是native 方法,則PC寄存器中為空

1.4 堆

堆記憶體是 JVM 所有線程共享的部分,在虛拟機啟動的時候就已經建立。所有的對象和數組都在堆上進行配置設定。這部分空間可通過 GC 進行回收。當申請不到空間時會抛出 OutOfMemoryError

堆的特點:

  • 線程共享,整個 Java 虛拟機隻有一個堆,所有的線程都通路同一個堆。而程式計數器、Java 虛拟機棧、本地方法棧都是一個線程對應一個
  • 在虛拟機啟動時建立
  • 垃圾回收的主要場所
  • 可分為:新生代(Eden區 From Survior To Survivor)、老年代

1.5 方法區

  • Java 虛拟機規範中定義方法區是堆的一個邏輯部分。
  • 方法區也是所有線程共享。主要用于存儲已經被虛拟機加載的類的資訊、常量池、方法資料、方法代碼等。方法區邏輯上屬于堆的一部分,但是為了與堆進行區分,通常又叫 非堆 。需要注意的是,方法區隻是規範上面的一個邏輯概念,并不是真實的實體存儲的命名

1.6 直接記憶體(堆外記憶體)

直接記憶體是除 Java 虛拟機之外的記憶體,但也可能被 Java 使用,直接記憶體的大小不受 Java 虛拟機控制,但既然是記憶體,當記憶體不足時就會抛出 OutOfMemoryError 異常

直接記憶體與堆記憶體比較:

  • 直接記憶體申請空間耗費更高的性能
  • 直接記憶體讀取 IO 的性能要優于普通的堆記憶體

02 JVM記憶體模型各部分的存儲資訊

北上廣深杭30K試題:JVM記憶體模型如何配置設定的?

image.png

03 針對JDK6、JDK7、JDK8三個版本的JVM記憶體模型調整說明

3.1 對永久代PermGen的說明

  • 永久代是方法區在hotspot的一個具體實作。通過在運作時資料區域開辟空間實作方法區。
  • hotspot jdk7 之前的永久代,比較完整
  • 從jdk7以後方法區就“四分五裂了”,不再是在單一的一個去區域内進行存儲
  • 在jdk8,移除了永久代,被Metsspace取代了,且Metsspace不在JVM堆内,放入了本地記憶體,元空間也就成了方法區的主要存放位置

絕大部分 Java 程式員應該都見過 java.lang.OutOfMemoryError: PermGen space 這個異常

這裡的 “PermGen space”其實指的就是方法區。不過方法區和“PermGen space”又有着本質的差別。前者是 JVM 的規範,而後者則是 JVM 規範的一種實作,并且隻有 HotSpot 才有 “PermGen space”,而對于其他類型的虛拟機,如 JRockit(Oracle)、J9(IBM) 并沒有“PermGen space”。由于方法區主要存儲類的相關資訊,是以對于動态生成類的情況比較容易出現永久代的記憶體溢出。最典型的場景就是,在 jsp 頁面比較多的情況,容易出現永久代記憶體溢出

3.2 對Metaspace元空間的說明

-XX:MetaspaceSize,初始空間大小,達到該值就會觸發垃圾收集進行類型解除安裝,同時GC會對該值進行調整:如果釋放了大量的空間,就适當降低該值;如果釋放了很少的空間,那麼在不超過MaxMetaspaceSize時,适當提高該值。
-XX:MaxMetaspaceSize,最大空間,預設是沒有限制的。
除了上面兩個指定大小的選項以外,還有兩個與 GC 相關的屬性:
-XX:MinMetaspaceFreeRatio,在GC之後,最小的Metaspace剩餘空間容量的百分比,減少為配置設定空間所導緻的垃圾收集
-XX:MaxMetaspaceFreeRatio,在GC之後,最大的Metaspace剩餘空間容量的百分比,減少為釋放空間所導緻的垃圾收集      
  • 符号引用(Symbols)轉移到了native heap;
  • 字面量(interned strings)轉移到了java heap;
  • 類的靜态變量(class statics)轉移到了java heap

3.3 元空間特點

  • 類及相關的中繼資料的生命周期與類加載器的一緻,如果GC發現某個類加載器不再存活了,才會把相關的空間整個回收掉
  • 每個類加載器有專門的存儲空間
  • 隻進行線性配置設定,省掉了GC掃描及壓縮的時間
  • 元空間裡的對象的位置是固定的

04 JVM記憶體劃分調整的幾個原因點分析

  • 字元串存在永久代中,容易出現性能問題和記憶體溢出
  • 類及方法的資訊等比較難确定其大小,是以對于永久代的大小指定比較困難,太小容易出現永久代溢出,太大則容易導緻老年代溢出
  • 永久代會為 GC 帶來不必要的複雜度,并且回收效率偏低