JVM中棧的frames詳解
簡介
我們知道JVM運行時數據區域專門有一個叫做Stack Area的區域,專門用來負責執行緒的執行調用。那麼JVM中的棧到底是怎麼工作的呢?快來一起看看吧。
JVM中的棧
小師妹:F師兄,JVM為每個執行緒的運行都分配了一個棧,這個棧到底是怎麼工作的呢?
小師妹,我們先看下JVM的整體運行架構圖:
我們可以看到運行時數據區域分為5大部分。
堆區是存儲共享對象的地方,而棧區是存儲執行緒私有對象的地方。
因為是棧的結構,所以這個區域總是LIFO(Last in first out)。我們考慮一個方法的執行,當方法執行的時候,就會在Stack Area中創建一個block,這個block中持有對本地對象和其他對象的引用。一旦方法執行完畢,則這個block就會出棧,供其他方法訪問。
Frame
JVM中的stack area是由一個個的Frame組成的。
Frame主要用來存儲數據和部分結果,以及執行動態鏈接,方法的返回值和調度異常。
每次調用方法時都會創建一個新Frame。當Frame的方法調用完成時,無論該方法是正常結束還是異常結束(它引發未捕獲的異常),這個frame都會被銷毀。
Frame是從JVM中的stack area中分配的。
每個frame都由三部分組成,分別是自己的local variables數組,自己的operand stack,以及對當前方法的run-time constant pool的引用。
在執行緒的執行過程中,任何一個時刻都只有一個frame處於活動狀態。這個frame被稱為current frame,它的方法被稱為current 方法,定義當前方法的類是當前類。
如果frame中的方法調用另一個方法或該frame的方法結束,那麼這個frame將不再是current frame。
每次調用新的方法,都會創建一個新的frame,並將控制權轉移到調用新的方法生成的框架。
在方法返回時,當前frame將其方法調用的結果(如果有的話)傳回上一個frame,並結束當前frame。
請注意,由執行緒創建的frame只能有該執行緒訪問,並且不能被任何其他執行緒引用。
Local Variables本地變數
每個frame都包含一個稱為其本地局部變數的變數數組。frame的局部變數數組的長度是在編譯的時候確定的。
單個局部變數可以保存以下類型的值:boolean, byte, char, short, int, float, reference, 或者 returnAddress。
如果對於long或double類型的值需要使用一對局部變數來存儲。
局部變數因為存儲在數組中,所以直接通過數字的索引來定位和訪問。
注意,這個數組的索引值是從0開始,到數組長度-1結束。
單個局部變數直接通過索引來訪問就夠了,那麼對於佔用兩個連續局部變數的long或者double類型來說,怎麼訪問呢?
比如說一個long類型佔用數組中的n和n+1兩個變數,那麼我們可以通過索引n值來訪問這個long類型,而不是通過n+1來訪問。
注意,在JVM中,並不一定要求這個n是偶數。
那麼這些局部變數有什麼用呢?
Java虛擬機使用局部變數在方法調用時傳遞參數。
我們知道在java中有兩種方法,一種是類方法,一種是實例方法。
在類方法調用中,所有參數都從局部變數0開始在連續的局部變數中傳遞。
在實例方法調用中,局部變數0始終指向的是該實例對象,也就是this。也就是說真實的參數是從局部變數1開始存儲的。
Operand Stacks
在每個frame內部,又包含了一個LIFO的棧,這個棧叫做Operand Stack。
剛開始創建的時候,這個Operand Stack是空的。然後JVM將local variables中的常量或者值載入到Operand Stack中去。
然後Java虛擬機指令從操作數堆棧中獲取操作數,對其進行操作,然後將結果壓回操作數堆棧。
比如說,現在的Operand Stack中已經有兩個值,1和2。
這個時候JVM要執行一個iadd指令,將1和2相加。那麼就會先將stack中的1和2兩個數取出,相加後,將結果3再壓入stack。
最終stack中保存的是iadd的結果3。
注意,在Local Variables本地變數中我們提到,如果是long或者double類型的話,需要兩個本地變數來存儲。而在Operand Stack中,一個值可以表示任何Java虛擬機類型的值。也就是說long和double在Operand Stack中,使用一個值就可以表示了。
Operand Stack中的任何操作都必須要確保其類型匹配。像之前提到的iadd指令是對兩個int進行相加,如果這個時候你的Operand Stacks中存儲的是long值,那麼iadd指令是會失敗的。
在任何時間點,操作數堆棧都具有關聯的深度,其中long或double類型的值對該深度貢獻兩個單位,而任何其他類型的值則貢獻一個單位深度。
Dynamic Linking動態鏈接
什麼是動態鏈接呢?
我們知道在class文件中除了包含類的版本、欄位、方法、介面
等描述資訊外,還有一項資訊就是常量池(constant pool table),用於存放編譯器生成的各種字面量(Literal)和符號引用(Symbolic References)。
所謂字面量就是常說的常量,可以有三種方式,分別是:文本字元串,八種基本類型和final類型的常量。
而符號引用是指用符號來描述所引用的目標。
符號引用和直接引用有什麼區別呢? 我們舉個例子。
比如我們定義了String name=”jack”, 其中jack是一個字面量,會在字元串常量池(String Pool)中保存一份。
如果我們存儲的時候,存的是name,那麼這個就是符號引用。
如果我們存儲的是jack在字元串常量池中地址,那麼這個就是直接引用。
從上面的介紹我們可以知道,為了實現最終的程式正常運行,所有的符號引用都需要轉換成為直接引用才能正常執行。
而這個轉換的過程,就叫做動態鏈接。
動態鏈接將這些符號方法引用轉換為具體的方法引用,根據需要載入類以解析尚未定義的符號,並將變數訪問轉換為與這些變數的運行時位置關聯的存儲結構中的適當偏移量。
方法執行完畢
方法執行完畢有兩種形式,一種是正常執行完畢,一種是執行過程中拋出了異常。
正常執行完畢的方法可以值返回給調用方。
這種情況下frame的作用就是恢復調用程式的狀態,包括其局部變數和操作數堆棧,並適當增加調用程式的程式計數器以跳過方法調用指令。
如果方法中拋出了異常,那麼該方法將不會有值返回給調用方。
本文已收錄於://www.flydean.com/jvm-thread-stack-frames/
最通俗的解讀,最深刻的乾貨,最簡潔的教程,眾多你不知道的小技巧等你來發現!
歡迎關注我的公眾號:「程式那些事」,懂技術,更懂你!