目錄
虛擬機棧(JVM Stack)的介紹
與程序計數器一樣,Java虛擬機棧(Java Virtual Machine Stack)也是線程私有的,它的生命週期與線程相同。虛擬機棧描述的是Java方法執行的線程內存模型:每個方法被執行的時候,Java虛擬機都會同步創建一個棧幀(Stack Frame)用於存儲 局部變量表、操作數棧、動態連接、方法出口 等信息。每一個方法被調用直至執行完畢的過程,就對應着一個棧幀在虛擬機棧中從入棧到出棧的過程。
局部變量表中存儲着方法的相關局部變量,包括各種基本數據類型,對象的引用,返回地址等。在局部變量表中,只有long和double類型會佔用2個局部變量空間(Slot,對於32位機器,一個Slot就是32個bit),其它都是1個Slot。需要注意的是,局部變量表是在編譯時就已經確定好的,方法運行所需要分配的空間在棧幀中是完全確定的,在方法的生命週期內都不會改變。
虛擬機棧中定義了兩種異常,如果線程調用的棧深度大於虛擬機允許的最大深度,則拋出 StatckOverFlowError(棧溢出);不過多數Java虛擬機都允許動態擴展虛擬機棧的大小(有少部分是固定長度的),所以線程可以一直申請棧,直到內存不足,此時,會拋出OutOfMemoryError(內存溢出)。
每個線程對應着一個虛擬機棧,因此虛擬機棧也是線程私有的。
虛擬機棧主要用於存儲四部分內容
【局部變量表】、【操作數棧】、【動態連接】和【方法返回地址】
1. Java虛擬機棧也是線程私有的,它的生命週期與線程相同(隨線程而生,隨線程而滅)
2. 如果線程請求的棧深度大於虛擬機所允許的深度,將拋出StackOverflowError異常;
如果虛擬機棧可以動態擴展,如果擴展時無法申請到足夠的內存,就會拋出OutOfMemoryError異常;
(當前大部分JVM都可以動態擴展,只不過JVM規範也允許固定長度的虛擬機棧)
3. Java虛擬機棧描述的是Java方法執行的內存模型:每個方法執行的同時會創建一個棧幀。
對於我們來說,主要關注的stack棧內存,就是虛擬機棧中局部變量表部分。
棧幀(Stack Frame)
棧幀(Stack Frame)是用於支持虛擬機進行 方法調用 和 方法執行 的數據結構。它是虛擬機運行時數據區中的java虛擬機棧的棧元素。
棧幀存儲了方法的 【局部變量表】、【操作數棧】、【動態連接】和【方法返回地址】等信息。
每一個方法從調用開始至執行完成的過程,都對應着一個棧幀在虛擬機裏面從入棧到出棧的過程。
在編譯程序代碼的時候,棧幀中 需要多大的局部變量表內存,多深的操作數棧都已經完全確定了。 因此一個棧幀需要分配多少內存,不會受到程序運行期變量數據的影響,而僅僅取決於具體的虛擬機實現。
棧執行結構圖
在活動線程中,只有位於棧頂的棧幀纔是有效的,稱爲【當前棧幀】,與這個棧幀相關聯的方法稱爲【當前方法】。
執行引擎運行的所有字節碼指令都只針對當前棧幀進行操作。
局部變量表
局部變量表(Local Variable Table)是一組變量值存儲空間,用於存放方法參數和方法內定義的局部變量。局部變量表的容量以變量槽(Variable Slot)爲最小單位,Java虛擬機規範並沒有定義一個槽所應該佔用內存空間的大小,但是規定了一個槽應該可以存放一個32位以內的數據類型。
虛擬機通過索引定位的方法查找相應的局部變量,索引的範圍是從0~局部變量表最大容量。如果Slot是32位的,則遇到一個64位數據類型的變量(如long或double型),則會連續使用兩個連續的Slot來存儲。
局部變量表的結構:
Slot複用 爲了儘可能節省棧幀空間,局部變量表中的Slot是可以重用的, 也就是說當PC計數器的指令指已經超出了某個變量的作用域(執行完畢), 那這個變量對應的Slot就可以交給其他變量使用。 優點 : 節省棧幀空間。 缺點 : 影響到系統的垃圾收集行爲。 (如大方法佔用較多的Slot,執行完該方法的作用域後沒有對Slot賦值或者清空設置null值,垃圾回收器便不能及時的回收該內存。)
1.局部變量表(Local Variable Table)是一組變量值存儲空間,用於存放方法參數和方法內部定義的局部變量。 並且在Java編譯爲Class文件時,就已經確定了該方法所需要分配的局部變量表的最大容量。
2.局部變量表存放了編譯期可知的各種基本數據類型(boolean、byte、char、short、int、float、long、double)「String是引用類型」,
對象引用(reference類型) 和 returnAddress類型(它指向了一條字節碼指令的地址)
!!!!! 很多人說:基本數據和對象引用存儲在棧中。
當然這種說法雖然是正確的,但是很不嚴謹,只能說這種說法針對的是局部變量。
局部變量存儲在局部變量表中,隨着線程而生,線程而滅。並且線程間數據不共享。
但是,如果是成員變量,或者定義在方法外對象的引用,它們存儲在堆中。
因爲在堆中,是線程共享數據的,並且棧幀裏的命名就已經清楚的劃分了界限 : 局部變量表!
reference(對象實例的引用)
一般來說,虛擬機都能從引用中直接或者間接的查找到對象的以下兩點 :
a.在Java堆中的數據存放的起始地址索引。
b.所屬數據類型在方法區中的存儲類型。
例如:我們在創建一個Student對象時的數據存儲結構:
操作數棧
操作數棧也常被稱爲操作棧,它是一個後入先出棧(LIFO)。同局部變量表一樣,操作數棧的最大深度也是編譯的時候被寫入到方法表的Code屬性的max_stacks數據項中。操作數棧的每一個元素可以是任意Java數據類型,包括long和double。32位數據類型所佔的棧容量爲1,64位數據類型所佔的棧容量爲2。棧容量的單位爲“字寬”,對於32位虛擬機來說,一個”字寬“佔4個字節,對於64位虛擬機來說,一個”字寬“佔8個字節。
當一個方法剛剛執行的時候,這個方法的操作數棧是空的,在方法執行的過程中,會有各種字節碼指向操作數棧中寫入和提取值,也就是入棧與出棧操作。例如,在做算術運算的時候就是通過操作數棧來進行的,又或者調用其它方法的時候是通過操作數棧來行參數傳遞的。
另外,在概念模型中,兩個棧幀作爲虛擬機棧的元素,相互之間是完全獨立的,但是大多數虛擬機的實現裏都會作一些優化處理,令兩個棧幀出現一部分重疊。讓下棧幀的部分操作數棧與上面棧幀的部分局部變量表重疊在一起,這樣在進行方法調用返回時就可以共用一部分數據,而無須進行額外的參數複製傳遞了,重疊過程如下圖:
通過一段代碼來了解操作數棧:
public class OperandStack{
public static int add(int a, int b){
int c = a + b;
return c;
}
public static void main(String[] args){
add(100, 98);
}
}
使用 javap 反編譯 OperandStack 後,根據虛擬機指令集,得出操作數棧的運行流程如下:
add 方法剛開始執行時,操作數棧是空的。當執行 iload_0 時,把局部變量 0 壓棧,即 100 入操作數棧。然後執行 iload_1,把局部變量1壓棧,即 98 入操作數棧。接着執行 iadd,彈出兩個變量(100 和 98 出操作數棧),對 100 和 98 進行求和,然後將結果 198 壓棧。然後執行 istore_2,彈出結果(出棧)。
下面通過一張圖,對比執行100+98操作,局部變量表和操作數棧的變化情況。
動態連接
每個棧幀都包含一個指向運行時 常量池中(運行時常量池(Runtime Constant Pool)是方法區的一部分。) 該棧幀所屬方法的引用,
持有這個引用是爲了支持方法調用過程中的動態連接(Dynamic Linking)。
在類加載階段中的解析階段會將符號引用轉爲直接引用,這種轉化也稱爲靜態解析。
另外的一部分將在每一次運行時期轉化爲直接引用。這部分稱爲動態連接。
爲什麼需要常量池?
A:字節碼文件中需要很多數據的支持,但數據很大,不能直接保存到字節碼文件中,所以常量池的作用就是爲了提供一些符號和常量,便於指令的識別。
方法返回地址
一個方法的結束有兩種方式:
正常執行結束
出現未處理的異常,非正常退出
當一個方法開始執行以後,只有兩種方法可以退出當前方法:
當執行遇到返回指令,會將返回值傳遞給上層的方法調用者,這種退出的方式稱爲正常完成出口(Normal Method Invocation Completion),一般來說,調用者的PC計數器可以作爲返回地址。
當執行遇到異常,並且當前方法體內沒有得到處理,就會導致方法退出,此時是沒有返回值的,稱爲異常完成出口(Abrupt Method Invocation Completion),返回地址要通過異常處理器表來確定。
當方法返回時,可能進行3個操作:
恢復上層方法的局部變量表和操作數棧
把返回值壓入調用者調用者棧幀的操作數棧
調整 PC 計數器的值以指向方法調用指令後面的一條指令
Return:
當一個方法開始執行後,執行引擎遇到任意一個方法返回的字節碼指令(return),會有返回值傳遞給上層的方法調用者,稱正常完成出口
字節碼指令中,返回指令包含ireturn(當返回值是boolean、byte、char、short、int類型時使用)lreturn、freturn、dreturn、areturn(return指令供聲明void的方法、實例初始化方法、類和接口的初始化方法使用)。
在方法中遇到異常(Expection):
並且這個異常沒有在方法內進行處理,也就是隻要在本地方法的異常表中沒有搜索到匹配的異常處理器,就會導致方法退出。簡稱異常完成出口
方法在執行過程中拋出異常時的異常處理存儲在一個異常處理表方便在處理異常時找到處理的對應代碼
使用 javap 反編譯 OperandStack
1. 首先你已經創建了OperandStack.java 文件
2. 電腦上已經有了jdk的運行環境, 通過cmd對 OperandStack.java 文件進行編譯:
指令 D:\gaoeclipselearning\hcgao\learning\javap> javac OperandStack.java
3.此時已經生成了class文件,對OperandStack.class 文件進行反編譯查看:
指令 D:\gaoeclipselearning\hcgao\learning\javap> javap -c -l OperandStack > test.txt
4.查看內容:
Compiled from "OperandStack.java"
public class com.hcgao.common.util.learning.javap.OperandStack {
public com.hcgao.common.util.learning.javap.OperandStack();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 2: 0public static int add(int, int);
Code:
0: iload_0
1: iload_1
2: iadd
3: istore_2
4: iload_2
5: ireturn
LineNumberTable:
line 5: 0
line 6: 4public static void main(java.lang.String[]);
Code:
0: bipush 100
2: bipush 98
4: invokestatic #2 // Method add:(II)I
7: pop
8: return
LineNumberTable:
line 10: 0
line 11: 8
}
下面來一個圖例:
執行 add(1,2) 的過程,最後 ireturn 會將操作數棧棧頂的值返回給調用者
javap的用法格式:
javap <options> <classes>
其中classes就是你要反編譯的class文件。
在命令行中直接輸入javap或javap -help可以看到javap的options有如下選項:
-hep --hep -? 輸出此用法消息
-version 版本信息,其實是當前javap所在jdk的版本信息,不是cass在哪個jdk下生成的。
-v -verbose 輸出附加信息(包括行號、本地變量表,反彙編等詳細信息)
- 輸出行號和本地變量表
-pubic 僅顯示公共類和成員
-protected 顯示受保護的/公共類和成員
-package 顯示程序包/受保護的/公共類 和成員 (默認)
-p -private 顯示所有類和成員
-c 對代碼進行反彙編
-s 輸出內部類型簽名
-sysinfo 顯示正在處理的類的系統信息 (路徑, 大小, 日期, MD5 散列)
-constants 顯示靜態最終常量
-casspath &t;path> 指定查找用戶類文件的位置
-bootcasspath &t;path> 覆蓋引導類文件的位置
一般常用的是-v -l -c三個選項。
javap -v classxx,不僅會輸出行號、本地變量表信息、反編譯彙編代碼,還會輸出當前類用到的常量池等信息。
javap -l 會輸出行號和本地變量表信息。
javap -c 會對當前class字節碼進行反編譯生成彙編代碼。
參考:《深入理解Java虛擬機第二版》、《Java虛擬機規範 JavaSE8版》
https://blog.csdn.net/rongtaoup/article/details/89142396
https://blog.csdn.net/w372426096/article/details/81664431
https://blog.csdn.net/u014296316/article/details/82668670