對(duì)線面試官:淺聊一下 Java 虛擬機(jī)棧?
對(duì)于 JVM(Java 虛擬機(jī))來說,它有兩個(gè)非常重要的區(qū)域,一個(gè)是棧(Java 虛擬機(jī)棧),另一個(gè)是堆。堆是 JVM 的存儲(chǔ)單位,所有的對(duì)象和數(shù)組都是存儲(chǔ)在此區(qū)域的;而棧是 JVM 的運(yùn)行單位,它主管 Java 程序運(yùn)行的。那么為什么它有這樣的魔力?它存儲(chǔ)的又是什么數(shù)據(jù)?接下來,我們一起來看。
1.棧定義
我們先來看棧的定義,我們這里的棧指的是 Java 虛擬機(jī)棧(Java Virtual Machine Stack)也叫做 JVM 棧,《Java虛擬機(jī)規(guī)范》對(duì)此區(qū)域的說明如下:
Each Java Virtual Machine thread has a private Java Virtual Machine stack, created at the same time as the thread. A Java Virtual Machine stack stores frames (§2.6). A Java Virtual Machine stack is analogous to the stack of a conventional language such as C: it holds local variables and partial results, and plays a part in method invocation and return. Because the Java Virtual Machine stack is never manipulated directly except to push and pop frames, frames may be heap allocated. The memory for a Java Virtual Machine stack does not need to be contiguous.
In the First Edition of The Java? Virtual Machine Specification, the Java Virtual Machine stack was known as the Java stack.
This specification permits Java Virtual Machine stacks either to be of a fixed size or to dynamically expand and contract as required by the computation. If the Java Virtual Machine stacks are of a fixed size, the size of each Java Virtual Machine stack may be chosen independently when that stack is created.
A Java Virtual Machine implementation may provide the programmer or the user control over the initial size of Java Virtual Machine stacks, as well as, in the case of dynamically expanding or contracting Java Virtual Machine stacks, control over the maximum and minimum sizes.
The following exceptional conditions are associated with Java Virtual Machine stacks:
- If the computation in a thread requires a larger Java Virtual Machine stack than is permitted, the Java Virtual Machine throws a StackOverflowError.
- If Java Virtual Machine stacks can be dynamically expanded, and expansion is attempted but insufficient memory can be made available to effect the expansion, or if insufficient memory can be made available to create the initial Java Virtual Machine stack for a new thread, the Java Virtual Machine throws an OutOfMemoryError.
以上內(nèi)容翻譯成中文的含義如下:
Java 虛擬機(jī)棧是線程私有的區(qū)域,它隨著線程的創(chuàng)建而創(chuàng)建。它里面保存的是局部變量表(基礎(chǔ)數(shù)據(jù)類型和對(duì)象引用地址)和計(jì)算過程中的中間結(jié)果。Java 虛擬機(jī)的內(nèi)存不需要連續(xù),它只有兩個(gè)操作:入棧和出棧。
Java 虛擬機(jī)棧要么大小固定,要么根據(jù)計(jì)算動(dòng)態(tài)的擴(kuò)展和收縮。程序員可以對(duì) Java 虛擬機(jī)棧進(jìn)行初始值的大小設(shè)置和最大值的設(shè)置。
Java 虛擬機(jī)棧出現(xiàn)的異常有兩種:
- 當(dāng) Java 虛擬機(jī)棧大小固定時(shí),如果程序中的棧分配超過了最大虛擬機(jī)棧就會(huì)出現(xiàn) StackOverflowError 異常。
- 如果 Java 虛擬機(jī)棧是動(dòng)態(tài)擴(kuò)展的,那么當(dāng)內(nèi)存不足時(shí),就會(huì)引發(fā) OutOfMemoryError 的異常。
2.棧結(jié)構(gòu)
棧是線程私有的,每個(gè)線程都有自己的棧(空間),棧中的數(shù)據(jù)是以棧幀(Stack Frame)的形式存在的,線程會(huì)為每個(gè)正在執(zhí)行的方法生成一個(gè)棧幀,如下圖所示:
PS:當(dāng)一個(gè)新的方法被調(diào)用時(shí),就會(huì)在棧中創(chuàng)建一個(gè)棧幀,當(dāng)方法調(diào)用完成之后,也就意味著這個(gè)棧幀會(huì)執(zhí)行出棧操作。
而棧幀中又存儲(chǔ)了 5 個(gè)內(nèi)容:
- 局部變量表(Local Variables);
- 操作(數(shù))棧(Operand Stack);
- 動(dòng)態(tài)鏈接(Dynamic Linking);
- 方法返回地址(Return Address);
- 附加信息。
如下圖所示:
棧的整體存儲(chǔ)結(jié)構(gòu)如下圖所示:
2.1 局部變量表
局部變量表也叫做局部變量數(shù)組或本地變量表。
局部變量表是一個(gè)數(shù)組,里面存儲(chǔ)的內(nèi)容有:
- 方法參數(shù);
- 方法內(nèi)的局部變量,也就是方法內(nèi)的基本數(shù)據(jù)類型和對(duì)象引用(Reference);
- 方法返回類型(Return Address)。
接下來我們通過類生成的字節(jié)碼來觀察一下局部變量表的內(nèi)容,首先,我們先來搞一個(gè) main 方法,具體代碼如下:
然后我們編譯類,再使用“javap -v
LocalVariablesExample.class”查看字節(jié)碼生成的內(nèi)容,其中包含的本地變量表內(nèi)容如下:
我們通過 JClassLib 也能觀察到局部變量表的信息,如下圖所示為局部變量表的長度:
局部變量表的詳細(xì)信息如下:
2.2 操作棧
操作棧也叫做操作數(shù)?;虮硎臼綏#僮鲾?shù)棧主要用于保存計(jì)算過程的中間結(jié)果,同時(shí)作為計(jì)算過程中變量臨時(shí)的存儲(chǔ)空間。
思考:為什么不把程序執(zhí)行過程中的中間結(jié)果保存到局部變量表,而是保存到操作數(shù)棧中呢?
因?yàn)榫植孔兞勘硎菙?shù)組,而數(shù)組的長度是在其創(chuàng)建時(shí)就要確定,所以局部變量表在編譯器就決定內(nèi)容和大小了,那么在程序執(zhí)行中的這些動(dòng)態(tài)中間結(jié)果,是需要新的空間來保存了,而操作數(shù)棧就可以實(shí)現(xiàn)此功能。
2.3 動(dòng)態(tài)鏈接
動(dòng)態(tài)鏈接也叫做指向運(yùn)行時(shí)常量池的方法引用。
這個(gè)區(qū)域的概念和作用稍微難理解一點(diǎn),在每一個(gè)棧幀內(nèi)部都包含一個(gè)指向運(yùn)行時(shí)常量池中該棧幀所屬方法的引用。當(dāng)一個(gè)方法調(diào)用了另外的其他方法時(shí),就是通過常量池中指向方法的符號(hào)引用來表示的,那么動(dòng)態(tài)鏈接的作用就是為了將這些符號(hào)引用轉(zhuǎn)換為調(diào)用方法的直接引用。
也就是說:當(dāng)一個(gè)方法調(diào)用另一個(gè)方法時(shí),不會(huì)再創(chuàng)建一個(gè)被調(diào)用的方法,而是通過常量池的方法引用來調(diào)用,而這個(gè)區(qū)域存儲(chǔ)的就是運(yùn)行時(shí)常量池的方法引用,這個(gè)區(qū)域的作用就是將運(yùn)行時(shí)常量池的符號(hào)引用轉(zhuǎn)換成直接引用。
2.4 方法返回地址
方法返回地址也叫做方法正常退出或異常退出的定義。
方法返回地址存放的是調(diào)用該方法的程序計(jì)數(shù)器的值。程序計(jì)數(shù)器里面保存的是該線程要執(zhí)行的下一行指令的位置。
也就是說:在一個(gè)方法中調(diào)用了另一個(gè)方法,當(dāng)被調(diào)用的方法執(zhí)行完之后,要執(zhí)行的下一行指令就是保存在此區(qū)域的。
2.5 附加信息
此區(qū)域在很多教程上會(huì)被省略,因?yàn)榇藚^(qū)域有可能有數(shù)據(jù),也有可能沒有數(shù)據(jù)。這些附加信息是和 Java 虛擬機(jī)實(shí)現(xiàn)相關(guān)的一些信息。例如,對(duì)程序調(diào)試提供支持的信息。
總結(jié)
棧作為 Java 虛擬機(jī)中最核心的組成部分之一,它包含了以下 5 部分的內(nèi)容:
- 局部變量表(Local Variables):主要存儲(chǔ)的是方法內(nèi)的基本數(shù)據(jù)類型和對(duì)象引用;
- 操作(數(shù))棧(Operand Stack):主要用于保存計(jì)算過程的中間結(jié)果,同時(shí)作為計(jì)算過程中變量臨時(shí)的存儲(chǔ)空間;
- 動(dòng)態(tài)鏈接(Dynamic Linking):存放的是指向運(yùn)行時(shí)常量池的方法引用;
- 方法返回地址(Return Address):存放的是調(diào)用該方法的程序計(jì)數(shù)器的值;
- 一些附加信息:存儲(chǔ)了一些和 Java 虛擬相關(guān)的數(shù)據(jù),比如程序的調(diào)試數(shù)據(jù)。
參考 & 鳴謝
《阿里巴巴Java開發(fā)手冊(cè)》
《尚硅谷JVM》