1. 程式人生 > >JVM系列第6講:Java 虛擬機器記憶體結構

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 的類載入機制。