文章目錄
JVM運行時數據區組成
Java虛擬機在執行Java程序的過程中會將其管理的內存劃分爲若干個不同的數據區域,這些區域有各自的用途、創建和銷燬的時間,有些區域隨虛擬機進程的啓動而存在,有些區域則是依賴用戶線程的啓動和結束來建立和銷燬。Java虛擬機所管理的內存包括以下幾個運行時數據區域,如圖:
- 程序計數器(Program Counter Register)
- 虛擬機棧(VM Stack)
- 本地方法棧(Native Method Stack)
- 方法區(Method Area)
- 堆(Heap)
程序計數器(Program Counter Register)
作用:指向當前線程正在執行的字節碼指令地址(行號 ),線程私有
引導思考:
- 在Java中最小的執行單位是什麼? --》線程
- 多線程中,線程切換是如何知道上一個時間片執行到的程序指令的? --》程序計數器
- 程序計數器標誌着線程正在執行的字節碼指令地址,那麼每個線程的執行狀態是否一致?–》該程序計數器是否線程私有–》 程序計數器線程私有
虛擬機棧(VM Stack)
作用:虛擬機棧是當前線程執行方法的內存模型。每個方法被執行的時候,都會創建一個棧幀,把棧幀壓人棧,當方法正常返回或者拋出未捕獲的異常時,棧幀就會出棧。
引導思考:
- 虛擬機棧是什麼類型的數據結構?–》棧
- 棧這種數據結構有什麼特點?–》先進後出(FILO)
- 一個方法包含了那些內容? --》數據、指令、返回地址
- 線程執行一個方法,需要把這個方法壓入棧中,壓入棧的方法的數據結構是什麼? --》棧幀
- 棧幀是不是裝載了一個方法,需要包含那些內容? --》 局部標量表,操作數棧,動態鏈接、出口(返回值)、附加信息(不強制要求附帶,一般忽略)
- 每個線程執行的方法時要壓入棧幀到虛擬機棧,每個線程執行方法是否一樣?–》 虛擬機棧是否線程私有? --》 虛擬機棧線程私有
- 線程每執行一個方法就要壓入一個裝載需要執行的方法的棧幀,方法嵌套調用方法如何執行 --》把方法中嵌套調用的方法棧幀壓入棧頂
- 遞歸調用是否需要重複壓棧幀入棧–》線程每執行一個方法就要壓入一個裝載需要執行的方法的棧幀–》是
- 是否能無限嵌套遞歸?–》虛擬機棧是否有大小限制?–》棧都有大小限制–》不能
- 當虛擬機棧大小不夠用的會怎麼樣 --》 拋出棧內存溢出異常 StackOverflowError
在講述棧幀具體的構成前需要一個輔助程序
public class JVM {
//成員變量
private Object obj = new Object();
private int sss = 0;
//局部變量
public void methodOne(int i){
int j = 0;
int sum = i + j;
Object acb = obj;
long start = System.currentTimeMillis();
methodTwo();
return;
}
public void methodTwo(){
File file = new File("");
}
}
反編譯出來的結果如下
public class learn.com.JVM {
public learn.com.JVM();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: new #2 // class java/lang/Object
8: dup
9: invokespecial #1 // Method java/lang/Object."<init>":()V
12: putfield #3 // Field obj:Ljava/lang/Object;
15: aload_0
16: iconst_0
17: putfield #4 // Field sss:I
20: return
public void methodOne(int);
Code:
0: iconst_0
1: istore_2
2: iload_1
3: iload_2
4: iadd
5: istore_3
6: aload_0
7: getfield #3 // Field obj:Ljava/lang/Object;
10: astore 4
12: invokestatic #5 // Method java/lang/System.currentTimeMillis:()J
15: lstore 5
17: aload_0
18: invokevirtual #6 // Method methodTwo:()V
21: return
public void methodTwo();
Code:
0: new #7 // class java/io/File
3: dup
4: ldc #8 // String
6: invokespecial #9 // Method java/io/File."<init>":(Ljava/lang/String;)V
9: astore_1
10: return
}
javap指令集:下面顯示只是抽取的部分指令,查看更全的指令:javap指令集
棧和局部變量操作
將常量壓入棧的指令
aconst_null 將null對象引用壓入棧
iconst_m1 將int類型常量-1壓入棧
iconst_0 將int類型常量0壓入棧
iconst_1 將int類型常量1壓入棧
iconst_2 將int類型常量2壓入棧
iconst_3 將int類型常量3壓入棧
iconst_4 將int類型常量4壓入棧
iconst_5 將int類型常量5壓入棧
astore 將將引用類型或returnAddress類型值存入局部變量
istore_0 將int類型值存入局部變量0
istore_1 將int類型值存入局部變量1
istore_2 將int類型值存入局部變量2
istore_3 將int類型值存入局部變量3
iload_0 從局部變量0中裝載int類型值
iload_1 從局部變量1中裝載int類型值
iload_2 從局部變量2中裝載int類型值
iload_3 從局部變量3中裝載int類型值
iadd 執行int類型的加法
局部標量表
定義:局部標量表是一組變量值的存儲空間,用於存放 方法參數 和 局部變量。局部變量數組所需要的空間在編譯期間完成分配,在方法運行期間不會改變局部變量數組的大小。變量槽 (Variable Slot)是局部變量表的最小單位,沒有強制規定大小爲 32 位。
引導思考:
-
線程執行過程中的變量存儲在哪裏?–》局部變量表
-
局部變量表的數據長度是多少位? --》 32位 --》尋址空間是多大? --》 2^32 = 4G
-
我們從代碼中看到,方法methodOne中,第一行代碼
j = 0;
被編譯成字節碼指令
0: iconst_0 //將int類型常量0壓入棧 1: istore_2 //將int類型值存入局部變量2
-
爲什麼我們的 局部變量 j 在局部變量表中的位置是2 ?–》成員函數是否有一個隱式的引用 this,方法有形參?—》this引用存入了成員變量表0的位置,形參存入了1的位置–》j 只能存入爲2的位置
操作數棧
操作變量的內存模型。操作數棧的最大深度在編譯的時候已經確定(寫入方法區code屬性的max_stacks項中)。操作數棧的的元素可以是任意Java類型,包括long和double,方法剛開始執行的時候,棧是空的,當方法執行過程中,各種字節碼指令往棧中存取數據。
程序代碼
int sum = i + j;
編譯成字節碼:
2: iload_1 //從局部變量1中裝載int類型值
3: iload_2 //從局部變量2中裝載int類型值
4: iadd //iadd 執行int類型的加法
5: istore_3 //將int類型值存入局部變量3
引導思考:
- 線程運行時數據計算的中間過程中,數據是如何存儲的? --》存儲在操作數棧中(32位)
- double類型佔用多少個棧單位空間 --》double是64位,棧是32位 --》double佔用2個棧單位空間
- 在程序計算操作的中間值存儲在哪裏? --》 操作數棧,計算完成後寫回局部變量表
動態鏈接
每個棧幀都包含一個執行運行時常量池中該棧幀所屬方法的引用,持有這個引用是爲了支持方法調用過程中的動態連接(Dynamic Linking)。
Class 文件中存放了大量的符號引用,字節碼中的方法調用指令就是以常量池中指向方法的符號引用作爲參數。這些符號引用一部分會在類加載階段或第一次使用時轉化爲直接引用,這種轉化稱爲靜態解析。另一部分將在每一次運行期間轉化爲直接引用,這部分稱爲動態連接。
引導思考:
- 方法內除了基本的類型,還有引用類型(多態),引用類型(多態)如何存儲記錄 --》 動態鏈接,運行期間指向引用的對象/方法地址
出口(返回值)
如果有返回值的話,壓入調用者棧幀中的操作數棧中,並且把PC的值指向 方法調用指令 後面的一條指令地址。
引導思考:
- 程序什麼時候會到達出口? --》
當執行遇到返回指令時,會將返回值傳遞給上層的方法調用者,這種退出的方式稱爲正常完成出口(Normal Method Invocation Completion),一般來說,調用者的PC計數器可以作爲返回地址。
當執行遇到異常時,並且當前方法體內沒有得到處理,就會導致方法退出,此時是沒有返回值的,稱爲異常完成出口(Abrupt Method Invocation Completion),返回地址要通過異常處理器表來確定。 - 當程序返回時,有哪些可能的操作? --》
- 恢復上層方法的局部變量表和操作數棧
- 把返回值壓入調用者調用者棧幀的操作數棧
- 調整 PC 計數器的值以指向方法調用指令後面的一條指令
本地方法棧(Native Method Stack)
與VM Strack相似,VM Strack爲JVM提供執行JAVA方法的服務,Native Method Stack則爲JVM提供使用native 方法的服務。
(1)調用本地native的內存模型
(2)線程獨享。
方法區(Method Area)
Object Class Data(加載類的類定義數據) 是存儲在方法區的。存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯後的代碼等數據。
垃圾回收在這個區域會比較少出現,這個區域內存回收的目的主要針對常量池的回收和類的卸載。
(1)線程共享的
(2)運行時常量池:
A、是方法區的一部分
B、存放編譯期生成的各種字面量和符號引用
C、Class文件中除了存有類的版本、字段、方法、接口等描述信息,還有一項是常量池,存有這個類的 編譯期生成的各種字面量和符號引用,這部分內容將在類加載後,存放到方法區的運行時常量池中。
堆(Heap)
Heap(堆)是JVM的內存數據區。
1. Java堆是虛擬機管理的內存中最大的一塊
2. Java堆是所有線程共享的區域
3. 在虛擬機啓動時創建
4. 此內存區域的唯一目的就是存放對象實例,幾乎所有對象實例都在這裏分配內存。實際上也只是保存對象實例的屬性值,屬性的類型和對象本身的類型標記等,並不保存對象的方法(以幀棧的形式保存在Stack中)
5. 對象實例在Heap 中分配好以後,需要在Stack中保存一個4字節的Heap 內存地址(引用地址)
6. Java堆是垃圾收集器管理的內存區域,因此很多時候稱爲“GC堆”
7. java堆處於物理不連續的內存空間中,只要邏輯上連續即可。