JVM虛擬機棧原理解析

    一個朋友面試的時候遇到了一個問題,下面代碼的運行結果是什麼(Person類沒有寫出,只有一個age屬性 get  set方法的類)?代碼的運行邏輯非常簡單,但是通過對這個運行邏輯的分析對Java虛擬機棧的運行原理有了新的理解,這裏做一個記錄。最後代碼輸出結果爲:2。

public class MyStack {

    public static void main(String[] args) {
        Person person = new Person();
        person.setAge(10);
        b(person);
        System.out.println(""+person.getAge());
    }

    private static void b(Person person) {
        person.setAge(2);
        person = new Person();
        person.setAge(3);
    }

}

 

一、Java虛擬機棧基礎知識

    Java虛擬機以方法作爲最基本的執行單元。

    每一條Java虛擬機線程都有自己私有的Java虛擬機棧,這個棧與線程同時創建,用於存儲棧幀,棧幀是用於支持虛擬機進行方法調用和方法執行背後的數據結構。

 

二、棧幀

    棧幀的作用是用來存儲數據和部分過程的結果的數據結構,同時也用來處理動態鏈接、方法返回值和異常分派以及支持虛擬機進行方法調用和方法執行。棧幀隨着方法的調用而創建,隨着方法的結束銷燬(無論方法是正常完成還是異常完成,都算作方法結束)。每一個方法被調用直至完畢的過程,就對應這一個棧幀在虛擬機中從入棧到出棧的過程。

局部變量表

    局部變量表是一組變量值的存儲空間,用於存放方法參數和方法內部定義的局部變量。當存在方法之間的調用的時候,實際上是把調用方法的方法參數值拷貝到被調用方法的局部變量表的一個過程(這裏我們暫且這樣理解,實際傳遞方式在下面操作數棧中進行講解),所以我們經常說Java中只有值傳遞,因爲所有的傳引用其實也是一個值傳遞的過程。

    局部變量表的容量以變量槽爲最小單位(《java虛擬機規範》中並沒有規定一個變量槽佔用的內存空間大小)。一個變量槽可以存放一個32位以內的數據類型,Java中佔用存儲空間不超過32位的數據類型有:boolean、byte、char、short、int、float、reference以及returnAddress(目前比較少見)。但是Java中有long和double兩種兩種64位的數據類型,這兩種數據類型一般需要兩個連續的變量槽進行存儲,並且虛擬機在訪問long和double的變量槽的時候必須連續方法,這裏不允許單獨進行訪問其中任何一個。

    局部變量表內容較多,可以自行通過閱讀《深入理解java虛擬機》或者《java虛擬機規範》這兩本書進行細緻瞭解。這裏我們只需要知道局部變量表是用來存放方法的參數和變量的數據結構即可。

 

操作數棧

    操作數棧是一個後入先出(LIFO)棧,同局部變量表一樣,操作數棧的最大深度也在編譯時被寫入到Code屬性的max_stacks數據項中。32位數據類型所佔的棧容量位1,64位數據類型所佔的棧容量位2.

    操作數棧可以理解爲Java虛擬機棧中一個用於計算並臨時存儲數據的區域。例如代碼當中在進行運算的時候是通過將運算設計的參數進行壓棧然後調用相應的計算指令來完成的,完成之後再將結果放入操作數棧供後續使用。

    剛剛上面講到方法之間的參數傳遞是通過局部變量表的拷貝來完成的不完全準確,這裏做個解釋。在方法調用的時候是通過操作數棧來進行方法參數傳遞的。如下圖,調用方法的Java棧幀的操作數棧和被調用方法的Java棧幀的局部變量表是有部分重合的,這樣做的目的爲了節約空間,同時在方法調用的時候可以之間公用參數傳遞的數據。調用方法會將參數拷貝到操作數棧的公共區域,被調用方法執行的時候可以訪問這個公共區域去獲取對應的參數數據,但是這個公共區域僅僅是方法之間的參數公用,,其他參數不能存在與公共區域。

 

 

動態鏈接

    每個棧幀都有一個指向運行時常量池中該棧幀所屬方法的引用,持有這個引用的目的是爲了支持方法調用過程當中的動態鏈接。在類加載階段所有的變量和方法引用都作爲符號引用保存在class文件的常量池裏,程序運行時將其加載進方法區的運行時常量池中。我們在Java虛擬機棧中所複製過來的並不是方法本身,而是方法的一個鏈接,當存在方法之間的調用的時候就通過這個動態鏈接可以找到方法區中對應的被調用方法。

 

方法返回地址

    當一個方法在Java虛擬機棧中開始執行之後,有兩種方式退出這個方法:正常調用完成和異常調用完成。

    正常調用完成,當一個方法正常調用完成時,執行引擎會遇到這個方法返回的字節碼指令,這時候被調用的當前方法會指向調用該方法的主調方法,然後當前方法所在的棧幀出棧,主調方法變爲當前方法。

    異常調用完成,當調用方法遇到異常且代碼中沒有對異常做任何處理的時候,這個時候當前方法不會爲主調方法提供任何的返回值。

    無論採用何種方式退出,在方法退出之後都必須返回主調方法當中調用該方法的位置。方法的退出可以看作是棧幀出棧,當一個方法執行完畢之後(無論正常調用完成還是異常調用完成),當前方法棧幀會出棧,如果有返回信息的話,返回信息會傳遞會調用該方法的主調方法位置(異常調用完成也是如此)。

 

附加信息

    《Java虛擬機規範》允許虛擬機實現增加一些規範裏沒有的描述信息到棧幀當中,例如與調試和性能相關的信息,這部分信息取決於虛擬機自己的實現,這裏不多做描述。

 

三、小節

    Java虛擬機棧相當於是以線程爲單位,一個執行線程擁有一個Java虛擬機棧。在Java虛擬機棧中以棧幀爲基本的執行單位,一個棧幀代表這Java代碼當中的一個方法。一個完整的Java線程執行過程相當於把這個過程涉及的所有方法按照調用的順序以棧幀的形式壓入到Java虛擬機棧當中然後進行順序調用的一個過程,同時操作數棧相當於一個用於計算和臨時存儲數據的區域(同時還用於方法之間的參數傳遞)。

    以上面的demo代碼爲例,一次基本的調用相當於將main方法和b方法順序地壓入到這個線程所擁有的Java虛擬機棧當中然後順序執行,當執行到main方法中發現有調用到了b方法的時候,Java虛擬機會去調用b方法對應的棧幀,當b方法的棧幀內容完全執行完畢之後會丟棄b方法(因爲此時b方法已經完全執行完畢)的棧幀並讓未執行完成的main方法變爲當前棧幀繼續執行。

    Java虛擬機棧的核心是棧幀的順序執行,一個棧幀我們可以看作一個Java方法。而一個Java方法的核心內容位三點:參數傳遞、方法調用以及數據計算。在棧幀當中參數傳遞是通過局部變量表和操作數棧來實現的,方法調用是通過動態鏈接來指向對應需要調用的方法,數據的計算是通過操作數棧來完成。(本段內容爲個人理解,如有問題還請指正)。

 

參考資料:

《深入理解java虛擬機第三版》

《java虛擬機規範》

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章