接下來的兩篇將介紹在KVM中字節是如何執行的,這是KVM中比較核心的內容,分為兩部分來講,本篇先介紹虛擬機中的棧和幀是如何實現的。
首先來看一些全局指針,在頭文件kvm/vmcommon/h/interPRet.h中定義有以下結構:
這五個變量就像CPU中的寄存器一樣,在KVM的運行過程中起到非?;A性的作用。它們分別是程序計數器、執行棧指針、局部變量指針、當前幀指針和當前常量池指針。
java虛擬機為每一個線程開設一個棧,棧中存儲的數據以“幀”為單位,虛擬機在調用一個新的方法時,會向棧中壓入一個新幀,幀內數據是這個方法的運行狀態,Java字節碼的執行總是在當前幀內進行,方法運行結束時這個幀會被彈出。所以這個??梢苑Q為“方法棧”,幀可以稱為“方法幀”。
按照Java虛擬機的規范,一個幀應由三個部分組成:局部變量區,操作數棧和幀數據區。每個幀的局部變量區和操作數棧的大小都可能不一樣,要依方法本身的龐大程度而定,但在調用一個方法時,可以根據這個方法的字節碼計算出所需要的局部變量區和操作數棧的大小。規范對幀數據區的大小沒有規定,幀數據區的大小和內容可由虛擬機實現來決定。
局部變量區:
局部變量區一般會位于幀中最前面(即地址最?。┑奈恢?,它包含了對應方法的參數和局部變量,一般情況下,它的大小是向4字節對齊的,每4字節是一個“字”,變量以“字”為單位來存入。在它的最前面順序存放的是對應方法的參數,類型為int、float、reference和returnAddress的參數占一個“字”,類型為byte、short和char類型的參數對被轉化為int型,所以也占一個字;long和double類型的值要占用兩個字。當然,“字長”選為多少是由虛擬機實現自己來決定的,不是一定要選4字節為一個字,如果選8字節構成一個字的話,所有值都只占一個字,更加整齊,但是浪費了很多空間。
如果方法不是靜態的,那么虛擬機會自動將方法所在對象的句柄存在局部變量區中索引為0的位置,真正的參數從位置1開始存;而如果方法是靜態的,它就與具體的對象沒有關系,所以不必存放對象句柄,參數從位置0開始存放。
在局部變量區接下來的空間中,虛擬機可以按照任意的方式來存貯方法內的局部變量。
操作數棧:
操作數棧的作用相當于CPU中的通用寄存器,由于Java虛擬機是一臺虛擬的機器,它沒有真正的寄存器,而Java虛擬機也沒有選擇與CPU相似的方式來模擬通用寄存器,而是選擇了另一種方法 — 使用棧,Java指令所使用的操作數都從操作數棧中得到。
某方法在被調用的時候,同樣可計算出它需要多大的操作數棧,所以在一個幀中,操作數棧的大小也是固定,而它的位置可以由實現來決定,不過在接下來KVM的實例中我們會發現,把操作數棧放在幀的最后面(地址最大)的地方是一個好辦法。
幀數據區:
幀數據是由虛擬機實現任意設計的,通常它都被用來實現常量池解析和異常處理等等。
下面來看一看,在KVM中如何實現棧和幀。
數據結構:
在頭文件kvm/vmcommon/h/frame.h中定義了棧和幀的結構:
每一個stackStruct結構體的變量就是一個Java?;騄ava棧的一部分,因為每一個stackStruct結構的大小是固定的,如果不夠用,可以得用next指針來擴展成鏈表。size是本結構體的大小,xxunusedxx是剩余空間,cells則是實際的存貯空間。
每一個線程開始的時候都會生成一個新的stackStruct,在每一次壓入新幀的時候會查看剩余空間是否夠用,如果不夠用,還會再生成新的stackStruct。
frameStruct這個結構的大小是固定的,它并不是一個幀,而只是“幀數據區”,前面說過,由于局部變量區和操作數棧的大小都不固定,所以整個幀的大小也是不固定的。幀的空間是在調用方法的時候臨時計算出來的,然后在當前線程的棧中申請,frameStruct結構的指針占據其中的一個字,其余空間都給局部變量區和操作數棧用。
KVM中棧和幀的模型如下:(為理解方便,暫不考慮棧要擴展的情況)
當棧中只有一個幀時,棧的結構如圖所示:
當棧中只有一個幀時,幀的低字節區是局部變量,接下來會有一個字(4字節)指向幀數據區結構體,再接下來的空間就是操作數棧。
只有一幀時,幀中各部分的結構很明晰,但如果多于一幀時,情況就會有些復雜,下面看當再壓入一幀時的圖示:
這個圖或許跟想像中的不一樣,兩幀數據之間出現了重疊。圖中畫出了一條虛線,這條虛線的位置是上一幀結束的位置,但是卻沒有成為新的一幀開始的位置,新的一幀在這之前就開始了。重疊的區域究竟是什么,可以讓兩幀共用呢?
當一個方法在執行時,如果一個指令需要參數,解釋器會到操作數棧里去裝載參數,如果這時的指令是調用一個方法的話(比如invokevirtual或invokestatic),待調用方法的參數應已經順序存在于操作數棧中,在執行調用指令的時候,這些參數被彈出,成為調用指令的參數,由于操作數棧在幀的最后面,所以這些參數后面再沒有本幀的有效數據。這些參數在當前幀的操作數棧中的排列順序與在新幀的局部變量區中的排列順序是一樣的,而且在新幀中,局部變量區在新幀的最前面,參數列表又在局部變量區的最前面,所以這部分數據是可以重用的,不會丟失有用的信息。
程序實現:
壓入幀和彈出幀的函數在源文件kvm/vmcommon/src/frame.c中:
void pushFrame(METHOD thisMethod);
void popFrame();
pushFrame()函數的一些關鍵代碼如下:
L1-L3分別讀出幀的大小、參數列表的大小和本幀實際申請空間的大?。◤膸袦p去與上一幀復用的部分);
sp是當前棧內的指針,也是操作數的指針,在新的一幀壓入之前,sp應指向操作數棧中最后一個參數的位置,所以L5中prev_sp所取得的是上一幀中函數參數列表的首地址,也就是新幀開始的位置,以后新方法返回的時候,新幀被彈出,這里應是操作數棧的當前位置,也就是sp的位置,函數的返回值要存放到這里;
L7為新幀申請了空間;
L10-L12為保存調用之前的寄存器狀態;
L19-L22為寄存器賦新值。
popFrame()函數比較簡單,主要就是調用了下面這個宏來恢復調用前寄存器的值:
(出處:http://www.49028c.com)
新聞熱點
疑難解答