天天看點

一文看懂JVM運作時記憶體分布前言從1+2來看JVM運作時記憶體分布JVM記憶體分布總結

前言

繁忙的一年即将過去,由于若幹種原因,下定決心開始寫一些基礎系列,主要包含Java基礎、Android基礎、設計模式與算法等,目前還沒給這個系列想到一個好聽的名字。

虛拟機的實作有很多,比如HotSpot、Android Dalvik 、 ART等,不同虛拟機具體實作方式不同但都符合Java虛拟機規範中的規則。

從1+2來看JVM運作時記憶體分布

建立一個Test類,定義一個靜态方法sum,代碼如下所示:

public class Test {

    public static void main(String[] args) {
        System.out.println(sum());
    }

    public static int sum() {
        int a = 1;
        int b = 2;
        return a + b;
    }
}           

複制

運作程式,列印結果為3。那麼運作Test檔案的流程是怎樣的呢?

JVM記憶體分布

首先Test.java檔案經過編輯器編譯生成Test.class檔案。當運作Test類時,通過ClassLoader将Test.class加載到JVM記憶體中,如圖1所示。

一文看懂JVM運作時記憶體分布前言從1+2來看JVM運作時記憶體分布JVM記憶體分布總結

圖1 Test.java 執行流程

JVM運作時記憶體主要分為:程式計數器、虛拟機棧、本地方法棧、堆、方法區五個部分,如圖2所示。

一文看懂JVM運作時記憶體分布前言從1+2來看JVM運作時記憶體分布JVM記憶體分布總結

圖2 JVM運作時記憶體分布

其中方法區和堆是線程間共享的 ,虛拟機棧、本地方法棧和程式計數器是線程私有的,依次來看這些區域各自的作用。

程式計數器

程式計數器用來記錄目前線程執行的位置。CPU可以在多個線程中配置設定執行時間,當某個線程被挂起時,程式計數器用來記錄代碼已經執行的位置,當線程恢複執行時繼續從記錄位置開始執行。常見的異常處理、分支操作等都是通過通過程式計數器來完成的。

每個線程内部都有一個程式計數器,随着線程的建立而建立,随着線程的銷毀而銷毀。計數器記錄的是正在執行的虛拟機位元組碼指令的位址,如果目前執行的是Native方法,計數器值為空。

虛拟機棧

虛拟機棧用來描述Java方法執行的記憶體模型,我們都知道,JVM是基于棧的解釋器執行的,這裡的棧指的就是虛拟機棧,更确切的說是虛拟機棧棧幀中的操作數棧。

線程在執行方式時會為每個方法建立一個棧幀,棧幀内部又包含局部變量表、操作數棧、動态連結與傳回位址。線程中棧幀分布如圖3所示。

一文看懂JVM運作時記憶體分布前言從1+2來看JVM運作時記憶體分布JVM記憶體分布總結

圖3 棧幀結構

局部變量表

局部變量表是變量值的存儲空間,調用方法傳遞的參數、方法内部建立的變量都會儲存在局部變量表中。java檔案經過編譯後局部變量表的大小已經确定,會寫在Code屬性表中max_locals屬性中。

以上面兩數相加的代碼為例,檢視Test檔案的位元組碼代碼如下所示:

public static int sum();
    descriptor: ()I
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=0
         0: iconst_1
         1: istore_0
         2: iconst_2
         3: istore_1
         4: iload_0
         5: iload_1
         6: iadd
         7: ireturn
      LineNumberTable:
        line 16: 0
        line 17: 2
        line 18: 4           

複制

從位元組碼檔案中可以看出locals屬性的值是2,說明局部變量表的大小為2 分别用來存儲變量a和變量b。args_size 表示是參數的個數,這裡參數是0,stack表示操作數棧的最大值,首先來看操作數棧是什麼。

操作數棧

操作數棧中可以存儲任意的Java資料類型。位元組碼code表中stack=2表示操作數棧的最大深度為2,方法執行的時候會有位元組碼指令壓入或彈出,以上面的位元組碼操作為例,來看一下操作數棧和局部變量表的變化。

首先開看下各指令值的含義:

iconst:将常量壓入操作數棧棧頂,與此類似的還有bipush指令,當 int 取值 -1~5 采用 iconst 指令,取值 -128~127 則使用 bipush 指令。

istore:将操作數棧棧頂元素出棧放入局部變量表的索引位置,istore_n表示将棧頂元素放在局部變量表下标為n的位置。

iload:iload_n表示将局部變量表中下标為n的值壓入棧頂

iadd:将操作數棧最上面的兩個元素相加,将結果壓入棧頂

以1+2的位元組碼方法為例

0: iconst_1
 1: istore_0
 2: iconst_2
 3: istore_1
 4: iload_0
 5: iload_1
 6: iadd
 7: ireturn                   

複制

剛開始執行sum方式時字局部變量表與操作數棧下圖4所示。

一文看懂JVM運作時記憶體分布前言從1+2來看JVM運作時記憶體分布JVM記憶體分布總結

圖4 局部變量表和操作數棧初始狀态

執行0: iconst_1之後,如圖5所示。

一文看懂JVM運作時記憶體分布前言從1+2來看JVM運作時記憶體分布JVM記憶體分布總結

圖5

執行 1: istore_0之後,如圖6 所示。

一文看懂JVM運作時記憶體分布前言從1+2來看JVM運作時記憶體分布JVM記憶體分布總結

圖6

同樣的執行

2: iconst_2

3: istore_1

4: iload_0

5: iload_1

6: iadd

依次變化如圖7所示。

一文看懂JVM運作時記憶體分布前言從1+2來看JVM運作時記憶體分布JVM記憶體分布總結

圖7 第2步到第6步局部變量表與操作數棧變化

最後執行return,将操作數棧中的元素3傳回,由此1+2=3的操作邊完成了,方法執行完成後局部變量表和操作數棧會被銷毀。

我們經常會遇到StackOverflowError的異常,這就是因為我們上面所說的每調用一個方法時都會在虛拟機棧中建立一個棧幀,當遇到異常導緻方法無法退出時,棧幀就不會銷毀進而導緻StackOverflowError的異常。

動态連結

動态連結是為了支援方法調用過程中的動态連結。一個方法若要調用另一個方法,需要将方法的符号引用轉化為記憶體位址的應用,符号引用存儲在方法區中。

傳回位址

傳回位址可以使目前方法恢複上層方法執行狀态,便于在方法退出後傳回到方法被調用的位置繼續執行。

方法退出方式無非就是兩種:正常退出和異常退出,正常退出時程式計數器可以作為傳回位址,異常退出時傳回位址需要通過異常處理器表來确定。

本地方法棧

本地方法棧與虛拟機棧基本相同,主要用來管理nttive方法,如在Android中使用JNI。這裡就不對本地方法棧單獨介紹了。

方法區

方法區主要用來存儲已被加載的類、靜态變量、常量等資訊。方法區僅僅是JVM規範中規定的區域,不同的JVM廠商實作方式是不同的。這一點是需要注意的。

堆在JVM管理管理的記憶體中是最大的一塊,堆用來存在對象的執行個體,也是GC管理的主要區域。

按照存儲對象時間不同可以劃分為新生代和老年代,其中新生代又分為Eden區和Survivor區,不同的存放區域存放不同生命周期的對象,這樣每個區域就可以使用不同的垃圾回收算法,以此來提高垃圾回收率。堆的劃分如圖8所示。

一文看懂JVM運作時記憶體分布前言從1+2來看JVM運作時記憶體分布JVM記憶體分布總結

圖8 堆區域劃分

堆和方法區都是線程間共享的記憶體區域。

總結

JVM運作時記憶體主要有程式計數器、虛拟機棧、本地方法棧、堆和方法區,隻有堆和方法區是線程間的資料共享區域。