首頁>技術>

虛擬機器棧,本身就是一個普通的棧,棧中的元素叫做棧幀。

虛擬機器棧是執行緒私有的,每有一個執行緒,虛擬機器就會建立一個虛擬機器棧,執行緒與虛擬機器棧一一對應。執行緒每呼叫一個方法,虛擬機器就會建立一個棧幀,並將此棧幀壓入虛擬機器棧中,當方法呼叫結束後,此棧幀又從虛擬機器棧中彈出。

執行緒每呼叫一個方法,都會起一個棧幀,因此棧幀的容量偏小,棧幀雖小,卻五臟俱全。棧幀包含:

區域性變量表運算元棧動態連結方法返回地址區域性變量表

可以將區域性變量表理解為一個數組,陣列中的內容為所在的棧幀對應的方法上引數以及方法體內的區域性變數,可以有基本資料型別、物件引用(真正的物件在堆上)。

既然是陣列,而陣列的長度是固定的,區域性變量表的長度也是固定的,在編譯期就確定下來了。雖然棧幀是在執行期建立的,但在執行期間,區域性變量表的長度不會改變。

這裡我們使用jclasslib來觀察生成的class檔案,需要先安裝此外掛。如果idea抽風,查不到這個外掛,那直接就去這裡下載吧,下載後不用解壓,最後直接從磁碟匯入該zip包。

以這樣的程式碼為例:

public class Main {    private Object test() {        Object obj = new Object();        double a = 0.12d;        int b = 1;        return obj;    }}

找到test方法,我們可以在Misc(雜項)中可以看到,Maximun local variables,即最大的本地變數個數為5,其實就是區域性變量表的最大長度為5。

我們點到LocalVariable Table(區域性變量表)中看看,到底有什麼元素:

Nr是編號Start PC是位元組碼指令的行號length是元素作用域的長度(Start PC+Length=Misc.Code Length)Index則是元素的下標,或者說slot號(槽位號)name是元素的名稱Descriptor則是元素的描述符。

第1個slot是this引用,對於一個例項方法或者構造方法,區域性變量表的第一個位置都是存放著this引用

第2個slot則是Object型別的引用obj

第3、4個slot則是Double型別的變數a,long和double是64位的,因此佔用兩個槽

第5個slot則是int型別的變數b

int佔用1個槽,byte、short、char都會轉化為int,boolean也會轉化為int,false轉化為0,true轉化為1。

當然,區域性變量表為了節省空間,對出了作用域的變數,其所佔據的slot會被複用。

    private void method() {        int a = 1;        {            int b = 2;            b = a + b;        }        int c = 3;    }

可以看到,此次的流程為:

第1個slot依然儲存this引用,作用域一直持續到PC=11(Start PC+Length)第2個slot給了a,作用域也是持續到PC=11第3個slot給了b,但b的作用域在PC>8時就結束了,因此下一個在PC=8以後才開始分配slot的變數直接可以複用b的slot因此接下來的c直接複用了b的slot運算元棧

運算元棧就是一種普通的棧,遵循後進先出的原則。運算元棧和區域性變量表在訪問資料的方式上存在差異,區域性變量表是透過索引,而運算元棧則是透過一系列的入棧-出棧的操作來訪問資料的。

但運算元棧和區域性變量表有一個相同的地方,就是運算元棧的最大深度與區域性變量表的最大長度,都是在編譯期間就確定。

現在用一個簡單的例子,來理解入棧出棧操作:

    public static void main(String[] args) {        int a = 1;        int b = 2;        int c = a + b;        System.out.println(c);    }

透過jclasslib得到位元組碼後:

 0 iconst_1                                         將數值1壓入棧中 1 istore_1                                         將棧頂元素1出棧,並儲存到區域性變量表中索引為1的slot 2 iconst_2                                         將數值2壓入棧中 3 istore_2                                         將棧頂元素2出棧,並儲存到區域性變量表中索引為2的slot 4 iload_1                                          將區域性變量表中索引為1的元素入棧 5 iload_2                                          將區域性變量表中索引為2的元素入棧                    6 iadd                                             將棧頂的2個元素出棧,執行相加後再入棧 7 istore_3                                         將棧頂元素2出棧,並儲存到區域性變量表中索引為2的slot 8 getstatic #2 <java/lang/System.out>              呼叫靜態變數System.out,PrintStream型別11 iload_3                                          將區域性變量表中索引為3的元素入棧12 invokevirtual #3 <java/io/PrintStream.println>   呼叫PrintStream的println方法輸出棧頂元素15 return                                           方法結束

當然,int型別在-1~5、-128~127、-32768~32767、-2147483648~2147483647範圍,執行入棧操作時,分別對應的指令是iconst、bipush、sipush、ldc(常量池中)。

動態連結

首先需要了解class常量池與執行時常量池

class常量池:

class檔案中除了包含類的版本、欄位、方法、介面等描述資訊外,還有一項資訊就是常量池(constant pool ),在java檔案被編譯成class檔案後,class常量池就會被生成,用於存放編譯器生成的各種字面量和符號引用。

字面量就是我們所說的常量概念,如字串、被宣告為final的常量值等。 符號引用是一組符號來描述所引用的目標,符號可以是任何形式的字面量,只要使用時能無歧義地定位到目標即可(它與直接引用區分一下,直接引用一般是指向方法區的本地指標,相對偏移量或是一個能間接定位到目標的控制代碼)。

符號引用一般包括下面三類常量:

類和介面的全限定名欄位的名稱和描述符方法的名稱和描述符

說白了,字面量就是我們隨便寫的一個字串,而符號引用更像是類的姓名,但是光靠符號引用是不能直接找到這個類。

執行時常量池:

首先,執行時常量池處於方法區,關於方法區在java7/8中又有不同的實現,可以先參考我的另外一篇文章

為什麼用元空間替換永久代?

class檔案在經過載入後(類載入機制,可以參考我的另外一篇文章類的奇幻漂流——類載入機制探秘),會進入記憶體,準確的說,是進入了方法區,我們可以在方法區中看到該class檔案對應的魔數,版本號,常量池(這個常量池是class常量池在方法區的一個副本,為了避免和執行時常量池混淆,我們稱這個常量池為靜態常量池),類,父類和介面陣列,欄位,方法等資訊,可以將這些資訊統稱為類的元資料。

接著,再經過連線中的解析階段後,元資料中的靜態常量池會進入執行時常量池,靜態常量池中的一部分符號引用會被翻譯成直接引用。直接引用就像是一個身份證,上面有你的姓名(符號引用),還有你的地址,透過地址可以直接定位到本人。

上文中,提到了“一部分”,這裡的一部分指的是在編譯期間就能確定方法的呼叫者,且在整個執行期間保持不變,可以直接稱為靜態連結。對於那些在執行時才能確定方法呼叫者的情況,需要在執行時動態地將符號引用轉化為直接引用,即動態地將符號引用連結到直接引用,簡稱為動態連結。

方法返回地址

假設有這樣的一個場景:

線上程t中,首先呼叫A方法,然後在A方法中的第p行(位元組碼中的第p行)呼叫B方法。則虛擬機器首先建立一個虛擬機器棧,將A方法的棧幀壓入棧,接著將B方法的棧幀壓入棧。

那麼當B方法在正常結束(沒有產生異常),B方法的棧幀在出棧後,會給方法A返回一個值,這個值就是p+1,即A方法繼續執行時程式計數器的值。

但是,如果B方法出現異常時,則不會給方法A的棧幀返回任何值。

正是有了方法返回地址,方法A才知道,呼叫完B方法後,自己下一步需要從哪裡開始執行。

12
最新評論
  • BSA-TRITC(10mg/ml) TRITC-BSA 牛血清白蛋白改性標記羅丹明
  • 如何應用OSCache快取框架提高Web應用系統頁面響應效能