JVM(七):JVM記憶體結構

iceWang丶發表於2019-07-02

JVM(七):JVM記憶體結構

在前幾節的文章我們多次講到 Class 物件需要分配入 JVM 記憶體,並在 JVM 記憶體中執行 Java 程式碼,完成物件記憶體的分配、執行、回收等操作,因此,如今讓我們來走入 JVM,看看 JVM 中的記憶體結構是如何構造的,下面就讓我們一探究竟吧。

記憶體劃分

在本小節中,我們以《Java 虛擬機器規範》中的要求,並以當前主流虛擬機器 Hotspot VM 為例,詳細講述記憶體區域中各個模組的劃分,瞭解其各自的用途以及其為何如何劃分等。

首先讓我們來看一下 Java 虛擬機器記憶體的劃分方式。

JVM記憶體區域

JVM 將記憶體劃分為 5個部分,分別為執行緒共享的 方法區,以及執行緒私有的 程式計數器虛擬機器棧本地方法棧,下面就讓我們針對這 5個區域進行學習,探究其儲存資料,生命週期和功能。

程式計數器

是一塊較小的記憶體區域,可以看做是當前執行緒執行的位元組碼的行號指示器。在虛擬機器概念模型裡,位元組碼解析器就是通過改變這個計數器的值來選取下一條需要執行的位元組碼,因此其在分支,迴圈,跳轉,異常跳轉,執行緒恢復等功能上都有著大作用。

PS:如果執行的是本地方法,那麼這個計數器的值則為空。

虛擬機器棧

虛擬機器棧也是執行緒私有的,其內描述的是 Java 方法執行的記憶體模型,即在每個執行同時建立一個棧幀,棧幀記憶體儲區域性變數表,運算元棧,動態連結,方法出口等資訊。每一個方法從開始到結束就對應著一個棧幀從入棧到出棧的過程。同時只有位於棧頂的棧幀才是有效的,與其關聯的方法稱為當前方法,執行引擎的所有位元組碼指令都只針對當前棧幀進行操作。

虛擬機器棧

區域性變數表

用於存放方法引數和方法內部定義的區域性變數,其在 Java 程式被編譯為 Class 檔案後,就已經確定了所需的最大容量。

其容量以變數槽(slot)為最小單位。因此在使用過程中是通過索引定位來使用區域性變數表的,索引範圍為 0~~slot 最大值。其中如果執行的是非 static 方法,那麼0則預設為 方法所屬物件例項引用,對應 Java 關鍵字的 this。其餘引數按照順序對應 1之後的槽位。

運算元棧

操作棧是一個後入先出的棧,其最大深度在編譯時也已經確定。其對應著方法執行過程中,各種位元組碼指令往運算元棧寫入和提取內容,也就是所謂的 入棧/出棧 操作。

也正是運算元棧的存在,因此Java執行引擎也被稱為 基於棧的執行引擎,與基於 基於暫存器的執行引擎 形成對比。

Java採取「基於棧的執行引擎」考慮到兩點:

  1. Java是一門跨平臺的語言,而不同機器的暫存器實現是不同的,有多又少,不利於統一;
  2. 為了使 class 檔案更加的緊湊,這樣設計可以使得大多數指令對齊,並且操作碼只佔一個位元組大小,減少資料量。

動態連線

指向執行時常量池中該棧幀所屬方法的引用,通過這個引用可以完成動態呼叫。

關於方法呼叫過程中的引用詳細解析過程,在日後的「方法呼叫」中,再具體描述。

返回地址

一個方法在執行完成後都需要返回到方法被呼叫的位置,讓程式繼續執行。

在方法正常執行完成退出後,呼叫者的程式計數器的值就可以作為返回地址存在棧幀中,而在方法異常退出後,返回地址則是通過異常處理器表來確定了。

附加資訊

附加資訊不是虛擬機器規範中必須要求有的,但其允許實現者可以增加一些特殊資訊到棧幀中,例如與除錯相關的資訊,這部分資訊取決於具體的虛擬機器實現,在這裡不再贅述。

本地方法棧

本地方法棧和虛擬機器棧的作用類似,區別僅僅是虛擬機器棧為虛擬機器執行的 Java 方法服務,而本地方法棧則是為 Native 方法服務。其具體實現由虛擬機器自行規定。

Java 堆是執行緒共享的。在一般情況下,堆可以說是 Java 記憶體中最大的記憶體區域。其存放了物件例項,幾乎所有的物件例項在這裡儲存。(這裡說是幾乎,是因為 JIT優化的存在,可能會有物件不在堆上分配,而在棧上進行分配)。

由於目前考慮到垃圾回收演算法大部分都是分代演算法,因此堆又可以細分為以下幾塊:

堆記憶體

但從其記憶體本質來看,其並沒有詳細的區別,都是用來儲存物件例項的,這種劃分方式是從記憶體回收的角度來闡述的,因此具體存放邏輯放在「記憶體回收」中再詳細闡述。

方法區

方法區也是執行緒共享的。其中存放的是被虛擬機器載入的類資訊,常量,靜態變數,即時編譯器編譯後的程式碼等資料。在HotSpot JDK7 以前的具體實現中,這部分被稱為永久代,和堆一起 JVM 管理。但在JDK8之後,這部分已經用 後設資料(meta space) 來替代了。此外像字串常量池也被從這一模組移除,轉而用堆來實現。

常量池

JDK7 之後將以前放在方法區的常量池放在堆中進行實現,例如 String 的 intern() 方法,在 JDK8 之後改為如果存在堆中的引用,則直接返回堆中引用,而並不會重新建立物件。

下面讓我們來看一下這段程式碼在 JDK8 下的結果是什麼。

    String s = new String("1");
    s.intern();
    String s2 = "1";
    System.out.println(s == s2);

    String s3 = new String("1") + new String("1");
    s3.intern();
    String s4 = "11";
    System.out.println(s3 == s4);

該程式碼在JDK8下輸出結果為:

false
true

下面就讓我們用下圖來分析一下是為什麼:

intern()

String s = new String("1") 這句生成了兩個物件,一個是物件 obj(1),另一個在 String pool 中,是 "1",s 則是指向物件。s.intern() 因為 "1" 在String pool中已經存在,所以直接返回,String s2 = "1",則是直接返回String pool中的引用給s2,最後比較的是兩個指向不同地方的引用,因此結果不同。

String s3 = new String("1") + new String("1") 生成了兩個物件,一個是物件obj(11),一個是String pool 中的 "1",s3.intern() 判斷當 堆中存在物件的時候,則在字串常量池中儲存該物件的引用,然後返回該物件的引用值String s4 = "11" 則讓 s4 指向 String pool 中的值,而 該引用的值就是obj(11)的引用,在最後 System.out.println(s3 == s4) 判斷相等的時候,兩個引用其實指向的是同一個值,因此返回相等。

直接記憶體

Direct Memory 不屬於 JVM 所管的記憶體區域,其受到機器總記憶體的影響。在具體使用中採用一個在 Java 堆中的DirectByteBuffer物件作為這塊記憶體的引用進行操作。

總結

在本文中我們學習了 JVM 在其內部是如何劃分割槽域進行功能協作的。瞭解了其內部將 JVM 劃分哪幾個模組,每個模組各自又都有神馬作用,其中儲存了什麼資料,每個模組的不同特性等。

在下文中,我們將講述物件在堆中的儲存,使用方式,瞭解的Java的 物件模型

iceWang公眾號

文章在公眾號「iceWang」第一手更新,有興趣的朋友可以關注公眾號,第一時間看到筆者分享的各項知識點,謝謝!筆芯!

本系列文章主要借鑑自《深入分析 JavaWeb 技術內幕》和《深入理解 Java 虛擬機器-JVM 高階特性與最佳實踐》。

相關文章