JVM_1.7_執行時記憶體區域_棧幀
執行時記憶體區域這塊,如果不將記憶體各個區域做什麼的瞭解清楚,後面看的會很累。
之前將JVM執行時記憶體區域的內容,整理在了一篇文章中。
在後續深入、細緻的學習中,整理的內容越來越多,一篇的話,會導致篇幅過長。
所以將《JVM執行時記憶體區域詳解》分為以下幾個章節:
這裡將《Java虛擬機器規範中文版》上傳了,點選下面連結,即可下載
目錄
棧幀
《深入理解Java虛擬機器:JVM高階特性與最佳實踐》
棧幀(Stack Frame)是用於支援虛擬機器進行方法呼叫和方法執行的資料結構,它是虛擬機器執行時資料區中Java虛擬機器棧的棧元素
(Java虛擬機器棧中儲存的就是一個個的棧幀)
棧幀儲存了方法的區域性變量表、運算元棧、動態連結和方法返回地址等資訊。
每一個方法從呼叫開始到執行完成的過程,就對應著一個棧幀在Java虛擬機器棧中,從入棧到出棧的過程。
每一個棧幀都包括了局部變量表、運算元棧、動態連結、方法返回地址和一些額外的附加資訊。
在編譯程式程式碼的時候,棧幀中需要多大的區域性變量表、多深的運算元棧都已經完全確定好了,因此一個棧幀需要分配多深記憶體,不會受程式執行期變數資料的影響,而僅僅取決於具體的虛擬機器實現。
一個執行緒中的方法呼叫鏈可能會很長,很多方法都同時處於執行狀態。
對於執行引擎來講,活動執行緒中,只有在棧頂的棧幀是有效的,稱為當前棧幀(Current Stack Frame)
區域性變量表
區域性變量表是一組變數值儲存空間,用於存放方法引數和方法內部定義的區域性變數。
在方法執行時,虛擬機器是使用區域性變數表完成引數值到引數變數列表的傳遞過程;
如果執行的是例項方法(非static的方法),那區域性變量表中第0位索引預設用於傳遞方法所屬物件例項的引用,在方法中可以通過關鍵字"this"來訪問到這個隱含的引數。其餘引數則按照引數表順序排序。
class Example3a { public static int runClassMethod(int i, long l, float f, double d, Object o, byte b) { return 0; } public int runInstanceMethod(char c, double d, short s, boolean b) { return 0; } }
從上面程式碼和圖例,很清楚的看出了局部變量表的一個狀態。
運算元棧
運算元棧也常被稱為操作棧,它是一個後入先出棧(Last In First Out , LIFO)
運算元棧的每一個元素可以是任意的Java資料型別,包括long和double。
當一個方法剛剛開始執行的時候,這個方法的運算元棧是空的;
在方法的執行過程中,會有各種位元組碼指令向運算元棧中寫入和提取內容,也就是入棧出棧操作。
例如:在做算術運算的時候是通過運算元棧來進行的。
整數加法的位元組碼指令iadd在執行的時候要求運算元棧中最接近棧頂的兩個元素已經存入了int型的數值,當執行這個指令時,會將這兩個int值出棧並相加,然後將相加結果入棧。
(這個栗子是不是有點眼熟,還記得小故事中的那個插畫嘛?)
Java虛擬機器的解釋執行引擎稱為“基於棧的執行引擎”,其中所指的“棧”就是運算元棧。
動態連結
每個棧幀都包含一個指向執行時常量池(Runntime Constant Pool)中該棧幀所屬方法的引用,持有這個引用是為了支援方法呼叫過程中的動態連線。
返回地址
當一個方法開始執行後,只有兩種方式可以退出這個方法。
- 第一種方式:
執行引擎遇到任意一個方法返回的位元組碼指令,這時候可能會有返回值傳遞給上層的方法呼叫者,這種退出方法的方式稱為正常完成出口。
- 第二種方式:
在方法執行過程中遇到了異常,並且這個異常沒有在方法體內得到處理,無論是Java虛擬機器內部產生的異常,還是程式碼中使用athrow位元組碼指令產生的異常,只要在笨方法的異常表中沒有搜尋到匹配的異常處理器,就會導致方法退出,這種退出方法的方式稱為異常完成出口。
一個方法使用異常完成出口的方式退出,是不會給它的上層呼叫者產生任何返回值的。
無論採用何種退出方式,在方法退出之後,都需要返回到方法被呼叫的位置,程式才能繼續執行,方法返回時可能需要在棧幀中儲存一些資訊,用來幫助恢復它的上層方法的執行狀態。
方法退出的過程實際上等同於把當前棧幀出棧。
《Java Virtual Machine Specification Java SE 7 中文版》
棧幀(Frame)是用來儲存資料和部分過程結果的資料結構,同時也被用來處理動態連結、方法返回值和異常分派。
棧幀隨著方法呼叫而建立,隨著方法結束而銷燬;無論方法是正常完成還是異常完成(丟擲了在方法內未被捕獲的異常)都算作方法結束。
棧幀的儲存空間分配在Java虛擬機器棧之中。
每一個棧幀都有自己的區域性變量表、運算元棧和執行當前方法所屬的類的執行時常量池的引用。
棧幀容量的大小僅僅取決於 Java 虛擬機器的實現和方法呼叫時可被分配的記憶體。
在一條執行緒之中,只有目前正在執行的那個方法的棧幀是活動的。
這個棧幀就被稱為是當前棧幀( Current Frame),這個棧幀對應的方法就被稱為是當前方法( Current Method),定義這個方法的類就稱作當前類( Current Class)。
對區域性變量表和運算元棧的各種操作,通常都指的是對當前棧幀的對區域性變量表和運算元棧進行的操作。
區域性變量表
每個棧幀內部都包含一組稱為區域性變量表(Local Variables)的變數列表。
一個區域性變數可以儲存一個型別為 boolean、byte、char、short、float、reference和 returnAddress 的資料,兩個區域性變數可以儲存一個型別為 long 和 double 的資料。
Java 虛擬機器使用區域性變量表來完成方法呼叫時的引數傳遞,當一個方法被呼叫的時候,它的引數將會傳遞至從 0 開始的連續的區域性變量表位置上。
特別地,當一個例項方法被呼叫的時候,第0個區域性變數一定是用來儲存被呼叫的例項方法所在的物件的引用(即 Java 語言中的“this”關鍵字),後續的其他引數將會傳遞至從 1 開始的連續的區域性變量表位置上。
運算元棧
每一個棧幀內部都包含一個稱為運算元棧(Operand Stack)的後進先出(Last-In-First-Out,LIFO)棧。
運算元棧所屬的棧幀在剛剛被建立的時候,運算元棧是空的。
Java虛擬機器提供一些位元組碼指令來從區域性變量表或者物件例項的欄位中複製常量或變數值到運算元棧中,也提供了一些指令用於從運算元棧取走資料、操作資料和把操作結果重新入棧。
在方法呼叫的時候,運算元棧也用來準備呼叫方法的引數以及接收方法返回結果。
舉個例子:
iadd位元組碼指令的作用是將兩個int型別的數值相加,它要求在執行的之前運算元棧的棧頂已經存在兩個由前面其他指令放入的 int 型數值。
在 iadd 指令執行時,2 個 int 值從操作棧中出棧,相加求和,然後將求和結果重新入棧。
每一個運算元棧的成員 (Entry) 可以儲存一個 Java 虛擬機器中定義的任意資料型別的值,包括 long 和 double 型別。
在運算元棧中的資料必須被正確地操作,這裡正確操作是指對運算元棧的操作必須與運算元棧棧頂的資料型別相匹配,例如不可以入棧兩個int型別的資料,然後當作long型別去操作他們,或者入棧兩個float型別的資料,然後使用iadd指令去對它們進行求和。
在任意時刻,運算元棧都會有一個確定的棧深度,一個long或者double型別的資料會佔用兩個單位的棧深度,其他資料型別則會佔用一個單位深度。
動態連結
每一個棧幀內部都包含一個指向執行時常量池(RunTime Constant Pool)的引用來支援當前方法的程式碼實現動態連結 (Dynamic Linking)。
在Class檔案裡面,描述一個方法呼叫了其他方法,或者訪問其成員變數是通過符號引用(Symbolic Reference)來表示的,動態連結的作用就是將這些符號引用所表示的方法轉換為實際方法的直接引用。
類載入的過程中將要解析掉尚未被解析的符號引用,並且將變數訪問轉化為訪問這些變數的儲存結構所在的執行時記憶體位置的正確偏移量。
返回地址-方法正常呼叫完成
方法正常呼叫完成是指在方法的執行過程中,沒有任何異常被丟擲——包括直接從Java虛擬機器之中丟擲的異常以及在執行時通過throw語句顯式丟擲的異常。
如果當前方法呼叫正常完成的話,它很可能會返回一個值給呼叫它的方法,方法正常完成發生在一個方法執行過程中遇到了方法返回的位元組碼指令的時候(例如return),使用哪種返回指令取決於方法返回值的資料型別(如果有返回值的話)。
在這種場景下,當前棧幀承擔著回覆呼叫者狀態的責任,其狀態包括呼叫者的區域性變量表、運算元棧和被正確增加過來表示執行了該方法呼叫指令的程式計數器等。
使得呼叫者的程式碼能在被呼叫的方法返回並且返回值被推入呼叫者棧幀的運算元棧後繼續正常地執行。
返回地址-方法異常呼叫完成
方法異常呼叫完成是指在方法的執行過程中,某些指令導致了Java虛擬機器丟擲異常,並且虛擬機器丟擲的異常在該方法中沒有辦法處理,或者在執行過程中遇到了athrow位元組碼指令顯示的丟擲異常,並且在該方法內部沒有把異常捕獲住。
如果在方法異常呼叫完成,那一定不會有方法返回值給它的呼叫者。