虛擬機字節(jié)碼執(zhí)行引擎--運行時棧幀結(jié)構(gòu)

聲明:本文摘抄自《深入理解Java虛擬機》一書,本文完全為自我學習,請感興趣的同學購買正版,支持原創(chuàng)

運行時棧幀結(jié)構(gòu)

棧幀(Stack Frame)是用于支持虛擬機方法調(diào)用和方法執(zhí)行的數(shù)據(jù)結(jié)構(gòu),它是虛擬機運行時數(shù)據(jù)區(qū)中的虛擬機棧的棧元素。棧幀存儲了方法的局部變量,操作數(shù)棧,動態(tài)連接,方法返回地址和其他一些額外的附加信息。每一個方法的調(diào)用從開始到執(zhí)行結(jié)束的過程,都對應著一個棧幀在虛擬機棧中入棧和出棧的過程。

在編譯代碼的時候,棧幀需要多大的局部變量表,多深的操作數(shù)棧都已經(jīng)完全確定了,并且寫入到方法表的Code屬性之中。因此一個棧幀需要分配多少內(nèi)存,不會受到程序運行期間變量數(shù)據(jù)的影響,而僅僅取決于虛擬機的實現(xiàn)。

對于執(zhí)行引擎來說,在活動線程中,只有位于棧頂?shù)臈攀怯行У模Q為當前棧幀(Current Stack Frame),與這個棧幀相關聯(lián)的方法稱為當前方法(Current Method)。執(zhí)行引擎運行的所有字節(jié)碼指令都只針對當前棧幀進行操作。

典型的棧幀結(jié)構(gòu)如下圖:


典型的棧幀結(jié)構(gòu)
局部變量表

局部變量表(Local Variable Table)是一組變量值存儲空間,用于存放方法參數(shù)和方法內(nèi)部定義的局部變量。在編譯Class文件時,就在方法的Code屬性的max_locals數(shù)據(jù)項中已經(jīng)確定了該方法需要分配的局部變量表的最大容量。

局部變量的容量以變量槽(Variable Slot)為最小單位,虛擬機規(guī)范中并沒有明確指定一個Slot應占用的內(nèi)存空間大小。只是很有導向性的說到每個Slot都應該能存放一個boolean, byte, char, short, int, float, reference或returnAddress類型的數(shù)據(jù)。它允許Slot的長度可以隨著處理器,操作系統(tǒng)或虛擬機的不同而發(fā)生變化。Reference類型表示對一個對象實例的引用,虛擬機規(guī)范并沒有說明它的長度,也沒有明確指出這種引用應有怎樣的結(jié)構(gòu)。但一般來說,虛擬機實現(xiàn)至少可以通過這個引用做到兩點:一是從此引用直接或間接查詢到對象在Java堆中的數(shù)據(jù)存放的起始地址索引,二是從此引用直接或間接的查找到對象所屬的數(shù)據(jù)類型在方法區(qū)中的存儲的類型信息。returnAddress類型目前已經(jīng)很少見了,它是為字節(jié)碼指令jsr,jsr_w和ret服務的,指向了一條字節(jié)碼指令的地址,很古老的Java虛擬機曾經(jīng)使用這幾條指令來實現(xiàn)異常處理,現(xiàn)在已經(jīng)由異常表代替。

對于64位的數(shù)據(jù)類型,虛擬機會以高位對齊的方式為其分配兩個連續(xù)的Slot空間。Java語言中明確的64位的數(shù)據(jù)類型只有l(wèi)ong和double兩種。由于局部變量表建立在線程的堆棧上,是線程私有的數(shù)據(jù),無論讀寫兩個連續(xù)的Slot是否為原子操作,都不會引起數(shù)據(jù)安全問題。

在方法執(zhí)行時,虛擬機是使用局部變量表完成參數(shù)值到參數(shù)變量列表的傳遞過程,如果執(zhí)行的是實例方法(非static方法),那局部變量表中第0為索引的Slot默認是用于傳遞方法所屬對象實例的引用,在方法中可以通過關鍵字“this”來訪問到這個隱含的參數(shù)。其余參數(shù)則按照參數(shù)表順序排列,占用從1開始的局部變量Slot,參數(shù)表分配完畢,再根據(jù)方法體內(nèi)部定義的變量順序和作用域分配其余的Slot。

為了盡可能節(jié)省棧幀空間,局部變量表的Slot是可以重用的,方法體中定義的變量,其作用域并不一定會覆蓋整個方法體,如果當前字節(jié)碼PC計數(shù)器的值已經(jīng)超出了某個變量的作用域,那這個變量對應的Slot就可以交給其他變量使用。不過,這樣的設計除了節(jié)省棧幀空間以外,還伴隨一些額外的副作用,例如:在某些情況下,Slot的復用會直接影響到系統(tǒng)的垃圾收集行為。

操作數(shù)棧

操作數(shù)棧(Operand Stack)是一個后入先出(Last In First Out, LIFO)棧。操作數(shù)棧的最大深度在編譯的時候已經(jīng)寫入到Code屬性的max_stacks數(shù)據(jù)項中。操作數(shù)棧中的元素可以是任意的Java數(shù)據(jù)類型,包括long和double。32位數(shù)據(jù)類型所占的棧容量為1,64位數(shù)據(jù)類型所占的棧容量為2。在方法執(zhí)行的時候,操作數(shù)棧的深度都不會超過max_stacks數(shù)據(jù)項中設定的最大值。

當一個方法剛開始執(zhí)行的時候,這個方法的操作數(shù)棧是空的,在方法執(zhí)行過程中,會有各種字節(jié)碼指令往操作數(shù)棧中寫入和讀取內(nèi)容。例如,整數(shù)加法的字節(jié)碼指令iadd在運行的時候操作數(shù)棧中最接近棧頂?shù)膬蓚€元素已經(jīng)存入了兩個int型的數(shù)值,當執(zhí)行這個指令的時候,會將這兩個數(shù)值出棧并相加,然后將相加的結(jié)果入棧。

操作數(shù)棧中的元素數(shù)據(jù)類型必須與字節(jié)碼指令的序列嚴格匹配,在編譯程序代碼的時候,編譯器要嚴格保證這一點,在類校驗階段的數(shù)據(jù)流分析中還要再次驗證這一點。再以上面的iadd指令為例,這個指令用于整型數(shù)加法,它在執(zhí)行時,最接近棧頂?shù)膬蓚€元素的數(shù)據(jù)類型必須為int型,不能出現(xiàn)一個為long和一個float使用iadd命令相加的情況。

動態(tài)連接

每個棧幀都包含一個指向運行時常量池中該棧幀所屬方法的引用,持有這個引用是為了支持方法調(diào)用過程中的動態(tài)鏈接(Dynamic Linking)。

方法返回地址

當一個方法開始執(zhí)行后,只有兩種方式可以退出該方法。第一種方式是執(zhí)行引擎遇到任意一個方法返回的字節(jié)碼指令,這時候可能會有返回值傳遞給上層的調(diào)用者,是否有返回值和返回值的類型將根據(jù)遇到何種方法返回指令來決定,這種退出方法的方式稱為正常完成出口(Normal Method Invocation Completion)。另一種退出方式是,在方法執(zhí)行過程中遇到異常,并且這個異常并沒有在方法體中得到處理,無論是Java虛擬機內(nèi)部產(chǎn)生的異常,還是代碼中使用athrow字節(jié)碼指令產(chǎn)生的異常,只要在本方法的異常表中沒有搜索到匹配的異常處理器,就會導致方法退出,這種退出方法的方式稱為異常完成出口(Abrupt Method Invocation Completion)。一個方使用異常完成出口的方式退出,是不會給他的上層調(diào)用者產(chǎn)生任何返回值的。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容