1.1 運(yùn)行時(shí)棧幀結(jié)構(gòu)棧幀(Stack Frame)是用于支持虛擬機(jī)進(jìn)行方法調(diào)用和方法執(zhí)行的數(shù)據(jù)結(jié)構(gòu),它是虛擬機(jī)運(yùn)行時(shí)數(shù)據(jù)區(qū)中的虛擬機(jī)棧(Virtual Machine Stack)的棧元素。棧幀存儲(chǔ)了方法的局部變量表、操作數(shù)棧、動(dòng)態(tài)連接和方法返回地址等信息。每一個(gè)方法從調(diào)用開始到執(zhí)行完成的過程,就對(duì)應(yīng)著一個(gè)棧幀在虛擬機(jī)里面從入棧到出棧的過程。 每一個(gè)棧幀都包括了局部變量表、操作數(shù)棧、方法返回地址和一些額外的附加信息。在編譯程序代碼的時(shí)候,棧幀中需要多大的局部變量表、多深的操作數(shù)棧都已經(jīng)完全確定了,并且寫入到方法表的Code屬性之中,因此一個(gè)棧幀需要分配多少內(nèi)存,不會(huì)受到程序運(yùn)行期變量數(shù)據(jù)的影響,而僅僅取決于具體的虛擬機(jī)實(shí)現(xiàn)。 一個(gè)線程中的方法調(diào)用鏈可能會(huì)很長,很多方法都同時(shí)處于執(zhí)行狀態(tài)。對(duì)于執(zhí)行引擎來講,活動(dòng)線程中,只有棧頂?shù)臈怯行У模Q為當(dāng)前棧幀(Current Stack Frame),這個(gè)棧幀所關(guān)聯(lián)的方法稱為當(dāng)前方法(Current Method)。執(zhí)行引擎所運(yùn)行的所有字節(jié)碼指令都只針對(duì)當(dāng)前棧幀進(jìn)行操作,棧幀的概念結(jié)構(gòu)如下圖所示: 接下來我們將詳細(xì)了解一下棧幀中的局部變量表、操作數(shù)棧、動(dòng)態(tài)連接、方法返回地址等各個(gè)部分的作用和數(shù)據(jù)結(jié)構(gòu)。 1.1.1局部變量表局部變量表是一組變量值存儲(chǔ)空間,用于存放方法參數(shù)和方法內(nèi)部定義的局部變量。在Java程序被編譯為Class文件時(shí),就在方法的Code屬性的max_locals數(shù)據(jù)項(xiàng)中確定了該方法所需要分配的最大局部變量表的容量。 局部變量表的容量以變量槽(Variable Slot,下稱Slot)為最小單位,虛擬機(jī)規(guī)范中并沒有明確指明一個(gè)Slot應(yīng)用占用空間大小,只是很有“導(dǎo)向性”地說明每個(gè)Slot都應(yīng)該能存放一個(gè)boolean、byte、char、short、int、float、reference或returnAddress類型的數(shù)據(jù),這種描述與明確指出“”有一些差別,它允許Slot的長度隨著處理器、操作系統(tǒng)或虛擬機(jī)的不同而發(fā)生變化。不過無論如何,即使在64位虛擬機(jī)中使用了64位長度的內(nèi)存空間來實(shí)現(xiàn)一個(gè)Slot,虛擬機(jī)仍要使用對(duì)齊和補(bǔ)白的手段讓Slot在外觀上看起來與32位虛擬機(jī)中和一致。 既然前面提到了數(shù)據(jù)類型,在此順便說一下,一個(gè)Slot可以存放一個(gè)32位以內(nèi)的數(shù)據(jù)類型,Java中占用32位以內(nèi)的數(shù)據(jù)類型有boolean、byte、char、short、int、float、reference和returnAddress八種類型。前面六種不需要多解釋,大家都認(rèn)識(shí),而后面的reference是對(duì)象的引用。虛擬機(jī)規(guī)范既沒有說明它的長度,也沒有明確指出這個(gè)引用應(yīng)有怎樣的結(jié)構(gòu),但是一般來說,虛擬機(jī)實(shí)現(xiàn)至少都應(yīng)當(dāng)能從此引用中直接或間接地查找到對(duì)象在Java堆中的起始地址索引和方法區(qū)中的對(duì)象類型數(shù)據(jù)。而returnAddress是為字節(jié)碼指令jsr、jsr_w和ret服務(wù)的,它指向了一條字節(jié)碼指令的地址。 對(duì)于64位的數(shù)據(jù)類型,虛擬機(jī)會(huì)以高位在前的方式為其分配兩個(gè)連續(xù)的Slot空間。Java語言中明確規(guī)定的64位的數(shù)據(jù)類型只有l(wèi)ong和double兩種(reference類型則可能是32位也可能是64位)。值得一提的是,這里把long和double數(shù)據(jù)類型讀寫分割為兩次32讀寫的做法類似,讀者閱讀到Java內(nèi)存模型時(shí)可以對(duì)比一下。不過,由于局部變量表建立在線程的堆棧上,是線程私有的數(shù)據(jù),無論讀寫兩個(gè)連續(xù)的Slot是否是原子操作,都不會(huì)引起數(shù)據(jù)安全問題。 虛擬機(jī)通過索引定位的方式使用局部變量表,索引值的范圍是從0開始到局部變量表最大的Slot數(shù)量。如果是32位數(shù)據(jù)類型的變量,索引n就代表了使用第n個(gè)Slot,如果是64位數(shù)據(jù)類型的變量,則說明要使用第n和第n+1兩個(gè)Slot。 在方法執(zhí)行時(shí),虛擬機(jī)是使用局部變量表完成參數(shù)值到參數(shù)變量列表的傳遞過程的,如果是實(shí)例方法(非static的方法),那么局部變量表中第0位索引的Slot默認(rèn)是用于傳遞方法所屬對(duì)象實(shí)例的引用,在方法中可以通過關(guān)鍵字“this”來訪問這個(gè)隱含的參數(shù)。其余參數(shù)則按照參數(shù)表的順序來排列,占用從1開始的局部變量Slot,參數(shù)表分配完畢后,再根據(jù)方法體內(nèi)部定義的變量順序和作用域分配其余的Slot。 局部變量表中的Slot是可重用的,方法體中定義的變量,其作用域并不一定會(huì)覆蓋整個(gè)方法體,如果當(dāng)前字節(jié)碼PC計(jì)數(shù)器的值已經(jīng)超出了某個(gè)變量的作用域,那么這個(gè)變量對(duì)應(yīng)的Slot就可以交給其他變量使用。這樣的設(shè)計(jì)不僅僅是為了節(jié)省??臻g,在某些情況下Slot的復(fù)用會(huì)直接影響到系統(tǒng)的垃圾收集行為,請看如下代碼清單的演示: publicclass LocalVariableTableTest { publicstaticvoid main(String[]args) { byte[] placeholder = newbyte[64 * 1024 * 1024]; System.gc(); } } 如上代碼清單中的代碼很簡單,向內(nèi)存填充了64MB的數(shù)據(jù),然后通知虛擬機(jī)進(jìn)行垃圾收集。我們在虛擬機(jī)運(yùn)行參數(shù)中加上“-verbose:gc”來看看垃圾收集的過程,發(fā)現(xiàn)在System.gc()運(yùn)行后并沒有回收掉這64MB的內(nèi)存,下面是運(yùn)行的結(jié)果: [Full GC 66933K->66064K(126720K), 0.0097959 secs] 沒有回收掉placeholder所占的內(nèi)存能說得過去,因?yàn)樵趫?zhí)行System.gc()時(shí),變量placeholder還處于作用域內(nèi),虛擬機(jī)自然不敢回收掉placeholder的內(nèi)存。我們把代碼修改一下,變成如下清單中的樣子: publicclass LocalVariableTableTest1 { publicstaticvoid main(String[]args) { { byte[] placeholder = newbyte[64 * 1024 * 1024]; } System.gc(); } } 加入了花括號(hào)之后,placeholder的作用域被限制在花括號(hào)之內(nèi),從代碼邏輯上講,在執(zhí)行System.gc()的時(shí)候,placeholder已經(jīng)不可能再被訪問了,但執(zhí)行一下這段程序,會(huì)發(fā)現(xiàn)運(yùn)行結(jié)果如下,還是有64MB的內(nèi)存沒有被回收掉,這又是為什么呢? [Full GC 66933K->66064K(126720K), 0.0070839 secs] 在解釋為什么之前,我們先對(duì)這段代碼進(jìn)行第二次修改,在調(diào)用System.gc()之前加入一行代碼“int a = 0;”,變成如下代碼清單的樣子: publicstaticvoid main(String[]args) { { byte[] placeholder = newbyte[64 * 1024 * 1024]; } int a = 0; System.gc(); } 這個(gè)修改看起來很莫名其妙,但運(yùn)行一下程序,卻發(fā)現(xiàn)這次內(nèi)存真的被正確回收了(這樣的場景并不多見): [Full GC 66933K->528K(126720K), 0.0067665 secs] 上面三段代碼清單中,placeholder能否被回收的根本原因就是:局部變量表中的Slot是否還存有關(guān)于placeholder數(shù)組對(duì)象的引用。第一次修改中,代碼雖然已經(jīng)離開了placeholder的作用域,但在此之后,沒有任何對(duì)局部變量表的讀寫操作,placeholder原本所占用的Slot還沒有被其他變量所復(fù)用,所以作為GC Roots一部分的局部變量表仍然保持著對(duì)它的關(guān)聯(lián)。這種關(guān)聯(lián)沒有被及時(shí)打斷,在絕大部分情況下影響都很輕微。但如果遇到一個(gè)方法,其后面的代碼有一些耗時(shí)很長的操作,而前面又定義了占用了大量內(nèi)存、實(shí)際上已經(jīng)不會(huì)再被使用的變量,手動(dòng)將其設(shè)置為null值(用來代替“int a = 0;”,把變量對(duì)應(yīng)的局部變量表Slot清空)就不是一個(gè)毫無意義的操作,這種操作可以作為一種在極特殊情形(對(duì)象占用內(nèi)存大、此方法的棧幀長時(shí)間不能被回收、方法調(diào)用次數(shù)達(dá)不到JIT的編譯條件)下的“奇技”來使用。但不應(yīng)當(dāng)對(duì)賦null值操作有過多的依賴,也沒有必要把它當(dāng)做一個(gè)普遍的編碼方法來推廣。以恰當(dāng)?shù)淖兞孔饔糜騺砜刂谱兞炕厥諘r(shí)間才是最優(yōu)雅的解決方法。 另外,賦null值的操作在經(jīng)過虛擬機(jī)JIT編譯器優(yōu)化之后會(huì)被消除掉,這時(shí)候?qū)⒆兞吭O(shè)置為null實(shí)際上是沒有意義的。字節(jié)碼被編譯為本地代碼后,對(duì)GC Roots的枚舉也與解釋執(zhí)行時(shí)期有所差別。 關(guān)于局部變量表,還有一點(diǎn)可能會(huì)對(duì)實(shí)際開發(fā)產(chǎn)生影響,就是局部變量不像前面介紹的類變量那樣存在“準(zhǔn)備階段”。通過前一章的講解,我們已經(jīng)知道變量有兩次賦初始值的過程,一次在準(zhǔn)備階段,賦予系統(tǒng)初始值;另外一次在初始化階段,賦予程序員定義的初始值。因此即使在初始化階段程序員沒有為類變量賦值也沒有關(guān)系,類變量仍然具有一個(gè)確定的初始值。但局部變量就不一樣了,如果一個(gè)局部變量定義了但沒有賦初始值是不能使用的。所以不要認(rèn)為Java中任何情況下都存在諸如整型變量默認(rèn)為0、布爾型變量默認(rèn)為false之類的默認(rèn)值。如下代碼清單所示: publicstaticvoid main(String[]args) { int a; System.out.println(a); } 這段代碼其實(shí)并不能運(yùn)行,所幸編譯器能在編譯期間檢查到并提示這一點(diǎn)。即便編譯器能通過手動(dòng)生成字節(jié)碼的方式制造出下面的代碼效果,字節(jié)碼檢驗(yàn)的時(shí)候也會(huì)被虛擬機(jī)發(fā)現(xiàn),從而導(dǎo)致類加載失敗。 1.1.2操作數(shù)棧操作數(shù)棧也常被稱為操作棧,它是一個(gè)后入先出(Last In First Out, LIFO)棧。同局部變量表一樣,操作數(shù)棧的最大深度也在編譯的時(shí)候被寫入到Code屬性的max_stacks數(shù)據(jù)項(xiàng)之中。操作數(shù)棧的每一個(gè)元素可以是任意的Java數(shù)據(jù)類型,包括long和double。32位數(shù)據(jù)類型所占的棧容量為1,64位數(shù)據(jù)類型所占的棧容量為2。在方法執(zhí)行的任何時(shí)候,操作數(shù)棧的深度都不會(huì)超過在max_stacks數(shù)據(jù)項(xiàng)中設(shè)定的最大值。 當(dāng)一個(gè)方法剛剛開始執(zhí)行的時(shí)候,這個(gè)方法的操作數(shù)棧是空的,在方法的執(zhí)行過程中,會(huì)有各種字節(jié)碼指令向操作數(shù)棧中寫入和提取內(nèi)容,也就是入棧出棧操作。例如,在做算術(shù)運(yùn)算的時(shí)候是通過操作數(shù)棧來進(jìn)行的,又或者在調(diào)用其他方法的時(shí)候是通過操作數(shù)棧來進(jìn)行參數(shù)傳遞的。 舉個(gè)例子,整數(shù)加法的字節(jié)碼指令iadd在運(yùn)行的時(shí)候要求操作數(shù)棧中最接近棧頂?shù)膬蓚€(gè)元素已經(jīng)存入了兩個(gè)int型的數(shù)值,當(dāng)執(zhí)行這個(gè)指令時(shí),會(huì)將這兩個(gè)int值和并相加,然后將相加的結(jié)果入棧。 操作數(shù)棧中元素的數(shù)據(jù)類型必須與字節(jié)碼指令的序列嚴(yán)格匹配,在編譯程序代碼的時(shí)候,編譯器要嚴(yán)格保證這一點(diǎn),在類校驗(yàn)階段的數(shù)據(jù)流分析中還要再次驗(yàn)證這一點(diǎn)。再以上面的iadd指令為例,這個(gè)指令用于整型數(shù)加法,它在執(zhí)行時(shí),最接近棧頂?shù)膬蓚€(gè)元素的數(shù)據(jù)類型必須為int型,不能出現(xiàn)一個(gè)long和一個(gè)float使用iadd命令相加的情況。 另外,在概念模型中,兩個(gè)棧幀作為虛擬機(jī)棧的元素,相互之間是完全獨(dú)立的。但是大多數(shù)虛擬機(jī)的實(shí)現(xiàn)里都會(huì)做一些優(yōu)化處理,令兩個(gè)棧幀出現(xiàn)一部分重疊。讓下面棧幀的部分操作數(shù)棧與上面棧幀的部分局部變量表重疊在一起,這樣在進(jìn)行方法調(diào)用時(shí)就可以共用一部分?jǐn)?shù)據(jù),而無須進(jìn)行額外的參數(shù)復(fù)制傳遞了,重疊的過程如下圖所示:
Java虛擬機(jī)的解釋執(zhí)行引擎稱為“基于棧的執(zhí)行引擎”,其中所指的“棧”就是操作數(shù)棧。 1.1.1動(dòng)態(tài)連接每個(gè)棧幀都包含一個(gè)指向運(yùn)行時(shí)常量池中該棧幀所屬方法的引用,持有這個(gè)引用是為了支持方法調(diào)用過程中的動(dòng)態(tài)連接。我們知道Class文件的常量池有存有大量的符號(hào)引用,字節(jié)碼中的方法調(diào)用指令就以常量池中指向方法的符號(hào)引用為參數(shù)。這些符號(hào)引用一部分會(huì)在類加載階段或第一次使用的時(shí)候轉(zhuǎn)化為直接引用,這種轉(zhuǎn)化稱為靜態(tài)解析。另外一部分將在每一次的運(yùn)行期間轉(zhuǎn)化為直接引用,這部分稱為動(dòng)態(tài)連接。 1.1.2方法返回地址當(dāng)一個(gè)方法被執(zhí)行后,有兩種方式退出這個(gè)方法。第一種方式是執(zhí)行引擎遇到任意一個(gè)方法返回的字節(jié)碼指令,這時(shí)候可能會(huì)有返回值傳遞給上層的方法調(diào)用者(調(diào)用當(dāng)前方法的方法稱為調(diào)用者),是否有返回值和返回值的類型將根據(jù)遇到何種方法返回指令來決定,這種退出方法的方式稱為正常完成出口(Normal Method Invocation Completion)。 另外一種退出方式是,在方法執(zhí)行過程中遇到了異常,并且這個(gè)異常沒有在方法體內(nèi)得到處理,無論是Java虛擬機(jī)內(nèi)部產(chǎn)生的異常,還是代碼中使用athrow字節(jié)碼指令產(chǎn)生的異常,只要在本方法的異常表中沒有搜索到匹配的異常處理器,就會(huì)導(dǎo)致方法退出,這種退出方法的方式稱為異常完成出口(Abrupt Method Invocation Completion)。一個(gè)方法使用異常完成出口的方式退出,是不會(huì)給它的上層調(diào)用者產(chǎn)生任何返回值的。 無論采用何種退出方式,在方法退出之后,都需要返回到方法被調(diào)用的位置,程序才能繼續(xù)執(zhí)行,方法返回時(shí)可能需要在棧幀中保存一些信息,用來幫助恢復(fù)它的上層方法的執(zhí)行狀態(tài)。一般來說,方法正常退出時(shí),調(diào)用者的PC計(jì)數(shù)器的值就可以作為返回地址,棧幀中很可能會(huì)保存這個(gè)計(jì)數(shù)器值。而方法異常退出時(shí),返回地址是要通過異常處理器來確定的,棧幀中一般不會(huì)保存這部分信息。 方法退出的過程實(shí)際上等同于把當(dāng)前棧幀出棧,因此退出時(shí)可能執(zhí)行的操作有:恢復(fù)上層方法的局部變量表和操作數(shù)棧,把返回值(如果有的話)壓入調(diào)用者棧幀的操作數(shù)棧中,調(diào)整PC計(jì)數(shù)器的值以指向方法調(diào)用指令后面的一條指令等。 1.1.3附加信息虛擬機(jī)規(guī)范允許具體的虛擬機(jī)實(shí)現(xiàn)增加一些規(guī)范里沒有描述的信息到棧幀之中,例如與調(diào)試相關(guān)的信息,這部分信息完全取決于具體的虛擬機(jī)實(shí)現(xiàn),這里不再詳述。在實(shí)際開發(fā)中,一般會(huì)把動(dòng)態(tài)連接、方法返回地址與其他附加信息全部歸為一類,稱為棧幀信息。 |
|
|