圖文並茂,帶你認識 JVM 運行時數據區

跨平台的本質

關於 JVM, Java 程式設計師的最熟悉的一句話就是:一處編碼,到處執行,指的就是 Java 語言可以通過 JVM 實現跨平台。而跨平台到底跨越了什麼這個問題相信很少有人知道,接下來就跟我一起了解一下吧。

下圖展示了兩種不同的彙編風格,除此之外還有 ARM 彙編(主要應用於移動平台)。不同平台擁有不同的編譯器,暫存器,識別不同的指令。例如圖片最後一行將 8 賦值給變數 eax 就有不同的寫法。正是因為彙編指令的不同,才造成了平台之間的不兼容性

不同彙編風格

而我們的 JVM 就充當了位元組碼文件根據不同平台翻譯成不同彙編指令的翻譯官,解決了跨平台的問題

JVM

聲明:本文首發於部落格園,作者:後青春期的Keats;地址://www.cnblogs.com/keatsCoder/ 轉載請註明,謝謝!

為什麼眾多語言選擇 JVM

目前市面上已經有包括 Java、Kotlin、Groovy 等 10 多種語言都基於 JVM 運行了,不同的語言能夠使用其編譯器將源程式碼通通編譯成 .class 位元組碼文件然後交給 JVM。為什麼它們都選擇 JVM 呢?

  • JVM 擁有優秀的記憶體管理模型,JDK8 及以前分代記憶體模型非常成熟、而且可以配合各種各樣垃圾回收器做垃圾回收
  • 位元組碼指令非常精簡 + 執行引擎效率非常高
  • 類載入器系統安全、可擴展
  • 高性能 + 低延遲的垃圾回收器

Java 是短暫的,JVM 是永恆的 — 魯班學院子牙老師

JVM 記憶體模型

image-20200729203714735

了解 JVM 記憶體模型,首先要搞清楚這四個概念

  • .class文件:.java 文件通過編譯器編譯後,存儲在硬碟上的文件
  • class content:類載入器將硬碟中的 .class 文件載入到直接記憶體中的那塊區域之後就成為 class content,此時 class content 內容和 class 文件是一樣的
  • class 對象:類載入器基於虛擬機規範將記憶體中的 class content 解析成 class 對象,放入 方法區 中
  • 對象:執行引擎在執行 new 操作的時候,會將 class 對象生成對象,放入 堆 中

方法區

方法區是模型,具體的典型實現有 Hotspot 在 1.8 之前的 永久代實現 和 1.8及以後的 元空間 實現

永久代實現是 HotSpot 的設計團隊選擇把垃圾回收器的分代設計擴展至方法區。使用永久代來實現方法區。使得 Hotspot 的垃圾回收器能像管理 Java 堆一樣管理方法區的這部分記憶體,省去專門為方法區編寫記憶體管理程式碼的工作。 因為永久代有最大記憶體限制,這種設計導致 Java 更容易 oom。而 元空間 策略只要不觸及物理記憶體上限就沒多大事

方法區為什麼由永久代實現改成元空間實現?

在 JDK1.8 以前,市面上的作業系統大部分還是32位,而32位作業系統最大支援 4GB 記憶體,這時如果程式出現死循環或其他原因而瘋狂創建新對象佔用記憶體空間,則硬體記憶體很容易被撐爆。因此 JVM 通過 永久代 實現來管理記憶體,緊緊把我記憶體的使用許可權;隨著硬體的發展,64 位作業系統佔據主流市場,最大支援 256TB 記憶體,市面上主流機器的記憶體也不斷增加。另外 Spring 等框架在一啟動就會創建很多的 class 對象,JVM 管理起來很吃力。因此 JVM 在 JDK 1.8 之後索性放鬆這塊的限制,變成了 元空間 實現

程式計數器

關鍵字:執行緒私有 不會OOM 位元組碼行號指示器

記錄著虛擬機棧中每個方法執行的位置,可以看作是當前執行緒所執行的位元組碼文件的行號指示器。 Java 程式中分支、循環、跳轉、異常處理、執行緒恢復等操作都需要程式計數器才能完成。

Java 虛擬機的多執行緒是由多執行緒輪流切換、分配處理器執行時間來實現的。在任何一個確定的時刻,一個內核都只會執行一個執行緒。因此 Java 中每個執行緒都有自己的程式計數器來確保執行緒切換後能回到準確的位置

虛擬機棧

關鍵字:執行緒私有 生命周期同執行緒 棧幀

image-20200729223707792

每個執行緒都有自己的虛擬機棧,局部變數是存儲在虛擬機棧中的,因此不存在並發問題

每個虛擬機棧又有很多個棧幀,每個方法在執行的時候,虛擬機會同步創建一個棧幀,包含了:局部變數表、操作數棧、動態鏈接、方法出口/返回地址(恢復現場)、附加資訊 這些部分

  • 局部變數表:存儲該方法編譯期可知的各種 Java 基本類型、對象引用和 returnAddress 類型。這些數據類型在局部變數表中的存儲空間以局部變數槽(Slot)來表示,其中 64 位長度的 long、double 類型占 2 個槽,其餘的數據類型占 1 個
  • 操作數棧:變數賦值操作等號右邊的部分
  • 動態鏈接:指向該方法在方法區的地址,虛擬機圖示中虛擬機棧指向方法區的棕色箭頭便是表示的動態鏈接
  • 返回地址(恢復現場) 該方法彈棧後,恢復現場進行了如下的操作
    • 局部變數表指針重置
    • 操作數棧指針重置
    • 返回值壓棧
    • 該方法佔用的棧幀記憶體回收
    • 程式計數器的數值重置
  • 附加資訊:HotSpot 並沒有對此進行實現

當執行緒請求的棧深度大於虛擬機允許的深度時,會報 StackOverflowError 異常

當無法申請到足夠的記憶體時,會 OOM

本地方法棧

和虛擬機棧類似,區別在於運行的是 native 方法,HotSpot 中把本地方法棧和虛擬機棧合二為一

Java堆

關鍵字:執行緒共享 垃圾分代收集

image-20200729212116639

所有的對象實例及數組幾乎都在堆中分配

為什麼老年代的空間是新生代的 2 倍?

老年代存在兩種對象:

  • 在新生代經過 15 次 GC 還沒有被回收的對象
  • 大小超過伊甸園(Eden區)的大對象,直接放到老年代。避免了對象在新生代三個區之間的複製、避免了新生代三個區被撐爆

可以看出老年代存儲的都是大對象 / 老對象。另外老年代也是一種空間擔保機制,避免由於新生代空間的限制導致的記憶體問題。因此需要更大的記憶體空間

新生代 Eden 和 From、To 區的記憶體比例為什麼是 8:1:1

新生成的對象放入 Eden 區,經過一次 GC 後存活就會放到下一個區。根據大部分的數據統計,90% – 95% 的對象逃不過第一次 GC (朝生夕死),取最小值 90%,得到的兩個區的比例是 9:1,但由於這樣回收會產生很多記憶體碎片,導致記憶體有空間但卻不可用。造成了記憶體浪費。因此將新生代再分成兩個區並使用複製演算法,一次釋放一片記憶體。就形成了現在的三個區 8:1:1 的比例

記憶體碎片是在 Eden 產生的還是 From / To?

只要有垃圾回收,就會有碎片產生。

複製演算法的細節是什麼?怎樣避免記憶體碎片的?

複製演算法是將記憶體分為等大的區域,比如 from 和 to,每次回收前只使用其中一個。當進行一次垃圾回收後,這個區域的記憶體被完整釋放。而存活的對象就被複制到另一個區域。這樣就避免了記憶體碎片的產生

虛擬機棧和Java堆的聯繫

當我們在方法中執行這樣一行程式碼:

Person p = new Person();

此時變數 p 會被存儲在虛擬機棧棧幀的局部變數表中,而 Person 對象則存放在堆中。虛擬機圖示中的虛擬機棧指向堆的黃色箭頭則表示 p 到 Person 對象的引用關係

堆和方法區之間的聯繫

堆中存儲的對象的對象頭的類型指針存在方法區中

對於靜態變數,例如 private static Person staticPerson = new Person(); class 對象存儲在方法區中,而他的靜態變數 staticPerson 指向的對象則存儲在堆中

參考文獻

B站魯班學院影片,子牙老師講解 JVM: //www.bilibili.com/video/BV1BC4y187Ti?p=19

《深入理解Java虛擬機》— 周志明

碼字不易,如果你覺得讀完以後有收穫,不妨點個推薦讓更多的人看到吧!

Tags: