每個人都知道,各種各樣的動畫視頻,都是由一幀一幀圖片連續切換結果的結果而產生的,其實虛擬機的運行和動畫也類似,每個在虛擬機中運行的程序也是由許多的幀的切換產生的結果,只是這些幀里面存放的是方法的局部變量,操作數棧,動態鏈接,方法返回地址和一些額外的附加信息組成,在虛擬機中包含這些信息的幀稱為“棧幀”,每個方法的執行,在虛擬機中都是對應的棧幀在虛擬機棧中的入棧到出棧的過程。其中比較重要的一點時,如果虛擬機中同時有多個線程在執行,那么各個線程的棧幀都是相互獨立,互不侵犯的,所以這也實現了局部變量在多線程的環境下也是線程安全的。
一個方法的調用鏈可能會很長,於是當調用一個方法時,可能會有很多的方法都處於執行狀態,但是對於執行引擎來講,至於位於虛擬機棧頂的棧幀才是有效的,這個棧幀被稱為當前棧,這個棧幀所關聯的方法稱為當前方法,執行引擎的所有指令都是針對當前棧幀進行操作的。
前面已經提到一個棧幀包括局部變量表,操作數棧,動態鏈接,方法返回地址和一些額外的附加信息組成,接下來對各個部分做一個簡單的介紹。
(一)局部變量表
通過名字可以看出這個里面放的都是局部變量,例如方法參數,方法內部定義的局部變量。一般情況下,在Java程序被編譯為class文件的時候這個表的容量最大值就已經確定下來,是存在方法的Code屬性的Max_locals數據項中
在局部變量表中Slot時最小的存儲單位,虛擬機規范並沒有明確指明一個Slot為多少位,Slot具體的大小也會隨着操作系統和虛擬機的不同而不同,一般情況下可以當成時32位來看待,但是規定了一個Slot必須可以存放boolean,byte,char,int,float,reference(可能32位也可能時64位),returnAddress.而對於在虛擬機規范中被明確定義位64位的Long和Double而言,需要用兩個連續的Slot來存放,由於時連個Slot來存儲,所以在對Long和Double進行操作的時候就會存在原子性的問題,不過虛擬機會對它作出原子性保證(因為每個線程之間的棧幀是相互獨立的,所以也不會由線程安全的問題)。
既然局部變量中存放了很多的局部變量,那么怎么來訪問每個變量了?虛擬機規范中指出,虛擬機會利用索引編號的遞增來對局部變量表中定義的變量進行依次訪問(從0開始),而對於實例方法(非static方法),其局部變量表的第0個索引就是我們熟悉的this,這也是為什么在實例方法中我們可以使用this.name....的原因。
下面來談談Slot對虛擬機的垃圾回收的影響。由於在一個方法中,某個方法內的局部變量的作用范圍也不一定可以覆蓋整個方法,這就可能導致Slot資源的浪費,如果這個Slot對應的資源足夠的大,那么Slot對資源的浪費也就可能會影響到整個虛擬機棧的使用,為了解決這個問題,虛擬機規范中規定了Slot的可重用性,即當一個方法中的某個局部變量超出了變量的有效范圍時,那么那個變量的Slot可以被另外一個局部變量來使用。被重用的Slot便失去了和原來堆中實例的聯系,這樣堆中的實例便可以被垃圾回收器回收,當然一般情況下這些輔助的操作可能對系統性能的提升由很小的影響,但是,如果在那個局部變量“過期”之后還有很多的代碼要執行,或者說后面由比較耗時的操作,而且在變量過期前,已經消耗了比較多的系統資源,那么這個輔助動作可能就非常有用了。
下面將通過三個例子來說明重用Slot對垃圾回收帶來的好處:
示例代碼:

public class SlotTest { /** * 主要驗證重復利用Slot對於垃圾回收的幫助 ×(1)運行參數:-verbose:gc -XX:+PrintGCDetails * (2)64M的對象大於了目前年輕代的空間,根據大對象直接進入老年代的原則,在觀察結果的時候需要關注ParOldGen * */ public static int M = 1024 << 10; public static void main(String[] args) { new SlotTest().test2(); } /* * replace 在執行gc操作的時候還沒有超過它的作用域,也就是堆中還有實例和它直接關聯所以不會被回收掉 * * [GC [PSYoungGen: 614K->352K(17856K)] 66150K->65888K(124224K), * 0.0024710 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [Full GC * (System) [PSYoungGen: 352K->0K(17856K)] [ParOldGen: * 65536K->65759K(106368K)] 65888K->65759K(124224K) [PSPermGen: * 2403K->2401K(21248K)], 0.0102720 secs] [Times: user=0.02 sys=0.00, * real=0.01 secs] */ public void test1() { // 64M byte[] replace = new byte[M << 6]; System.gc(); } /* * 在執行gc時,雖然replace已經過期,但是由於它的Slot中仍然存有相關的局部變量信息,所以gc 還是不可以 對64M的內存進行回收 * * [GC [PSYoungGen: 614K->288K(17856K)] 66150K->65824K(124224K), * 0.0019600 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] [Full GC * (System) [PSYoungGen: 288K->0K(17856K)] [ParOldGen: * 65536K->65758K(106368K)] 65824K->65758K(124224K) [PSPermGen: * 2403K->2401K(21248K)], 0.0139210 secs] [Times: user=0.02 sys=0.00, * real=0.01 secs] */ public void test2() { { byte[] replace = new byte[M << 6]; } System.gc(); } /*在執行gc之前,由於a復用了replace 的Slot,所以此時可以認為replace在堆中的實例沒有相關的引用,因此在gc的時候會將它回收 * [GC [PSYoungGen: 614K->368K(17856K)] 66150K->65904K(124224K), * 0.0019430 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [Full GC * (System) [PSYoungGen: 368K->0K(17856K)] [ParOldGen: * 65536K->223K(106368K)] 65904K->223K(124224K) [PSPermGen: * 2403K->2401K(21248K)], 0.0107030 secs] [Times: user=0.01 sys=0.01, * real=0.01 secs] */ public void test3() { { byte[] replace = new byte[M << 6]; } int a = 0; System.gc(); } }
對於上面代碼中的test3(),也可以用replace=null來達到同樣的效果。但是由於賦null值的操作在經過虛擬機JIT編譯優化之后就會被消除掉,所以在這種情況下設置null值是沒有意義的,其實就是test3()中的做法也是在特殊的情況下才會考慮的做法(后續的方法執行比較耗資源和時間,且前面的操作已經消耗了過多的資源),一般情況下只需要正確的保證每個局部變量有正確的變量作用域就可以了
最后要說明的是,由於局部變量不像實例變量或類變量那樣會在准備階段或者或者初始化階段對其進行賦值,所以局部變量在沒有賦值的情況下是不可以使用的,如果出現下面的情況,那么編譯的時候就會提示“局部變量沒有賦值.
public void test4(){ int a; System.out.println(a); }
(二)操作數棧:
首先根據名稱可以看出操作數棧是一個基本的棧來實現數據結構,那么它自然也遵守棧的后入先出的原則.其次,它里面主要存放的是一些算數運算用到的參數也可能是中間結果,也可能是在調用其他方法時需要用到的參數,通過這點可以看出,方法剛剛開始執行的時候,這個里面是空的.最后 要說明的是操作數棧中可以存放任意的Java數據類型,包括long和double,且32位的數據類型占一個棧空間,64位的數據類型占2個棧空間.
(三)動態連接:
在說明什么是動態連接之前先看看方法的大概調用過程,首先在虛擬機運行的時候,運行時常量池會保存大量的符號引用,這些符號引用可以看成是每個方法的間接引用,如果代表棧幀A的方法想調用代表棧幀B的方法,那么這個虛擬機的方法調用指令就會以B方法的符號引用作為參數,但是因為符號引用並不是直接指向代表B方法的內存位置,所以在調用之前還必須要將符號引用轉換為直接引用,然后通過直接引用才可以訪問到真正的方法,這時候就有一點需要注意,如果符號引用是在類加載階段或者第一次使用的時候轉化為直接應用,那么這種轉換成為靜態解析,如果是在運行期間轉換為直接引用,那么這種轉換就成為動態連接。
(四)方法的返回地址
方法的返回分為兩種情況,一種是正常退出,退出后會根據方法的定義來決定是否要傳返回值給上層的調用者,一種是異常導致的方法結束,這種情況是不會傳返回值給上層的調用方法.
不過無論是那種方式的方法結束,在退出當前方法時都會跳轉到當前方法被調用的位置,如果方法是正常退出的,則調用者的PC計數器的值就可以作為返回地址,如果是因為異常退出的,則是需要通過異常處理表來確定.
在方法的的一次調用就對應着棧幀在虛擬機棧中的一次入棧出棧操作,因此方法退出時可能做的事情包括,恢復上層方法的局部變量表以及操作數棧,如果有返回值的話,就把返回值壓入到調用者棧幀的操作數棧中,還會把PC計數器的值調整為方法調用入口的下一條指令。