圖文並茂,帶你認識 JVM 執行時資料區
跨平臺的本質
關於 JVM, Java 程式設計師的最熟悉的一句話就是:一處編碼,到處執行,指的就是 Java 語言可以通過 JVM 實現跨平臺。而跨平臺到底跨越了什麼這個問題相信很少有人知道,接下來就跟我一起了解一下吧。
下圖展示了兩種不同的彙編風格,除此之外還有 ARM 彙編(主要應用於移動平臺)。不同平臺擁有不同的編譯器,暫存器,識別不同的指令。例如圖片最後一行將 8 賦值給變數 eax 就有不同的寫法。正是因為彙編指令的不同,才造成了平臺之間的不相容性
而我們的 JVM 就充當了位元組碼檔案根據不同平臺翻譯成不同彙編指令的翻譯官,解決了跨平臺的問題
宣告:本文首發於部落格園,作者:後青春期的Keats;地址:
為什麼眾多語言選擇 JVM
目前市面上已經有包括 Java、Kotlin、Groovy 等 10 多種語言都基於 JVM 運行了,不同的語言能夠使用其編譯器將原始碼通通編譯成 .class 位元組碼檔案然後交給 JVM。為什麼它們都選擇 JVM 呢?
- JVM 擁有優秀的記憶體管理模型,JDK8 及以前分代記憶體模型非常成熟、而且可以配合各種各樣垃圾回收器做垃圾回收
- 位元組碼指令非常精簡 + 執行引擎效率非常高
- 類載入器系統安全、可擴充套件
- 高效能 + 低延遲的垃圾回收器
Java 是短暫的,JVM 是永恆的 --- 魯班學院子牙老師
JVM 記憶體模型
瞭解 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 中每個執行緒都有自己的程式計數器來確保執行緒切換後能回到準確的位置
虛擬機器棧
關鍵字:執行緒私有 生命週期同線程 棧幀
每個執行緒都有自己的虛擬機器棧,區域性變數是儲存在虛擬機器棧中的,因此不存在併發問題
每個虛擬機器棧又有很多個棧幀,每個方法在執行的時候,虛擬機器會同步建立一個棧幀,包含了:區域性變量表、運算元棧、動態連結、方法出口/返回地址(恢復現場)、附加資訊 這些部分
- 區域性變量表:儲存該方法編譯期可知的各種 Java 基本型別、物件引用和 returnAddress 型別。這些資料型別在區域性變量表中的儲存空間以區域性變數槽(Slot)來表示,其中 64 位長度的 long、double 型別佔 2 個槽,其餘的資料型別佔 1 個
- 運算元棧:變數賦值操作等號右邊的部分
- 動態連結:指向該方法在方法區的地址,虛擬機器圖示中虛擬機器棧指向方法區的棕色箭頭便是表示的動態連結
- 返回地址(恢復現場) 該方法彈棧後,恢復現場進行了如下的操作
- 區域性變量表指標重置
- 運算元棧指標重置
- 返回值壓棧
- 該方法佔用的棧幀記憶體回收
- 程式計數器的數值重置
- 附加資訊:HotSpot 並沒有對此進行實現
當執行緒請求的棧深度大於虛擬機器允許的深度時,會報 StackOverflowError 異常
當無法申請到足夠的記憶體時,會 OOM
本地方法棧
和虛擬機器棧類似,區別在於執行的是 native 方法,HotSpot 中把本地方法棧和虛擬機器棧合二為一
Java堆
關鍵字:執行緒共享 垃圾分代收集
所有的物件例項及陣列幾乎都在堆中分配
為什麼老年代的空間是新生代的 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: https://www.bilibili.com/video/BV1BC4y187Ti?p=19
《深入理解Java虛擬機器》--- 周志明
碼字不易,如果你覺得讀完以後有收穫,不妨點個推薦讓更多的人看到吧!