17張圖帶你瞭解,JVM 執行時資料區

17張圖帶你瞭解,JVM 執行時資料區

作者 | 崔皓

開篇

眾所周知,Java程式的執行需要依賴於JVM(Java 虛擬機器)。JVM 會將Java原始碼編譯成位元組碼檔案,然後使用類載入器將其載入到執行時資料區中執行,垃圾收集器也會針對執行時資料區進行物件回收的工作。今天就來說說JVM的執行時資料區。

執行時資料區概述

在計算機世界中,記憶體是十分重要的系統資源,它承載著作業系統和應用程式實時執行的責任。JVM記憶體佈局規定了Java在執行過程中記憶體申請、分配、管理的策略,從而保證了JVM的高效穩定執行。

Java虛擬機器在執行Java程式的過程中,會將涉及到的資料劃分到不同的記憶體區域去管理,在這些資料區域,有些是隨著虛擬機器啟動而建立,虛擬機器關閉而銷燬。還有一部分是隨著執行緒生命週期建立銷燬的。這部分割槽域就是接下來要講的Java虛擬機器的執行時資料區。

17張圖帶你瞭解,JVM 執行時資料區

圖1 執行時資料區

如圖1所示,紅色的部分就是執行時資料區,它包括:方法區、堆、虛擬機器棧、本地方法棧以及程式計數器五個部分。

圖1中標註為黃色的方法區和堆是執行緒間共享的,也就是說它們會隨著虛擬機器啟動而建立,隨著虛擬機器退出而銷燬。橙色部分為每個執行緒單獨享有的,即它們與執行緒是一一對應的,會隨著執行緒開始和結束而建立和銷燬。在HotSpot JVM中,每個執行緒都與作業系統的本地執行緒直接對映,例如:有一個Java執行緒準備好執行時,就有一個作業系統的本地執行緒被建立並且與Java 執行緒對應,當Java執行緒執行終止後,本地執行緒也會被回收。同時作業系統負責執行緒排程,及分配對應的CPU執行執行緒,一旦作業系統的本地執行緒初始化成功,它就會呼叫Java執行緒中的的run()方法去執行Java執行緒。

褐色部分的執行引擎就負責讀取指令並且交由CPU執行,它包括直譯器、JIT(即時編譯器),GC(垃圾回收器)。而另外一個褐色的本地庫介面會提供Java程式呼叫的native方法。

另外,執行時資料區的劃分也隨著JDK的發展不斷變遷,如圖2 所示, JDK 1。6、JDK 1。7、JDK 1。8 的記憶體劃分都會有所不同。

17張圖帶你瞭解,JVM 執行時資料區

圖2 執行時資料區的變遷

如圖2 所示,在JDK 1。8 中加入了元資料區的概念,將原來儲存在方法區中的執行時常量池和類常量池都包括其中。

虛擬機器棧

上面介紹了JVM 執行時資料區的概念和組成,接下來一次介紹每個組成部分,首先從虛擬機器棧開始。

每個Java執行緒都會對應一個虛擬機器棧,換句話說多個執行緒就對應多個虛擬機器棧。上面講過了虛擬機器棧是執行緒私有,虛擬機器棧中包含多個棧幀(Stack Frame),每一個棧幀是為方法執行而建立的,棧幀中描述的是Java方法執行的記憶體模型。每個方法從呼叫開始直到完成的全過程都對應著一個棧幀。棧幀是用來管理Java程式的執行,並儲存方法的區域性變數、部分結果、並參與方法的呼叫與返回。

在活動執行緒中,只有一個棧幀是處於活躍狀態的,也就是說只有位於棧頂的棧幀才是有效的,稱為當前棧幀,與這個棧幀相關聯的方法稱為當前方法。執行引擎執行的所有位元組碼指令都只針對當前棧幀進行操作。

如圖3 所示,每個Java 方法都會對應一個棧幀,左邊的四個方法就對應了四個棧幀,從下往上依次是方法呼叫的順序,最終方法1 會呼叫方法4, 此時正在執行方法4 ,它對應的棧幀4 就是“當前棧幀”,就是出於活躍狀態的,其包含了區域性變量表、運算元棧、動態連結以及返回地址等資訊。

17張圖帶你瞭解,JVM 執行時資料區

圖3 棧幀結構

區域性變量表

它定義為數字陣列,主要用於儲存方法引數和定義在方體內的區域性變數,包含基本資料型別,物件引用,以及returnAddress型別。它建立線上程的棧上,是執行緒的私有資料,因此不存在資料的安全問題。

區域性變量表所需的容量在編譯期間確定,在執行期間是不改變其容量。方法巢狀呼叫的次數由棧的容量來決定,例如圖3就進行了4個方法的巢狀,也就是說棧越大,方法巢狀呼叫次數越多。對一個函式而言,它的引數和區域性變數越多,對應的棧幀就越大。因此,函式呼叫就會佔用更多的棧空間。區域性變量表中的變數只在當前方法呼叫中有效。在方法執行時,虛擬機器透過使用區域性變量表完成引數值到引數變數列表的傳遞。當方法呼叫結束後,隨著方法棧幀的銷燬,區域性變量表也會隨之銷燬。

運算元棧

它是一個後進先出的棧,在方法執行的過程中,根據位元組碼指令、往棧中寫入或取出資料,即入棧/出棧。位元組碼指令將值壓入操作棧,其餘的位元組碼指令將運算元取出棧,進行操作之後再將結果壓入棧。操作包括:複製、交換、求和等。

這樣講比較抽象,來看一個具體的例子。

如圖4 所示,生成一個testAdd 方法,給變數i和j 分別賦值為1 和2 ,然後讓其相加並且把結果賦值給k。

17張圖帶你瞭解,JVM 執行時資料區

圖4 運算元棧程式碼

使用jclasslib反編譯上面的程式碼得到圖5 的結果。

17張圖帶你瞭解,JVM 執行時資料區

圖5 jclasslib反編譯結果

如圖6 所示,當執行地址 0 的時候操作指令為bipush,此時程式暫存器的地址顯示為0 ,bipush 命令將 1 壓入到運算元棧的頂部。

17張圖帶你瞭解,JVM 執行時資料區

圖6

如圖7 所示,當指令地址到2 的時候,程式暫存器顯示為2, 此時執行istore_1 的指令,將棧頂的數字1 儲存到區域性變量表中。

17張圖帶你瞭解,JVM 執行時資料區

圖7

如圖8所示,指令地址執行到3 的時候,程式暫存器為3 , bipush指令把2 壓入到運算元棧的頂部。

17張圖帶你瞭解,JVM 執行時資料區

圖8

在指令地址為5 的時候,程式暫存器的值為5, istore_2指令將運算元棧中的2 儲存到區域性變量表中的2 的位置。

17張圖帶你瞭解,JVM 執行時資料區

圖9

如圖10所示,指令地址為6 的時候,執行iload_1 指令獲取區域性變量表中 位置為1 的值,也就是1 並且把它放到運算元棧的頂部。

17張圖帶你瞭解,JVM 執行時資料區

圖10

如圖11所示,指令地址為7 的時候,執行iload_2 指令,從區域性變量表2 的位置取出值2 放到運算元棧的頂部。

17張圖帶你瞭解,JVM 執行時資料區

圖11

如圖12 所示,在指令地址為8 時,執行iadd 指令,將運算元棧的兩個數字1和2 相加結果為3,並且將其放到運算元棧的頂部。

17張圖帶你瞭解,JVM 執行時資料區

圖12

如圖13 所示,接著執行指令地址 9 , istore_3 執行之後將運算元棧頂的3 儲存到區域性變量表3 的位置,完成相加的操作,最後透過指令地址10 中的return指令返回方法。

17張圖帶你瞭解,JVM 執行時資料區

圖13

動態連結

在介紹動態連結之前先說說靜態連結,即位元組碼檔案被裝載進JVM內部時,如果被呼叫的目標方法在編譯期可知,且執行期間保持不變時。這種情況下將呼叫方法的符號引用轉換為直接引用的過程稱之為靜態連結。但是,如果被呼叫方法在編譯期間無法被確定下來,只能在程式執行時將呼叫方法的符號引用轉換為直接引用,由於這種引用轉換的過程具備動態性,被稱為動態連結。

17張圖帶你瞭解,JVM 執行時資料區

圖14 從位元組碼到常量池中的方法引用

如圖15所示,當位元組碼檔案被載入後,位元組碼檔案中的一些資料,如型別資訊、域資訊、方法資訊等,就會被放置到方法區中。而棧幀中的當前類常量池引用(Current Class Constant Pool Reference)儲存的是方法符號引用,真正的方法引用放在了方法區(Method Area)中的方法引用(method reference)中了,這個方法引用是為了支援程式碼的動態連結。動態連結就是將符號引用轉化為直接引用。

17張圖帶你瞭解,JVM 執行時資料區

圖15 棧幀中的當前類常量池引用對應方法區中的方法引用

JVM之所以這麼設計是因為位元組碼檔案需要資料支援的量會很大,因此不能直接將這些資料存放到位元組碼中。針對方法的引用建立符號引用,這個符號引用放在棧幀的常量池引用中,而實際的方法和符號引用的對照表卻放在方法區的常量池中,這樣位元組碼就可以透過常量池中的對照關係找到引用的方法,並且也不會增加棧幀的容量。

方法返回地址

當一個方法開始執行後,可以透過兩種方式退出該方法。第一種是執行引擎遇到方法返回的位元組碼指令,此時返回值會傳遞到上層呼叫者,這種方式稱為正常完成出口。另外一種退出方式是在方法執行中遇到異常,這個異常在方法體內沒有得到處理,就會導致方法退出,這種方式稱為異常完成出口。由於是異常退出,就不會給上層呼叫者任何返回值。無論採取上面那種退出方式,方法都會到處呼叫它的位置,程式才能繼續執行。方法在返回的時候需要在棧幀中儲存一些資訊,用來恢復呼叫該方法的上層方法的執行狀態。這裡可以透過方法呼叫者的程式計數器存放返回地址,如果是正常退出方法,上層方法會從程式計數器中儲存的地址繼續執行接下來的步驟。如果是異常退出的情況,返回地址就需要異常處理器來確定了。

程式計數器

有了上面虛擬機器棧的講解,對於程式計數器的理解會相對簡單點。記得在虛擬機器棧中的運算元棧的例子中,提到了使用程式計數器記錄操作指令的地址。程式計數器就是一塊較小的記憶體空間,它是當前執行緒執行的位元組碼的行號(操作指令的地址)指示器。在棧幀中位元組碼直譯器就是透過改變計數器的值來選去下一條要執行的位元組碼指令的,例如:分支、迴圈、跳轉、異常處理、執行緒恢復等。

上面講虛擬機器棧的時候提到過,多個執行的Java執行緒就是多個虛擬機器棧,每個棧中存在多個棧幀,在一個時刻只有一個棧幀執行,也就是當前棧幀。也就是說在一個時刻一個處理只會對一個執行緒中的一個幀棧執行一條指令,而每個棧幀都會維護一個屬於自己的程式計數器,這個計數器就是來記錄指令執行的地址的。每個執行緒的計數器不會相互影響,這也保證了在Java 多執行緒進行切換的時候,每個執行緒都能夠保證正確的指令地址被讀取。

如圖 16所示,在invokevirtual的框圖中存在多個執行緒,每個執行緒就是一個虛擬機器棧,每個執行緒中包含多個Frame 也就是棧幀,針對每個執行緒都會維護一個PC Registers也就是程式暫存器,它會記錄指令地址資訊,從而讓方法實現:跳轉、分支、迴圈、異常處理和執行緒恢復的功能。

17張圖帶你瞭解,JVM 執行時資料區

圖16 程式計數器

本地方法棧

本地方法棧與虛擬機器棧所發揮的作用是非常相似的,它們之間的區別是虛擬機器棧為虛擬機器執行Java方法(也就是位元組碼)服務,而本地方法棧為虛擬機器所使用到的Native方法服務。本地方法棧也會丟擲StackOverflowError和OutOfMemoryError異常。

說白了,本地方法(Native Method)就是一個Java呼叫非Java程式碼的介面。 當Java應用需要與Java之外的環境互動時就需要使用本地方法,特別與底層系統、作業系統以及硬體打交道時就會用到本地方法。大家可以把本地方法理解為一種交流機制:它提供了一個對外的簡潔的介面,讓我們無需去了解Java應用之外的細節。

那麼JVM是如何使用Native Method的呢?當一個類第一次被使用時,類的位元組碼會被載入到記憶體,在位元組碼的入口維持著該類所有方法描述符的list,包括:方法程式碼來源,引數,方法描述符(例如:public)等等。

堆和方法區

上面說的虛擬機器棧、程式計數器和本地方法棧都是執行緒私有的,而接下來說的方法區和堆是執行緒共享的。這裡把堆和方法區合起來說。

Java堆是Java虛擬機器所管理記憶體中最大的一塊,在虛擬機器啟動時建立,被所有執行緒共享。Java物件例項以及陣列都在堆上分配。堆的大小可以是固定的,也可以根據計算的需要進行擴充套件,如果不需要更大的堆,則可以收縮。堆的記憶體不需要是連續的。Java虛擬機器實現可以為程式設計師或使用者提供對堆初始大小的控制,如果可以動態擴充套件或收縮堆,還可以控制堆的最大和最小大小。

Java堆是垃圾收集器管理的主要區域,所以也被稱為GC堆。從記憶體回收的角度來看,由於現在收集器基本都採用分代收集演算法,所以Java堆中還可以細分為:新生代和老年代;新生代再細分就是:Eden空間、From Survivor空間、ToSurvivor空間等。從記憶體分配的角度來看,執行緒共享的Java堆中可能劃分出多個執行緒私有的分配緩衝區(Thread Local Allocation Buffer,TLAB)。不論如何劃分,都與存放內容無關,無論哪個區域,存放的都仍然是物件例項;進一步劃分的目的是為了更好的回收記憶體,或者更快地分配記憶體。

對於堆中垃圾回收的部分這裡不展開說明,後面會有文章去介紹。

方法區

方法區和堆一樣是執行緒共享的記憶體區域,它用來存放被虛擬機器載入的型別資訊、執行時常量池、靜態變數、JIT程式碼快取、域資訊、方法資訊等。方法區(Method Area)與Java堆一樣,是各個執行緒共享的記憶體區域,有如下特點:

方法區在JVM啟動的時候被建立,並且它的實際的物理記憶體空間和Java堆區一樣都可以是不連續的。

方法區的大小,和堆空間一樣,可以選擇固定大小和可擴充套件。

方法區的大小決定了系統可以儲存多少個類,如果系統定義了太多的類,導致方法區溢位,虛擬機器就會丟擲記憶體溢位錯誤:

java。lang。OutOfMemoryError:PermGenspace

或者 java。lang。OutOfMemoryError: Metaspace。

關閉JVM就會釋放這個區域的記憶體。

這裡把堆、方法區和虛擬機器棧的關係整理一下。如圖17 所示,在右邊建立了AppMain 類,在執行時JVM 會把AppMain的資訊放入到方法區,因為方法區會存放型別資訊。同時main 的方法本身也會放入到方法區。接下來的new Sample(“測試1”)的語句中Sample的自定義物件會放到堆裡面,而對應的test1 應用會放入到虛擬機器棧中,對應的test1。printName()方法的執行會在虛擬機器棧中的棧幀中透過指令執行完成。另外下面的class Sample也是放到方法區中的,宣告的private name,其中name的引用放在虛擬機器棧中,name對應的物件放在堆中。對應的printName方法是放在方法區中的。

17張圖帶你瞭解,JVM 執行時資料區

圖17 棧、堆、方法區關係

總結

JVM 會把Java的位元組碼載入到執行時資料區內,這個記憶體區域分為:方法區、堆、虛擬機器棧、本地方法棧以及程式計數器。堆裡面放物件,也是垃圾回收器要處理的物件;方法區放型別、方法描述、方法本體;程式計數器負責記錄虛擬機器棧中指令執行的地址;虛擬機器棧對應Java執行的執行緒,物件的引用都儲存在棧幀中,透過指令地址和指令執行方法中的內容;本地方法棧用來呼叫Java 之外的系統級別的介面。

總結

崔皓,51CTO社群編輯,資深架構師,擁有18年的軟體開發和架構經驗,10年分散式架構經驗。曾任惠普技術專家。樂於分享,撰寫了很多熱門技術文章,閱讀量超過60萬。《分散式架構原理與實踐》作者。

17張圖帶你瞭解,JVM 執行時資料區

點分享

17張圖帶你瞭解,JVM 執行時資料區

點收藏

17張圖帶你瞭解,JVM 執行時資料區

點點贊

17張圖帶你瞭解,JVM 執行時資料區

TAG: 方法執行緒虛擬機器Java指令