JVM基礎系列第6講:Java 虛擬機內存結構


看到這里,我相信大家對於一個 Java 源文件是如何變成字節碼文件,以及字節碼文件的含義已經非常清楚了。那么接下來就是讓 Java 虛擬機運行字節碼文件,從而得出我們最終想要的結果了。在這個過程中,Java 虛擬機會加載字節碼文件,將其存入 Java 虛擬機的內存空間中,之后進行一系列的初始化動作,最后運行程序得出結果。

那么字節碼數據在 Java 虛擬機內存中是如何存放的 ?Java 虛擬機在為類實例或成員變量分配內存是如何分配的 ?要解答上面這些問題,我們首先需要了解一下 Java 虛擬機的內存結構。

其實 Java 虛擬機的內存結構並不是官方的說法,在《Java 虛擬機規范》中用的是「運行時數據區」這個術語。但很多時候這個名詞並不是很形象,再加上日積月累的習慣,我們都習慣用虛擬機內存結構這個說法了。

根據《Java 虛擬機規范》中的說法,Java 虛擬機的內存結構可以分為公有和私有兩部分。公有指的是所有線程都共享的部分,指的是 Java 堆、方法區、常量池。私有指的是每個線程的私有數據,包括:PC寄存器、Java 虛擬機棧、本地方法棧。

公有部分:Java堆、方法區、常量池

在 Java 虛擬機中,線程共享部分包括 Java 堆、方法區及常量池。

Java 堆指的是從 JVM 划分出來的一塊區域,這塊區域專門用於 Java 實例對象的內存分配,幾乎所有實例對象都在會這里進行內存的分配。之所以說幾乎是因為有特殊情況,有些時候小對象會直接在棧上進行分配,這種現象我們稱之為「棧上分配」。這里並不深入介紹,后續有章節會介紹。

方法區指的是存儲 Java 類字節碼數據的一塊區域,它存儲了每一個類的結構信息,例如運行時常量池、字段和方法數據、構造方法等。可以看到常量池其實是存放在方法區中的,但《Java 虛擬機規范》將常量池和方法區放在同一個等級上,這點我們知曉即可。

方法區在不同版本的虛擬機有不同的表現形式,例如在 1.7 版本的 HotSpot 虛擬機中,方法區被稱為永久代(Permanent Space),而在 JDK 1.8 中則被稱之為 MetaSpace。

說完這幾個部分的大致作用之后,我們來深入說說 Java 堆。

Java 堆根據對象存活時間的不同,Java 堆還被分為年輕代、老年代兩個區域,年輕代還被進一步划分為 Eden 區、From Survivor 0、To Survivor 1 區。如下圖所示。

當有對象需要分配時,一個對象永遠優先被分配在年輕代的 Eden 區,等到 Eden 區域內存不夠時,Java 虛擬機會啟動垃圾回收。此時 Eden 區中沒有被引用的對象的內存就會被回收,而一些存活時間較長的對象則會進入到老年代。在 JVM 中有一個名為 -XX:MaxTenuringThreshold 的參數專門用來設置晉升到老年代所需要經歷的 GC 次數,即在年輕代的對象經過了指定次數的 GC 后,將在下次 GC 時進入老年代。

這里讓我們思考一個問題:為什么 Java 堆要進行這樣一個區域划分呢?

根據我們的經驗,虛擬機中的對象必然有存活時間長的對象,也有存活時間短的對象,這是一個普遍存在的正態分布規律。如果我們將其混在一起,那么因為存活時間短的對象有很多,那么勢必導致較為頻繁的垃圾回收。而垃圾回收時不得不對所有內存都進行掃描,但其實有一部分對象,它們存活時間很長,對他們進行掃描完全是浪費時間。因此為了提高垃圾回收效率,分區就理所當然了。

另外一個值得我們思考的問題是:為什么默認的虛擬機配置,Eden:from :to = 8:1:1 呢?

其實這是 IBM 公司根據大量統計得出的結果。根據 IBM 公司對對象存活時間的統計,他們發現 80% 的對象存活時間都很短。於是他們將 Eden 區設置為年輕代的 80%,這樣可以減少內存空間的浪費,提高內存空間利用率。

私有部分:PC寄存器、Java 虛擬機棧、本地方法棧

Java 堆以及方法區的數據是共享的,但是有一些部分則是線程私有的。線程私有部分可以分為:PC 寄存器、Java 虛擬機棧、本地方法棧三大部分。

PC 寄存器,顧名思義 Program Counter 寄存器,指的是保存線程當前正在執行的方法。如果這個方法不是 native 方法,那么 PC 寄存器就保存 Java 虛擬機正在執行的字節碼指令地址。如果是 native 方法,那么 PC 寄存器保存的值是 undefined。任意時刻,一條 Java 虛擬機線程只會執行一個方法的代碼,而這個被線程執行的方法稱為該線程的當前方法,其地址被存在 PC 寄存器中。

Java 虛擬機棧,這個棧與線程同時創建,用來存儲棧幀,即存儲局部變量與一些過程結果的地方。棧幀存儲的數據包括:局部變量表、操作數棧。

當 Java 虛擬機使用其他語言(例如 C 語言)來實現指令集解釋器時,也會使用到本地方法棧。如果 Java 虛擬機不支持 natvie 方法,並且自己也不依賴傳統棧的話,可以無需支持本地方法棧。

總結

Java 虛擬機的內存結構是學習虛擬機所必須掌握的地方,其中以 Java 堆的內存模型最為重要,因為線上問題很多時候都是 Java 堆出現問題。因此掌握 Java 堆的划分以及常用參數的調整最為關鍵。

除了上述所說的六大部分之外,其實在 Java 中還有直接內存、棧幀等數據結構。但因為直接內存、棧幀的使用場景還比較少,所以這里並不做介紹,以免讓初學者一時間混淆。

學到這里,一個 Java 文件就加載到內存中了,並且 Java 類信息就會存儲在我們的方法區中。如果創建對象,那么對象數據就會存放在 Java 堆中。如果調用方法,就會用到 PC 寄存器、Java 虛擬機棧、本地方法棧等結構。那么面對如此之多的 Java 類,JVM 是如何決定這些類的加載順序,又是如此控制它們的加載的呢?下一節,我們講講 JVM 的類加載機制。


如果只是看,其實無法真正學會知識的。為了幫助大家更好地學習,我建了一個虛擬機群,專門討論學習 Java 虛擬機方面的內容,每周針對我所發文章進行討論答疑。如果你有興趣,關注「Java技術精選」公眾號,通過右下角菜單「入群交流」加我好友,小助手會拉你入群。


JVM系列目錄


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM