棧幀(Stack Frame)是用於支持虛擬機進行方法調用和方法執行的數據結構,它是虛擬機運行時數據區中的虛擬機棧(Virtual Machine Stack)的棧元素。棧幀存儲了方法的局部變量表、操做數棧、動態鏈接和方法返回地址等信息。每個方法從調用開始至執行完成的過程,都對應着一個棧幀在虛擬機棧裏面從入棧到出棧的過程。每個棧幀都包括了局部變量表、操做數棧、動態鏈接、方法返回地址和一些額外的附加信息。在編譯程序代碼的時候,棧幀中須要多大的局部變量表,多深的操做數棧都已經徹底肯定了,而且寫入到方法表的Code屬性之中,所以一個棧幀須要分配多少內存,不會受到程序運行期變量數據的影響,而僅僅取決於具體的虛擬機實現。程序員
一個線程中的方法調用鏈可能會很長,不少方法都同時處於執行狀態。對於執行引擎來講,在活動線程中,只有位於棧頂的棧幀纔是有效的,稱爲當前棧幀(Current Stack Frame),與這個棧幀相關聯的方法稱爲當前方法(Current Method)。執行引擎運行的全部字節碼指令都只針對當前棧幀進行操做,在概念模型上,典型的棧幀結構如圖所示。數組
局部變量表(Local Variable Table)是一組變量值存儲空間,用於存放方法參數和方法內部定義的局部變量。在Java程序編譯爲Class文件時,就在方法的Code屬性的max_locals數據項中肯定了該方法所須要分配的局部變量表的最大容量。安全
局部變量表的容量以變量槽(Variable Slot,下稱Slot)爲最小單位,虛擬機規範中並無明確指明一個Slot應占用的內存空間大小,只是頗有導向性地說到每一個Slot都應該能存放一個boolean、byte、char、short、int、float、reference或returnAddress類型的數據,這8種數據類型,均可以使用32位或更小的物理內存來存放,但這種描述與明確指出「每一個Slot佔用32位長度的內存空間」是有一些差異的,它容許Slot的長度能夠隨着處理器、操做系統或虛擬機的不一樣而發生變化。只要保證即便在64位虛擬機中使用了64位的物理內存空間去實現一個Slot,虛擬機仍要使用對齊和補白的手段讓Slot在外觀上看起來與32位虛擬機中的一致。數據結構
一個Slot能夠存放一個32位之內的數據類型,Java中佔用32位之內的數據類型有boolean、byte、char、short、int、float、reference(Java虛擬機規範中沒有明確規定reference類型的長度,它的長度與實際使用32仍是64位虛擬機有關,若是是64位虛擬機,還與是否開啓某些對象指針壓縮的優化有關)和returnAddress 8種類型。第7種reference類型表示對一個對象實例的引用,虛擬機規範既沒有說明它的長度,也沒有明確指出這種引用應有怎樣的結構。但通常來講,虛擬機實現至少都應當能經過這個引用作到兩點,一是今後引用中直接或間接地查找到對象在Java堆中的數據存放的起始地址索引,二是此引用中直接或間接地查找到對象所屬數據類型在方法區中的存儲的類型信息,不然沒法實現Java語言規範中定義的語法約束。並非全部語言的對象引用都能知足這兩點,例如C++語言,默認狀況下(不開啓RTTI支持的狀況),就只能知足第一點,而不知足第二點。這也是爲什麼C++中提供Java語言裏很常見的反射的根本緣由。第8種即returnAddress類型目前已經不多見了,它是爲字節碼指令jsr、jsr_w和ret服務的,指向了一條字節碼指令的地址,很古老的Java虛擬機曾經使用這幾條指令來實現異常處理,如今已經由異常表代替。優化
對於64位的數據類型,虛擬機會以高位對齊的方式爲其分配兩個連續的Slot空間。Java語言中明確的(reference類型則多是32位也多是64位)64位的數據類型只有long和double兩種。值得一提的是,這裏把long和double數據類型分割存儲的作法與「long和double的非原子性協定」中把一次long和double數據類型讀寫分割爲兩次32位讀寫的作法有些相似。不過,因爲局部變量表創建在線程的堆棧上,是線程私有的數據,不管讀寫兩個連續的Slot是否爲原子操做,都不會引發數據安全問題。this
虛擬機經過索引定位的方式使用局部變量表,索引值的範圍是從0開始至局部變量表最大的Slot數量。若是訪問的是32位數據類型的變量,索引n就表明了使用第n個Slot,若是是64位數據類型的變量,則說明會同時使用n和n+1兩個Slot。對於兩個相鄰的共同存放一個64位數據的兩個Slot,不容許採用任何方式單獨訪問其中的某一個,Java虛擬機規範中明確要求了若是遇到進行這種操做的字節碼序列,虛擬機應該在類加載的校驗階段拋出異常。spa
在方法執行時,虛擬機是使用局部變量表完成參數值到參數變量列表的傳遞過程的,若是執行的是實例方法(非static的方法),那局部變量表中第0位索引的Slot默認是用於傳遞方法所屬對象實例的引用,在方法中能夠經過關鍵字「this」來訪問到這個隱含的參數。其他參數則按照參數表順序排列,佔用從1開始的局部變量Slot,參數表分配完畢後,再根據方法體內部定義的變量順序和做用域分配其他的Slot。操作系統
爲了儘量節省棧幀空間,局部變量表中的Slot是能夠重用的,方法體中定義的變量,其做用域並不必定會覆蓋整個方法體,若是當前字節碼PC計數器的值已經超出了某個變量的做用域,那這個變量對應的Slot就能夠交給其餘變量使用。不過,這樣的設計除了節省棧幀空間之外,還會伴隨一些額外的反作用,例如,在某些狀況下,Slot的複用會直接影響到系統的垃圾收集行爲線程
public static void main(String[]args)(){ { byte[] placeholder=new byte[64*1024*1024]; } int a=0; System.gc(); } 運行一下程序,卻發現此次內存真的被正確回收了。 [GC 66401K->65778K(125632K),0.0035471 secs] [Full GC 65778K->218K(125632K),0.0140596 secs]
placeholder可否被回收的根本緣由是:局部變量表中的Slot是否還存有關於placeholder數組對象的引用。代碼雖然已經離開了placeholder的做用域,但在此以後,沒有任何對局部變量表的讀寫操做(即沒有int a=0這段代碼),placeholder本來所佔用的Slot尚未被其餘變量所複用,因此做爲GC Roots一部分的局部變量表仍然保持着對它的關聯。這種關聯沒有被及時打斷,在絕大部分狀況下影響都很輕微。但若是遇到一個方法,其後面的代碼有一些耗時很長的操做,而前面又定義了佔用了大量內存、實際上已經不會再使用的變量,手動將其設置爲null值(用來代替那句int a=0,把變量對應的局部變量表Slot清空)便不見得是一個絕對無心義的操做,這種操做能夠做爲一種在極特殊情形(對象佔用內存大、此方法的棧幀長時間不能被回收、方法調用次數達不到JIT的編譯條件)下的「奇技」來使用。設計
關於局部變量表,還有一點可能會對實際開發產生影響,就是局部變量不像前面介紹的類變量那樣存在「準備階段」。咱們已經知道類變量有兩次賦初始值的過程,一次在準備階段,賦予系統初始值;另一次在初始化階段,賦予程序員定義的初始值。所以,即便在初始化階段程序員沒有爲類變量賦值也沒有關係,類變量仍然具備一個肯定的初始值。但局部變量就不同,若是一個局部變量定義了但沒有賦初始值是不能使用的,不要認爲Java中任何狀況下都存在諸如整型變量默認爲0,布爾型變量默認爲false等這樣的默認值。這段代碼其實並不能運行,還好編譯器能在編譯期間就檢查到並提示這一點,即使編譯能經過或者手動生成字節碼的方式製造出下面代碼的效果,字節碼校驗的時候也會被虛擬機發現而致使類加載失敗。
public static void main(String[]args){ int a; System.out.println(a); }
操做數棧(Operand Stack)也常稱爲操做棧,它是一個後入先出(Last In First Out,LIFO)棧。同局部變量表同樣,操做數棧的最大深度也在編譯的時候寫入到Code屬性的max_stacks數據項中。操做數棧的每個元素能夠是任意的Java數據類型,包括long和double。32位數據類型所佔的棧容量爲1,64位數據類型所佔的棧容量爲2。在方法執行的任什麼時候候,操做數棧的深度都不會超過在max_stacks數據項中設定的最大值。
當一個方法剛剛開始執行的時候,這個方法的操做數棧是空的,在方法的執行過程當中,會有各類字節碼指令往操做數棧中寫入和提取內容,也就是出棧/入棧操做。例如,在作算術運算的時候是經過操做數棧來進行的,又或者在調用其餘方法的時候是經過操做數棧來進行參數傳遞的。舉個例子,整數加法的字節碼指令iadd在運行的時候操做數棧中最接近棧頂的兩個元素已經存入了兩個int型的數值,當執行這個指令時,會將這兩個int值出棧並相加,而後將相加的結果入棧。
操做數棧中元素的數據類型必須與字節碼指令的序列嚴格匹配,在編譯程序代碼的時候,編譯器要嚴格保證這一點,在類校驗階段的數據流分析中還要再次驗證這一點。再以上面的iadd指令爲例,這個指令用於整型數加法,它在執行時,最接近棧頂的兩個元素的數據類型必須爲int型,不能出現一個long和一個float使用iadd命令相加的狀況。另外,在概念模型中,兩個棧幀做爲虛擬機棧的元素,是徹底相互獨立的。但在大多虛擬機的實現裏都會作一些優化處理,令兩個棧幀出現一部分重疊。讓下面棧幀的部分操做數棧與上面棧幀的部分局部變量表重疊在一塊兒,這樣在進行方法調用時就能夠共用一部分數據,無須進行額外的參數複製傳遞,Java虛擬機的解釋執行引擎稱爲「基於棧的執行引擎」,其中所指的「棧」就是操做數棧。
每一個棧幀都包含一個指向運行時常量池中該棧幀所屬方法的引用,持有這個引用是爲了支持方法調用過程當中的動態鏈接(Dynamic Linking)。咱們知道Class文件的常量池中存有大量的符號引用,字節碼中的方法調用指令就以常量池中指向方法的符號引用做爲參數。這些符號引用一部分會在類加載階段或者第一次使用的時候就轉化爲直接引用,這種轉化稱爲靜態解析。另一部分將在每一次運行期間轉化爲直接引用,這部分稱爲動態鏈接。(靜態分派,動態分派)
當一個方法開始執行後,只有兩種方式能夠退出這個方法。第一種方式是執行引擎遇到任意一個方法返回的字節碼指令,這時候可能會有返回值傳遞給上層的方法調用者(調用當前方法的方法稱爲調用者),是否有返回值和返回值的類型將根據遇到何種方法返回指令來決定,這種退出方法的方式稱爲正常完成出口(Normal Method Invocation Completion)。
另一種退出方式是,在方法執行過程當中遇到了異常,而且這個異常沒有在方法體內獲得處理,不管是Java虛擬機內部產生的異常,仍是代碼中使用athrow字節碼指令產生的異常,只要在本方法的異常表中沒有搜索到匹配的異常處理器,就會致使方法退出,這種退出方法的方式稱爲異常完成出口(Abrupt Method Invocation Completion)。一個方法使用異常完成出口的方式退出,是不會給它的上層調用者產生任何返回值的。
不管採用何種退出方式,在方法退出以後,都須要返回到方法被調用的位置,程序才能繼續執行,方法返回時可能須要在棧幀中保存一些信息,用來幫助恢復它的上層方法的執行狀態。通常來講,方法正常退出時,調用者的PC計數器的值能夠做爲返回地址,棧幀中極可能會保存這個計數器值。而方法異常退出時,返回地址是要經過異常處理器表來肯定的,棧幀中通常不會保存這部分信息。
方法退出的過程實際上就等同於把當前棧幀出棧,所以退出時可能執行的操做有:恢復上層方法的局部變量表和操做數棧,把返回值(若是有的話)壓入調用者棧幀的操做數棧中,調整PC計數器的值以指向方法調用指令後面的一條指令等。
虛擬機規範容許具體的虛擬機實現增長一些規範裏沒有描述的信息到棧幀之中,例如與調試相關的信息,這部分信息徹底取決於具體的虛擬機實現。在實際開發中,通常會把動態鏈接、方法返回地址與其餘附加信息所有歸爲一類,稱爲棧幀信息。