Java虛擬機(jī)以方法作為最基本的執(zhí)行單元,“棧幀(Stack Frame)”則是用于支持虛擬機(jī)進(jìn)行方法調(diào)用和方法執(zhí)行背后的數(shù)據(jù)結(jié)構(gòu),它也是虛擬機(jī)運(yùn)行時(shí)數(shù)據(jù)區(qū)的虛擬機(jī)棧(Virtual Machine Stack)的棧元素。
每一個(gè)棧幀都包括了局部變量表、操作數(shù)棧、動態(tài)連接、方法返回地址和一些額外的附加信息。在編譯Java程序源碼的時(shí)候,棧幀中需要多大的局部變量表,需要多深的操作數(shù)棧就已經(jīng)被分析計(jì)算出來,并且寫入到方法表的Code屬性之中。
一個(gè)線程中的方法調(diào)用鏈可能會很長,以Java程序的角度來看,統(tǒng)一時(shí)刻、同一條線程里面,在調(diào)用堆棧的所有方法都同時(shí)處于執(zhí)行狀態(tài)。而對于執(zhí)行引擎來講,在活動線程中,只有位于棧頂?shù)姆椒ú攀窃谶\(yùn)行的、生效的,其被稱為“當(dāng)前棧幀(Current Stack Frame)”,與之關(guān)聯(lián)的方法被稱為“當(dāng)前方法(Current Method)”。執(zhí)行引擎所運(yùn)行的所有字節(jié)碼指令都只針對當(dāng)前棧幀進(jìn)行操作。棧幀結(jié)構(gòu)如下圖所示:

1、局部變量表
局部變量表(Local Variables Table)是一組變量值的存儲空間,用于存放方法參數(shù)和方法內(nèi)部定義的局部變量。在Java程序被編譯為Class文件時(shí),就在方法的Code屬性的max_locals數(shù)據(jù)項(xiàng)中確定了該方法所需分配的局部變量表的最大容量。局部變量表的容量以變量槽(Variable Slot)為最小單位。
Java虛擬機(jī)通過索引定位的方式使用局部變量表,索引值的范圍是從0開始至局部變量表最大的變量槽數(shù)量。如果訪問的是32為數(shù)據(jù)類型的變量,索引N就代表了使用第N個(gè)變量槽,如果訪問的是64位數(shù)據(jù)類型的變量,則說明會同時(shí)使用第N和N+1兩個(gè)變量槽。對于兩個(gè)相鄰的共同存放一個(gè)64位數(shù)據(jù)的兩個(gè)變量槽,虛擬機(jī)不允許采用任何方式單獨(dú)訪問其中的某一個(gè)。
當(dāng)一個(gè)方法被調(diào)用時(shí),Java虛擬機(jī)會使用局部變量表來完成參數(shù)值到參數(shù)變量列表的傳遞過程,即實(shí)參到形參的傳遞。如果執(zhí)行的是實(shí)例方法(沒有被static修飾的方法),那局部變量表中第0位索引的變量槽默認(rèn)是用于傳遞方法所屬對象實(shí)例的引用,在方法中可以通過關(guān)鍵字“this”來訪問這到這個(gè)隱含的參數(shù)。其余參數(shù)則按照參數(shù)表順序排列,占用從1開始的局部變量槽,參數(shù)表分配完畢后,再根據(jù)方法體內(nèi)部定義的變量順序和作用域分配其余的變量槽。
2、操作數(shù)棧
操作數(shù)棧(Operand Stack)也常被稱為操作棧,它是一個(gè)后入先出(Last In First Out,LIFO)棧。同局部變量表一樣,操作數(shù)棧的最大深度也在編譯的時(shí)候被寫入到Code屬性的max_stacks數(shù)據(jù)項(xiàng)之中。操作數(shù)棧的每一個(gè)原始都可以是包括long和double在內(nèi)的任意Java數(shù)據(jù)類型。32位數(shù)據(jù)類型所占的棧容量位1,64位數(shù)據(jù)類型所占的棧容量位2。Javac編譯器的數(shù)據(jù)流分析工作保證了在方法執(zhí)行的任何時(shí)候,操作棧數(shù)的深度都不會超過在max_stacks數(shù)據(jù)項(xiàng)中設(shè)定的最大值。
當(dāng)一個(gè)方法剛剛開始執(zhí)行的時(shí)候,這個(gè)方法的操作數(shù)棧是空的,在方法的執(zhí)行過程中,會有各種字節(jié)碼指令往操作數(shù)棧中寫入和提取內(nèi)容,也就是出棧和入棧操作。譬如在做算術(shù)運(yùn)算的時(shí)候是通過 將運(yùn)算涉及的操作數(shù)棧壓入棧頂后調(diào)用運(yùn)算指令來進(jìn)行的,又譬如在調(diào)用其他方法的時(shí)候是通過操作數(shù)棧來進(jìn)行方法參數(shù)的傳遞。
另外在概念模型中兩個(gè)不同棧幀作為不同方法的虛擬機(jī)棧的元素,是完全相互獨(dú)立的。但是在大多虛擬機(jī)的實(shí)現(xiàn)里都會進(jìn)行一些優(yōu)化處理,令兩個(gè)棧幀出現(xiàn)一部分重疊。讓下面棧幀的部分操作數(shù)棧與上面棧幀的部分局部變量表重疊在一起,這樣做不僅節(jié)約了一些空間,更重要的是進(jìn)行方法調(diào)用時(shí)可以直接共用一部分?jǐn)?shù)據(jù),無須進(jìn)行額外的參數(shù)復(fù)制轉(zhuǎn)遞了,重疊的過程如下圖所示:

3、動態(tài)連接
每個(gè)棧幀都包含一個(gè)指向運(yùn)行時(shí)常量池中該棧幀所屬方法的引用,持有這個(gè)引用是為了支持方法調(diào)用過程中的動態(tài)連接(Dynamic Linking)。Class文件的常量池中存有大量的符號引用,字節(jié)碼中的方法調(diào)用指令就以常量池里指向方法的符號引用作為參數(shù)。這些符號引用一部分會在類加載階段或者第一次使用的時(shí)候就被轉(zhuǎn)化為直接引用。這種轉(zhuǎn)化被稱為靜態(tài)解析。另外一部分將在每一次運(yùn)行期間都轉(zhuǎn)化為直接引用,這部分就稱為動態(tài)連接。
4、方法返回地址
當(dāng)一個(gè)方法開始執(zhí)行后,只有兩種方式退出這個(gè)方法。第一種方式是執(zhí)行引擎遇到任意一個(gè)方法返回的字節(jié)碼指令,這時(shí)候可能會有返回值傳遞給上層的方法調(diào)用者(調(diào)用當(dāng)前方法的方法稱為調(diào)用者或者主調(diào)方法),方法是否有返回值以及返回值的類型將根據(jù)遇到何種方法返回指令來決定。
另外一種退出方式是在方法執(zhí)行的過程中遇到了異常,并且這個(gè)異常沒有在方法體內(nèi)得到妥善處理。無論是Java虛擬機(jī)內(nèi)部產(chǎn)生的異常,還是代碼中使用athrow字節(jié)碼指令產(chǎn)生的異常,只要在本方法的異常表中沒有搜索到匹配的異常處理器,就會導(dǎo)致方法退出,這種退出放你發(fā)的方式稱為“異常調(diào)用完成(Abrupt Method Invocation Completion)”。一個(gè)方法使用異常完成出口的方式退出,是不會給它的上層調(diào)用者提供任何返回值的。
無論采用何種退出方式,在方法退出之后,都必須返回到最初方法被調(diào)用時(shí)的位置,程序才能繼續(xù)執(zhí)行,方法返回時(shí)可能需要在棧幀中保存一些信息,用來幫助恢復(fù)它的上層主調(diào)方法的執(zhí)行狀態(tài)。 一般來說,方法正常退出時(shí),主調(diào)方法的PC計(jì)數(shù)器的值就可以作為返回地址,棧幀中很可能會保存這 個(gè)計(jì)數(shù)器值。而方法異常退出時(shí),返回地址是要通過異常處理器表來確定的,棧幀中就一般不會保存這部分信息。
方法退出的過程實(shí)際上等同于把當(dāng)前棧幀出棧,因此退出時(shí)可能執(zhí)行的操作有:恢復(fù)上層方法的 局部變量表和操作數(shù)棧,把返回值(如果有的話)壓入調(diào)用者棧幀的操作數(shù)棧中,調(diào)整PC計(jì)數(shù)器的值 以指向方法調(diào)用指令后面的一條指令等。