深入理解JVM(一)——JVM記憶體模型

piny發表於2021-09-09

JVM記憶體模型

Java虛擬機器(Java Virtual Machine=JVM)的記憶體空間分為五個部分,分別是:

1. 程式計數器

2. Java虛擬機器棧

3. 本地方法棧

4. 堆

5. 方法區。

深入理解JVM(一)——JVM記憶體模型

下面對這五個區域展開深入的介紹。

一、 程式計數器

1.1. 什麼是程式計數器?

程式計數器是一塊較小的記憶體空間,可以把它看作當前執行緒正在執行的位元組碼的行號指示器。也就是說,程式計數器裡面記錄的是當前執行緒正在執行的那一條位元組碼指令的地址。

注:但是,如果當前執行緒正在執行的是一個本地方法,那麼此時程式計數器為空。

1.2. 程式計數器的作用

程式計數器有兩個作用:


位元組碼直譯器通過改變程式計數器來依次讀取指令,從而實現程式碼的流程控制,如:順序執行、選擇、迴圈、異常處理。

在多執行緒的情況下,程式計數器用於記錄當前執行緒執行的位置,從而當執行緒被切換回來的時候能夠知道該執行緒上次執行到哪兒了。


1.3. 程式計數器的特點

是一塊較小的儲存空間

執行緒私有。每條執行緒都有一個程式計數器。

是唯一一個不會出現OutOfMemoryError的記憶體區域。

生命週期隨著執行緒的建立而建立,隨著執行緒的結束而死亡。


二. Java虛擬機器棧(JVM Stack)

2.1. 什麼是Java虛擬機器棧?

Java虛擬機器棧是描述Java方法執行過程的記憶體模型。

Java虛擬機器棧會為每一個即將執行的Java方法建立一塊叫做“棧幀”的區域,這塊區域用於儲存該方法在執行過程中所需要的一些資訊,這些資訊包括:


區域性變數表

存放基本資料型別變數、引用型別的變數、returnAddress型別的變數。

運算元棧

動態連結

方法出口資訊

當一個方法即將被執行時,Java虛擬機器棧首先會在Java虛擬機器棧中為該方法建立一塊“棧幀”,棧幀中包含區域性變數表、運算元棧、動態連結、方法出口資訊等。當方法在執行過程中需要建立區域性變數時,就將區域性變數的值存入棧幀的區域性變數表中。

當這個方法執行完畢後,這個方法所對應的棧幀將會出棧,並釋放記憶體空間。


注意:人們常說,Java的記憶體空間分為“棧”和“堆”,棧中存放區域性變數,堆中存放物件。

這句話不完全正確!這裡的“堆”可以這麼理解,但這裡的“棧”只代表了Java虛擬機器棧中的區域性變數表部分。真正的Java虛擬機器棧是由一個個棧幀組成,而每個棧幀中都擁有:區域性變數表、運算元棧、動態連結、方法出口資訊。


2.2. Java虛擬機器棧的特點

區域性變數表的建立是在方法被執行的時候,隨著棧幀的建立而建立。而且,區域性變數表的大小在編譯時期就確定下來了,在建立的時候只需分配事先規定好的大小即可。此外,在方法執行的過程中區域性變數表的大小是不會發生改變的。

Java虛擬機器棧會出現兩種異常:StackOverFlowError和OutOfMemoryError。

a) StackOverFlowError:

若Java虛擬機器棧的記憶體大小不允許動態擴充套件,那麼當執行緒請求棧的深度超過當前Java虛擬機器棧的最大深度的時候,就丟擲StackOverFlowError異常。

b) OutOfMemoryError:

若Java虛擬機器棧的記憶體大小允許動態擴充套件,且當執行緒請求棧時記憶體用完了,無法再動態擴充套件了,此時丟擲OutOfMemoryError異常。

Java虛擬機器棧也是執行緒私有的,每個執行緒都有各自的Java虛擬機器棧,而且隨著執行緒的建立而建立,隨著執行緒的死亡而死亡。

注:StackOverFlowError和OutOfMemoryError的異同?

StackOverFlowError表示當前執行緒申請的棧超過了事先定好的棧的最大深度,但記憶體空間可能還有很多。

而OutOfMemoryError是指當執行緒申請棧時發現棧已經滿了,而且記憶體也全都用光了。


3. 本地方法棧

3.1. 什麼是本地方法棧?

本地方法棧和Java虛擬機器棧實現的功能類似,只不過本地方法區是本地方法執行的記憶體模型。


本地方法被執行的時候,在本地方法棧也會建立一個棧幀,用於存放該本地方法的區域性變數表、運算元棧、動態連結、出口資訊。


方法執行完畢後相應的棧幀也會出棧並釋放記憶體空間。


也會丟擲StackOverFlowError和OutOfMemoryError異常。


4. 堆

4.1. 什麼是堆?

堆是用來存放物件的記憶體空間。

幾乎所有的物件都儲存在堆中。


4.2. 堆的特點

執行緒共享

整個Java虛擬機器只有一個堆,所有的執行緒都訪問同一個堆。而程式計數器、Java虛擬機器棧、本地方法棧都是一個執行緒對應一個的。

在虛擬機器啟動時建立

垃圾回收的主要場所。

可以進一步細分為:新生代、老年代。

新生代又可被分為:Eden、From Survior、To Survior。

不同的區域存放具有不同生命週期的物件。這樣可以根據不同的區域使用不同的垃圾回收演算法,從而更具有針對性,從而更高效。

堆的大小既可以固定也可以擴充套件,但主流的虛擬機器堆的大小是可擴充套件的,因此當執行緒請求分配記憶體,但堆已滿,且記憶體已滿無法再擴充套件時,就丟擲OutOfMemoryError。


5. 方法區

5.1. 什麼是方法區?

Java虛擬機器規範中定義方法區是堆的一個邏輯部分。

方法區中存放已經被虛擬機器載入的類資訊、常量、靜態變數、即時編譯器編譯後的程式碼等。


5.2. 方法區的特點

執行緒共享

方法區是堆的一個邏輯部分,因此和堆一樣,都是執行緒共享的。整個虛擬機器中只有一個方法區。

永久代

方法區中的資訊一般需要長期存在,而且它又是堆的邏輯分割槽,因此用堆的劃分方法,我們把方法區稱為老年代。

記憶體回收效率低

方法區中的資訊一般需要長期存在,回收一遍記憶體之後可能只有少量資訊無效。

對方法區的記憶體回收的主要目標是:對常量池的回收 和 對型別的解除安裝。

Java虛擬機器規範對方法區的要求比較寬鬆。

和堆一樣,允許固定大小,也允許可擴充套件的大小,還允許不實現垃圾回收。


5.3. 什麼是執行時常量池?

方法區中存放三種資料:類資訊、常量、靜態變數、即時編譯器編譯後的程式碼。其中常量儲存在執行時常量池中。


我們一般在一個類中通過public static final來宣告一個常量。這個類被編譯後便生成Class檔案,這個類的所有資訊都儲存在這個class檔案中。


當這個類被Java虛擬機器載入後,class檔案中的常量就存放在方法區的執行時常量池中。而且在執行期間,可以向常量池中新增新的常量。如:String類的intern()方法就能在執行期間向常量池中新增字串常量。


當執行時常量池中的某些常量沒有被物件引用,同時也沒有被變數引用,那麼就需要垃圾收集器回收。


6. 直接記憶體

直接記憶體是除Java虛擬機器之外的記憶體,但也有可能被Java使用。


在NIO中引入了一種基於通道和緩衝的IO方式。它可以通過呼叫本地方法直接分配Java虛擬機器之外的記憶體,然後通過一個儲存在Java堆中的DirectByteBuffer物件直接操作該記憶體,而無需先將外面記憶體中的資料複製到堆中再操作,從而提升了資料操作的效率。


直接記憶體的大小不受Java虛擬機器控制,但既然是記憶體,當記憶體不足時就會丟擲OOM異常。


綜上所述

Java虛擬機器的記憶體模型中一共有兩個“棧”,分別是:Java虛擬機器棧和本地方法棧。

兩個“棧”的功能類似,都是方法執行過程的記憶體模型。並且兩個“棧”內部構造相同,都是執行緒私有。

只不過Java虛擬機器棧描述的是Java方法執行過程的記憶體模型,而本地方法棧是描述Java本地方法執行過程的記憶體模型。

Java虛擬機器的記憶體模型中一共有兩個“堆”,一個是原本的堆,一個是方法區。方法區本質上是屬於堆的一個邏輯部分。堆中存放物件,方法區中存放類資訊、常量、靜態變數、即時編譯器編譯的程式碼。


堆是Java虛擬機器中最大的一塊記憶體區域,也是垃圾收集器主要的工作區域。

程式計數器、Java虛擬機器棧、本地方法棧是執行緒私有的,即每個執行緒都擁有各自的程式計數器、Java虛擬機器棧、本地方法區。並且他們的生命週期和所屬的執行緒一樣。


而堆、方法區是執行緒共享的,在Java虛擬機器中只有一個堆、一個方法棧。並在JVM啟動的時候就建立,JVM停止才銷燬。

Java後端進階公眾號致力於分享Java架構技術,包括有Java高效能、spring Boot  高併發、分散式等主流技術,喜歡的技術的朋友都可以關注一下!

深入理解JVM(一)——JVM記憶體模型


相關文章