特點
- Java 虛擬機棧(Java Virtual Machine Stacks)是線程私有的,生命周期隨著線程,線程啟動而產(chǎn)生,線程結(jié)束而消亡。
- Java 虛擬機棧描述的是 Java 方法執(zhí)行的內(nèi)存模型,用于存儲棧幀。線程啟動時會創(chuàng)建虛擬機棧,每個方法在執(zhí)行時會在虛擬機棧中創(chuàng)建一個棧幀,用于存儲局部變量表、操作數(shù)棧、動態(tài)連接、方法返回地址、附加信息等信息。每個方法從調(diào)用到執(zhí)行完成的過程,就對應(yīng)著一個棧幀在虛擬機棧中的入棧(壓棧)到出棧(彈棧)的過程。
- Java 虛擬機棧使用的內(nèi)存不需要保證是連續(xù)的。
- Java 虛擬機規(guī)范即允許 Java 虛擬機棧被實現(xiàn)成固定大?。?code>-Xss),也允許通過計算結(jié)果動態(tài)來擴容和收縮大小。如果采用固定大小的 Java 虛擬機棧,那每個線程的 Java 虛擬機棧容量可以在線程創(chuàng)建的時候就已經(jīng)確定。
Java 虛擬機棧會出現(xiàn)的異常
- 如果線程請求分配的棧容量超過了 Java 虛擬機棧允許的最大容量,Java 虛擬機將會拋出 StackOverflowError 異常。
- 如果 Java 虛擬機棧可以動態(tài)擴展,并且在嘗試擴展的時候無法申請到足夠的內(nèi)存,或者在創(chuàng)建新的線程時沒有足夠的內(nèi)存去創(chuàng)建對應(yīng)的虛擬機棧,那 Java 虛擬機將拋出一個 OutOfMemoryError 異常。
Java 虛擬機棧執(zhí)行過程
可以參考一下這篇文章:https://blog.csdn.net/azhegps/article/details/54092466
棧幀(Stack Frame)
- 棧幀存在于 Java 虛擬機棧中,是 Java 虛擬機棧中的單位元素,每個線程中調(diào)用同一個方法或者不同的方法,都會創(chuàng)建不同的棧幀(可以簡單理解為,一個線程調(diào)用一個方法創(chuàng)建一個棧幀),所以,調(diào)用的方法鏈越多,創(chuàng)建的棧幀越多(代表作:遞歸)。在 Running 的線程,只有當(dāng)前棧幀有效(Java 虛擬機棧中棧頂?shù)臈?,與當(dāng)前棧幀相關(guān)聯(lián)的方法稱為當(dāng)前方法。每調(diào)用一個新的方法,被調(diào)用方法對應(yīng)的棧幀就會被放到棧頂(入棧),也就是成為新的當(dāng)前棧幀。當(dāng)一個方法執(zhí)行完成退出的時候,此方法對應(yīng)的棧幀也相應(yīng)銷毀(出棧)。
棧幀結(jié)構(gòu)如圖:
棧幀結(jié)構(gòu)
局部變量表(Local Variable Table)
- 每個棧幀中都包含一組稱為局部變量表的變量列表,用于存放方法參數(shù)和方法內(nèi)部定義的局部變量。在 Java 程序編譯成 Class 文件時,在 Class 文件格式屬性表中 Code 屬性的 max_locals(局部變量表所需的存儲空間,單位是 Slot) 數(shù)據(jù)項中確定了需要分配的局部變量表的最大容量。
- 局部變量表的容量以變量槽(Variable Slot)為最小單位,不過 Java 虛擬機規(guī)范中并沒有明確規(guī)定每個 Slot 所占據(jù)的內(nèi)存空間大小,只是有導(dǎo)向性地說明每個 Slot 都應(yīng)該存放的8種類型: byte、short、int、float、char、boolean、reference(對象引用就是存到這個棧幀中的局部變量表里的,這里的引用指的是局部變量的對象引用,而不是成員變量的引用。成員變量的對象引用是存儲在 Java 堆(Heap)中)、returnAddress(虛擬機數(shù)據(jù)類型,Sun JDK 1.4.2版本之前使用
jsr/ret指令用于進行異常處理,后續(xù)版本已廢棄這種實現(xiàn)方式,目前使用異常處理器表代替)類型的數(shù)據(jù),這8種類型的數(shù)據(jù),都可以使用32位或者更小的空間去存儲。Java 虛擬機規(guī)范允許 Slot 的長度可以隨著處理器、操作系統(tǒng)或者虛擬機的不同而發(fā)生變化。對于64位的數(shù)據(jù)類型,虛擬機會以高位在前的方式為其分配兩個連續(xù)的 Slot 空間。即 long 和 double 兩種類型。做法是將 long 和 double 類型速寫分割為32位讀寫的做法。不過由于局部變量表建立在線程的堆棧上,是線程的私有數(shù)據(jù),無論讀寫兩個連續(xù)的 Slot 是否是原子操作,都不會引起數(shù)據(jù)安全問題。 - Java 虛擬機通過索引定位的方式使用局部變量表,索引值的范圍是從0開始到局部變量表最大的 Slot 數(shù)量。如果是32位數(shù)據(jù)類型的數(shù)據(jù),索引 n 就表示使用第 n 個 Slot,如果是64位數(shù)據(jù)類型的變量,則說明要使用第 n 和第 n+1 兩個 Slot。
- 在方法執(zhí)行過程中,Java 虛擬機是使用局部變量表完成參數(shù)值到參數(shù)變量列表的傳遞過程。如果是實例方法(非
static方法),那么局部變量表中的第0位索引的 Slot 默認(rèn)是用來傳遞方法所屬對象實例的引用,在方法中可以通過關(guān)鍵字this來訪問這個隱含的參數(shù)。其余參數(shù)按照參數(shù)表的順序來排列,占用從1開始的局部變量 Slot,參數(shù)表分配完畢后,再根據(jù)方法體內(nèi)部定義的變量順序和作用域分配其余的 Slot。 - 局部變量表中的 Slot 是可重用的,方法體中定義的變量,其作用域并不一定會覆蓋整個方法體,如果當(dāng)前字節(jié)碼程序計數(shù)器的值已經(jīng)超過了某個變量的作用域,那么這個變量相應(yīng)的 Slot 就可以交給其他變量去使用,節(jié)省??臻g,但也有可能會影響到系統(tǒng)的垃圾收集行為。
- 局部變量無初始值(實例變量和類變量都會被賦予初始值),類變量有兩次賦初始值的過程,一次在準(zhǔn)備階段,賦予系統(tǒng)初始值;另外一次在初始化階段,賦予開發(fā)者定義的值。因此即使在初始化階段開發(fā)者沒有為類變量賦值也沒有關(guān)系,類變量仍然具有一個確定的默認(rèn)值。但局部變量就不一樣了,如果一個局部變量定義了但沒有賦初始值是不能使用的。
使用一段代碼說明一下局部變量表:
// java 代碼
public int test() {
int x = 0;
int y = 1;
return x + y;
}
// javac 編譯后的字節(jié)碼,使用 javap -v 查看
public int test();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
0: iconst_0
1: istore_1
2: iconst_1
3: istore_2
4: iload_1
5: iload_2
6: iadd
7: ireturn
LineNumberTable:
line 7: 0
line 8: 2
line 9: 4
LocalVariableTable:
Start Length Slot Name Signature
0 8 0 this Lcom/alibaba/uc/TestClass;
2 6 1 x I
4 4 2 y I
對應(yīng)上面的解釋說明,通過 LocalVariableTable 也可以看出來:
Code 屬性:
stack(int x(1個棧深度)+ int y(1個棧深度))=2, locals(this(1 Slot)+ int x(1 Slot)+ int y(1 Slot))=3, args_size(非 static 方法,this 隱含參數(shù))=1
驗證 Slot 復(fù)用,運行以下代碼時,在 VM 參數(shù)中添加 -verbose:gc:
public void test() {
{
byte[] placeholder = new byte[64 * 1024 * 1024];
}
int a = 0; // 當(dāng)這段代碼注釋掉時,System.gc() 執(zhí)行后,也并不會回收這64MB內(nèi)存。當(dāng)這段代碼執(zhí)行時,內(nèi)存被回收了
System.gc();
}
局部變量表中的 Slot 是否還存在關(guān)于 placeholder 數(shù)組對象的引用。當(dāng) int a = 0; 不執(zhí)行時,代碼雖然已經(jīng)離開了 placeholder 的作用域,但是后續(xù)并沒有任何對局部變量表的讀寫操作,placeholder 原本所占用的 Slot 還沒有被其他變量所復(fù)用,所以 placeholder 作為 GC Roots(所有 Java 線程當(dāng)前活躍的棧幀里指向 Java 堆里的對象的引用) 仍然是可達對象。當(dāng) int a = 0; 執(zhí)行時,placeholder 的 Slot 被變量 a 復(fù)用,所以 GC 觸發(fā)時,placeholder 變成了不可達對象,即可被 GC 回收。
操作數(shù)棧(Operand Stack)
- 操作數(shù)棧是一個后入先出(Last In First Out)棧,方法的執(zhí)行操作在操作數(shù)棧中完成,每一個字節(jié)碼指令往操作數(shù)棧進行寫入和提取的過程,就是入棧和出棧的過程。
- 同局部變量表一樣,操作數(shù)棧的最大深度也是Java 程序編譯成 Class 文件時被寫入到 Class 文件格式屬性表的 Code 屬性的 max_stacks 數(shù)據(jù)項中。
- 操作數(shù)棧的每一個元素可以是任意的 Java 數(shù)據(jù)類型,32位數(shù)據(jù)類型所占的棧容量為1,64位數(shù)據(jù)類型所占的棧容量為2,在方法執(zhí)行的任何時候,操作數(shù)棧的深度都不會超過在 max_stacks 數(shù)據(jù)項中設(shè)定的最大值(指的是進入操作數(shù)棧的 “同一批操作” 的數(shù)據(jù)類型的棧容量的和)。
- 當(dāng)一個方法剛剛執(zhí)行的時候,這個方法的操作數(shù)棧是空的,在方法執(zhí)行的過程中,通過一些字節(jié)碼指令從局部變量表或者對象實例字段中復(fù)制常量或者變量值到操作數(shù)棧中,也提供一些指令向操作數(shù)棧中寫入和提取值,及結(jié)果入棧,也用于存放調(diào)用方法需要的參數(shù)及接受方法返回的結(jié)果。例如,整數(shù)加法的字節(jié)碼指令
iadd(使用iadd指令時,相加的兩個元素也必須是 int 型) 在運行的時候?qū)⒉僮鲾?shù)棧中最接近棧頂?shù)膬蓚€ int 數(shù)值元素出棧相加,然后將相加結(jié)果入棧。
以下代碼會以什么形式進入操作數(shù)棧?
// java 代碼
public void test() {
byte a = 1;
short b = 1;
int c = 1;
long d = 1L;
float e = 1F;
double f = 1D;
char g = 'a';
boolean h = true;
}
// 字節(jié)碼指令
0: iconst_1 // 把 a 壓入操作數(shù)棧棧頂
1: istore_1 // 將棧頂?shù)?a 存入局部變量表索引為1的 Slot
2: iconst_1 // 把 b 壓入操作數(shù)棧棧頂
3: istore_2 // 將棧頂?shù)?b 存入局部變量表索引為2的 Slot
4: iconst_1 // 把 c 壓入操作數(shù)棧棧頂
5: istore_3 // 將棧頂?shù)?c 存入局部變量表索引為3的 Slot
6: lconst_1 // 把 d 壓入操作數(shù)棧棧頂
7: lstore 4 // 將棧頂?shù)?d 存入局部變量表索引為4的 Slot,由于 long 是64位,所以占2個 Slot
9: fconst_1 // 把 e 壓入操作數(shù)棧棧頂
10: fstore 6 // 將棧頂?shù)?e 存入局部變量表索引為6的 Slot
12: dconst_1 // 把 f 壓入操作數(shù)棧棧頂
13: dstore 7 // 將棧頂?shù)?f 存入局部變量表索引為4的 Slot,由于 double 是64位,所以占2個 Slot
15: bipush 97 // 把 g 壓入操作數(shù)棧棧頂
17: istore 9 // 將棧頂?shù)?g 存入局部變量表索引為9的 Slot
19: iconst_1 // 把 h 壓入操作數(shù)棧棧頂
20: istore 10 // 將棧頂?shù)?h 存入局部變量表索引為10的 Slot
從上面字節(jié)碼指令可以看出來,除了 long、double、float 類型使用的字節(jié)碼指令不是 iconst 和 istore,其他類型都是使用這兩個字節(jié)碼指令操作,說明 byte、short、char、boolean 進入操作數(shù)棧時,都會被轉(zhuǎn)化成 int 型。
-
在概念模型中,兩個棧幀作為虛擬機棧的元素,是完全相互獨立的。但在大多虛擬機實現(xiàn)會做一些優(yōu)化,令兩個棧幀出現(xiàn)一部分重疊。讓下面的棧幀的部分操作數(shù)棧與上面棧幀的部分局部變量表重疊在一起,這樣在進行方法調(diào)用時就可以共用一部分?jǐn)?shù)據(jù),無需進行額外的參數(shù)復(fù)制傳遞。
棧幀共享 Java 虛擬機的解釋執(zhí)行引擎稱為 “基于棧的執(zhí)行引擎”,其中所指的 “?!?就是操作數(shù)棧。
動態(tài)連接(Dynamic Linking)
- 每個棧幀都包含一個指向運行時常量池(JVM 運行時數(shù)據(jù)區(qū)域)中該棧幀所屬性方法的引用,持有這個引用是為了支持方法調(diào)用過程中的動態(tài)連接。
- 在 Class 文件格式的常量池(存儲字面量和符號引用)中存有大量的符號引用(1.類的全限定名,2.字段名和屬性,3.方法名和屬性),字節(jié)碼中的方法調(diào)用指令就以常量池中指向方法的符號引用為參數(shù)。這些符號引用一部分會在類加載過程的解析階段的時候轉(zhuǎn)化為直接引用(指向目標(biāo)的指針、相對偏移量或者是一個能夠直接定位到目標(biāo)的句柄),這種轉(zhuǎn)化稱為靜態(tài)解析。另外一部分將在每一次的運行期期間轉(zhuǎn)化為直接引用,這部分稱為動態(tài)連接。
看看以下代碼的 Class 文件格式的常量池:
// java 代碼
public Test test() {
return new Test();
}
// 字節(jié)碼指令
Constant pool:
#1 = Methodref #4.#19 // java/lang/Object."<init>":()V
#2 = Fieldref #3.#20 // com/alibaba/uc/Test.i:I
#3 = Class #21 // com/alibaba/uc/Test
#4 = Class #22 // java/lang/Object
#5 = Utf8 i
#6 = Utf8 I
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 Lcom/alibaba/uc/Test;
#14 = Utf8 test
#15 = Utf8 ()I
#16 = Utf8 <clinit>
#17 = Utf8 SourceFile
#18 = Utf8 Test.java
#19 = NameAndType #7:#8 // "<init>":()V
#20 = NameAndType #5:#6 // i:I
#21 = Utf8 com/alibaba/uc/Test
#22 = Utf8 java/lang/Object
public int test();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: getstatic #2 // Field i:I
3: areturn
LineNumberTable:
line 8: 0
LocalVariableTable:
Start Length Slot Name Signature
0 4 0 this Lcom/alibaba/uc/Test;
從上面字節(jié)碼指令看出 0: getstatic #2 // Field i:I 這行字節(jié)碼指令指向 Constant pool 中的 #2,而 #2 中指向了 #3 和 #20 為符號引用,在類加載過程的解析階段會被轉(zhuǎn)化為直接引用(指向方法區(qū)的指針)。
方法返回地址
- 當(dāng)一個方法開始執(zhí)行后,只有兩種方式可以退出這個方法。第一種方式是執(zhí)行引擎遇到任意一個方法返回的字節(jié)碼指令(例如:
areturn),這時候可能會有返回值傳遞給上層的方法調(diào)用者(調(diào)用當(dāng)前方法的方法稱為調(diào)用者),是否有返回值和返回值的類型將根據(jù)遇到何種方法返回指令來決定,這種退出方法的方式稱為正常完成出口(Normal Method Invocation Completion)。 - 另外一種退出方式是,在方法執(zhí)行過程中遇到了異常,并且這個異常沒有在方法體內(nèi)得到處理,無論是Java虛擬機內(nèi)部產(chǎn)生的異常,還是代碼中使用
athrow字節(jié)碼指令產(chǎn)生的異常,只要在本方法的異常處理器表中沒有搜索到匹配的異常處理器,就會導(dǎo)致方法退出,這種退出方法的方式稱為異常完成出口(Abrupt Method Invocation Completion)。一個方法使用異常完成出口的方式退出,是不會給它的上層調(diào)用者產(chǎn)生任何返回值的。 - 無論采用何種退出方式,在方法退出之后,都需要返回到方法被調(diào)用的位置,程序才能繼續(xù)執(zhí)行,方法返回時可能需要在棧幀中保存一些信息,用來幫助恢復(fù)它的上層方法的執(zhí)行狀態(tài)。一般來說,方法正常退出時,調(diào)用者的程序計數(shù)器的值可以作為返回地址,棧幀中很可能會保存這個計數(shù)器值。而方法異常退出時,返回地址是要通過異常處理器表來確定的,棧幀中一般不會保存這部分信息。
- 方法退出的過程實際上就等同于把當(dāng)前棧幀出棧,因此退出時可能執(zhí)行的操作有:恢復(fù)上層方法的局部變量表和操作數(shù)棧,把返回值(如果有的話)壓入調(diào)用者棧幀的操作數(shù)棧中,調(diào)整程序計數(shù)器的值以指向方法調(diào)用指令后面的一條指令等。
簡述:
虛擬機會使用針對每種返回類型的操作來返回,返回值將從操作數(shù)棧出棧并且入棧到調(diào)用方法的方法棧幀中,當(dāng)前棧幀出棧,被調(diào)用方法的棧幀變成當(dāng)前棧幀,程序計數(shù)器將重置為調(diào)用這個方法的指令的下一條指令。
附加信息
虛擬機規(guī)范允許具體的虛擬機實現(xiàn)增加一些規(guī)范里沒有描述的信息到棧幀中,例如與調(diào)試相關(guān)的信息,這部分信息完全取決于具體的虛擬機實現(xiàn)。在實際開發(fā)中,一般會把動態(tài)連接,方法返回地址與其它附加信息全部歸為一類,稱為棧幀信息。

