《深入理解java虛擬機》讀書筆記——運行時棧幀結構

棧幀是用於虛擬機進行方法調用方法執行的數據結構,是虛擬機棧的棧元素。每一個方法從調用開始至執行完成的過程,都對應着一個棧幀在虛擬機棧裏面從入棧到出棧的過程。

每一個棧幀都包括了局部變量表操作數棧動態連接方法返回地址和一些額外的附加信息。在編譯程序代碼的時候,棧幀需要多大的局部變量表,多深的操作數棧都已經完全確定,並且寫入到方法表的Code屬性中,因此一個棧幀需要分配多少內存,不會受到程序運行期變量數據的影響,而僅僅取決於具體的虛擬機實現。

對於執行引擎來說,在活動線程中,只有位於棧頂的棧幀纔是有效的,稱爲當前棧幀與這個棧幀相關聯的方法稱爲當前方法。執行引擎運行的所有字節碼指令都只針對當前棧幀進行操作


一、局部變量表

局部變量表是一組變量值存儲空間,用於存放方法參數和方法內部定義的局部變量。在Class 文件的方法表的 Code 屬性的 max_locals 指定了該方法所需局部變量表的最大容量。
變量槽 (Variable Slot)是局部變量表的容量的最小單位,虛擬機規範沒有明確規定其佔用的內存空間大小,一個 Slot 可以存放一個32位以內的數據類型: boolean、byte、char、short、int、float、reference 和 returnAddress 8種類型。其中 reference 表示對一個對象實例的引用,通過它可以得到對象在Java 堆中存放的起始地址的索引和該數據所屬數據類型在方法區的存儲的類型信息。returnAddress 則指向了一條字節碼指令的地址。 對於64位的 long 和 double 變量而言,虛擬機會以高位對齊的方式爲其分配兩個連續的 Slot 空間(由於局部變量表建立在線程的堆棧上,是線程私有的數據,無論讀寫兩個連續的Slot是否爲原子操作,都不會引起數據安全問題)。

虛擬機通過索引定位的方式使用局部變量表。索引值的範圍是0到局部變量表最大的Slot數量。如果訪問32位數據類型的變量,索引n就代表了使用第n個Slot。如果是64位數據類型的變量,則說明會同時使用n和n+1兩個Slot,對於這兩個Slot,不允許採用任何方式單獨訪問其中的某一個。

在方法執行時,如果執行的是實例方法(非static方法),那局部變量表的第0位索引的Slot默認是用於傳遞方法所屬對象實例的引用,即this關鍵字指代的對象的引用。然後第1個索引Slot開始用於方法的參數表,再接着是根據方法體內部定義的變量順序和作用域分配其餘的Slot。

爲可能節省棧幀空間,局部變量表中的Slot是可以複用的,方法體中定義的變量的作用於不一定會覆蓋整個方法體,如果當前字節碼PC計數器的值已經超過某個變量的作用於,那這個變量對應的Slot就可以被其他變量複用。Slot複用可能會影響到垃圾回收。如下所示(在虛擬機運行參數加上-verbose:gc)

代碼一:



結果一:


----------------------------------------------------------------分割線--------------------------------------------------------------------------


代碼二:



結果二:


從兩個代碼來看,照理來說兩個placeholder在離開他的作用域之後,應該會成爲一個"死對象"而被回收掉,但是根據結果來,代碼一沒被回收二代碼二被回收了。

所以,placeholder對象能否被回收的根本原因是:局部變量表中的Slot是否還存在關於placeholder數據對象的引用。代碼一中雖然已經離開了placeholder的作用域,但在此之後,沒有任何對局部變量表的讀寫操作,placeholder原本所佔用的Slot還沒有被其他變量複用,所以作爲GC Roots一部分的局部變量表還保持着對placeholder的關聯。而代碼二中的a複用了placeholder的Slot,使得placeholder跟局部變量表的關聯被打斷,所以也就被回收。

可以用手動賦值不使用的變量爲null,用來代替那句int a = 0,把變量對應的局部變量表Slot清空(《Pratice Java》中把“不適用的對象手動賦值爲null”作爲一條推薦的編碼規則)。但是不應該對 賦 null 值有過多的依賴,主要有兩點原因:
            1、從編碼的角度來講,用恰當的變量作用域來控制變量的回收纔是最優雅的解決方法。
            2、從執行角度講,使用賦值 null 的操作優化內存回收是建立在對字節碼執行引擎概念模型基礎上的,但是概念模型與實際執行模型可能完全不同。在使用解釋器執行時,通常離概念模型還比較接近,但經過JIT 編譯後,纔是虛擬機執行代碼的主要方式,賦 null 值在JIT編譯優化之後會被完全消除,這時候賦 null 值是完全沒有意義的。(其實,上面代碼一在 JIT 編譯後,System.gc() 執行時就可以正確地回收掉內存,無需寫成代碼二的樣子)


另外,局部變量不像類變量一樣存在“準備階段”,所以一個局部變量定義了但沒有賦初始值是不能使用的




二、操作數棧

操作數棧和普通的棧(後入先出)一樣,只不過他是用來存放操作數以及操作結果的。Java 虛擬機的解釋執行引擎稱爲”基於棧的執行引擎“,這裏的棧就是指操作數棧。同局部變量表一樣,操作數棧的最大深度也在編譯的時候寫入到Code屬性的max_stacks數據項中,32位數據類型所佔的棧容量爲1,64位數據類型所佔的棧容量爲2,在方法執行的任何時候,操作數棧的深度,都不會超過在max_stacks數據項中設定的最大值。

在做算術運算又或者是調用其他的方法進行參數傳遞的時候是通過操作數棧進行的。

在概念模型中,兩個棧幀作爲虛擬機棧的元素,是相互獨立的。但是大多數虛擬機的實現都會進行優化,令兩個棧幀出現一部分重疊。令下面的部分操作數棧與上面的局部變量表重疊在一塊,這樣在方法調用的時候可以共用一部分數據,無須進行額外的參數複製傳遞。如圖



三、動態連接

每個棧幀都包含一個執行運行時常量池中該棧幀所屬方法的引用,持有這個引用是爲了支持方法調用過程中的動態連接(Dynamic Linking)。
Class 文件中存放了大量的符號引用,字節碼中的方法調用指令就是以常量池中指向方法的符號引用作爲參數。這些符號引用一部分會在類加載階段或第一次使用時轉化爲直接引用,這種轉化稱爲靜態解析。另一部分將在每一次運行期間轉化爲直接引用,這部分稱爲動態連接。


四、方法返回地址

當一個方法開始執行以後,只有兩種方法可以退出這個方法:
      1、當執行遇到任意一個方法返回的字節碼指令,可能會將返回值傳遞給上層的方法調用者,是否有返回值和返回值的類型將根據遇到何種方法返回指令來定,這種退出的方式稱爲正常完成出口,一般來說,調用者的PC計數器可以作爲返回地址。
      2、當執行遇到異常,並且這個異常在方法體內沒有得到處理,就會導致方法退出,此時是沒有返回值的,稱爲異常完成出口,返回地址要通過異常處理器表來確定。


當方法退出時,可能進行3個操作:
1、恢復上層方法的局部變量表和操作數棧。
2、把返回值壓入調用者調用者棧幀的操作數棧。
3、調整 PC 計數器的值以指向方法調用指令後面的一條指令。


五、附加信息

虛擬機規範允許具體的虛擬機實現增加一些規範裏沒有描述的信息到棧幀之中(比如與調試相關的信息),這部分的信息完全取決於具體的虛擬機實現。在實際開發中,一般會把動態連接,方法返回地址和其他附加信息全部歸爲一類,稱爲棧幀信息。








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