前言
繁忙的一年即將過去,由于若干種原因,下定決心開始寫一些基礎系列,主要包含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所示,

圖1 Test.java 執行流程
JVM運行時記憶體主要分為:程式計數器、虛擬機堆疊、本地方法堆疊、堆、方法區五個部分,如圖2所示,

圖2 JVM運行時記憶體分布
其中方法區和堆是執行緒間共享的 ,虛擬機堆疊、本地方法堆疊和程式計數器是執行緒私有的,依次來看這些區域各自的作用,
程式計數器
程式計數器用來記錄當前執行緒執行的位置,CPU可以在多個執行緒中分配執行時間,當某個執行緒被掛起時,程式計數器用來記錄代碼已經執行的位置,當執行緒恢復執行時繼續從記錄位置開始執行,常見的例外處理、分支操作等都是通過通程序式計數器來完成的,
每個執行緒內部都有一個程式計數器,隨著執行緒的創建而創建,隨著執行緒的銷毀而銷毀,計數器記錄的是正在執行的虛擬機位元組碼指令的地址,如果當前執行的是Native方法,計數器值為空,
虛擬機堆疊
虛擬機堆疊用來描述Java方法執行的記憶體模型,我們都知道,JVM是基于堆疊的解釋器執行的,這里的堆疊指的就是虛擬機堆疊,更確切的說是虛擬機堆疊堆疊幀中的運算元堆疊,
執行緒在執行方式時會為每個方法創建一個堆疊幀,堆疊幀內部又包含區域變數表、運算元堆疊、動態鏈接與回傳地址,執行緒中堆疊幀分布如圖3所示,

圖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所示,

圖4 區域變數表和運算元堆疊初始狀態
執行0: iconst_1之后,如圖5所示,

圖5
執行 1: istore_0之后,如圖6 所示,

圖6
同樣的執行
2: iconst_2
3: istore_1
4: iload_0
5: iload_1
6: iadd
依次變化如圖7所示,

圖7 第2步到第6步區域變數表與運算元堆疊變化
最后執行return,將運算元堆疊中的元素3回傳,由此1+2=3的操作邊完成了,方法執行完成后區域變數表和運算元堆疊會被銷毀,
我們經常會遇到StackOverflowError的例外,這就是因為我們上面所說的每呼叫一個方法時都會在虛擬機堆疊中創建一個堆疊幀,當遇到例外導致方法無法退出時,堆疊幀就不會銷毀從而導致StackOverflowError的例外,
動態鏈接
動態鏈接是為了支持方法呼叫程序中的動態鏈接,一個方法若要呼叫另一個方法,需要將方法的符號參考轉化為記憶體地址的應用,符號參考存盤在方法區中,
回傳地址
回傳地址可以使當前方法恢復上層方法執行狀態,便于在方法退出后回傳到方法被呼叫的位置繼續執行,
方法退出方式無非就是兩種:正常退出和例外退出,正常退出時程式計數器可以作為回傳地址,例外退出時回傳地址需要通過例外處理器表來確定,
本地方法堆疊
本地方法堆疊與虛擬機堆疊基本相同,主要用來管理nttive方法,如在Android中使用JNI,這里就不對本地方法堆疊單獨介紹了,
方法區
方法區主要用來存盤已被加載的類、靜態變數、常量等資訊,方法區僅僅是JVM規范中規定的區域,不同的JVM廠商實作方式是不同的,這一點是需要注意的,
堆
堆在JVM管理管理的記憶體中是最大的一塊,堆用來存在物件的實體,也是GC管理的主要區域,
按照存盤物件時間不同可以劃分為新生代和老年代,其中新生代又分為Eden區和Survivor區,不同的存放區域存放不同生命周期的物件,這樣每個區域就可以使用不同的垃圾回收演算法,以此來提高垃圾回收率,堆的劃分如圖8所示,

圖8 堆區域劃分
堆和方法區都是執行緒間共享的記憶體區域,
總結
JVM運行時記憶體主要有程式計數器、虛擬機堆疊、本地方法堆疊、堆和方法區,只有堆和方法區是執行緒間的資料共享區域,
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/374739.html
標籤:java
上一篇:ES實戰專案——仿京東商城
