一個Java類的生命周期概括來說需要經(jīng)過加載、驗證狈究、準備碗淌、解析以及初始化、使用及卸載的過程抖锥。這里不展開加載Class 的過程以及Class文件格式(后期會陸續(xù)探討)亿眠。在執(zhí)行過程中,JVM是如何把Class文件里的字節(jié)碼轉(zhuǎn)換成我們的虛擬機棧的操作指令磅废,以及整個虛擬機棧的內(nèi)部數(shù)據(jù)結(jié)構(gòu)是怎樣的纳像,這篇文章后續(xù)會詳細介紹,并且稍微擴展下JVM規(guī)范中的一些字節(jié)碼指令集拯勉。
其實這篇文章的主要目的還是為了引入后續(xù)要介紹的ASM框架的CoreApi 中的Method接口和組件來做一個鋪墊竟趾。我們知道,Class文件是編譯后的以8byte為單位存儲的二進制字節(jié)流宫峦,想要生成和解析一個Class文件岔帽,那么我們需要更好地了解在JVM中他是怎樣被解析和執(zhí)行的。
一导绷、字節(jié)碼執(zhí)行
方法調(diào)用在JVM中轉(zhuǎn)換成的是字節(jié)碼執(zhí)行犀勒,字節(jié)碼指令執(zhí)行的數(shù)據(jù)結(jié)構(gòu)就是棧幀(stack frame)。也就是在虛擬機棧中的棧元素妥曲。虛擬機會為每個方法分配一個棧幀贾费,因為虛擬機棧是LIFO(后進先出)的,所以當前線程正在活動的棧幀檐盟,也就是棧頂?shù)臈荆琂VM規(guī)范中稱之為“CurrentFrame”,這個當前棧幀對應(yīng)的方法就是“CurrentMethod”。字節(jié)碼的執(zhí)行操作遵堵,指的就是對當前棧幀數(shù)據(jù)結(jié)構(gòu)進行的操作。
棧幀的數(shù)據(jù)結(jié)構(gòu)主要分為四個部分:局部變量表怨规、操作數(shù)棧陌宿、動態(tài)鏈接以及方法返回地址(包括正常調(diào)用和異常調(diào)用的完成結(jié)果)。下面就一一介紹下這四種數(shù)據(jù)結(jié)構(gòu)波丰。
1壳坪、局部變量表(local variables)
當方法被調(diào)用時,參數(shù)會傳遞到從0開始的連續(xù)的局部變量表的索引位置上掰烟。棧幀中局部變量表的長度存儲在類或接口的二進制表示中爽蝴。閱讀Class文件會找到Code屬性沐批,所以我們能知道local variables的最大長度是在編譯期間決定的。一個局部變量表的占用了32位的存儲空間(一個存儲單位稱之為slot蝎亚,槽)九孩,所以可以存儲一個boolean、byte发框、char躺彬、short、float梅惯、int宪拥、refrence和returnAdress數(shù)據(jù),long和double需要2個連續(xù)的局部變量表來保存铣减,通過較小位置的索引來獲取她君。如果被調(diào)用的是實例方法,那么第0個位置存儲“this”關(guān)鍵字代表當前實例對象的引用葫哗。
2缔刹、操作數(shù)棧(operand stack)
操作數(shù)棧同局部變量表一樣,也是編譯期間就能決定了其存儲空間(最大的單位長度)魄梯,通過 Code屬性存儲在類或接口的字節(jié)流中桨螺。操作數(shù)棧也是個LIFO棧。
操作數(shù)棧是在JVM字節(jié)碼執(zhí)行一些指令(第二部分會介紹一些指令集)時創(chuàng)建的酿秸,主要是把局部變量表中的變量壓入操作數(shù)棧灭翔,在操作數(shù)棧中進行字節(jié)碼指令的操作,再將變量出操作數(shù)棧辣苏,結(jié)果入操作數(shù)棧肝箱。同局部變量表,除了long和double,其他類型數(shù)據(jù)都只占用一個棧的單位深度。
3稀蟋、動態(tài)鏈接
每個棧幀指向運行時常量池中該棧幀所屬的方法的引用煌张,也就是字節(jié)碼的發(fā)放調(diào)用的引用。動態(tài)鏈接就是將符號引用所表示的方法退客,轉(zhuǎn)換成方法的直接引用骏融。加載階段或第一次使用時轉(zhuǎn)化為直接引用的(將變量的訪問轉(zhuǎn)化為訪問這些變量的存儲結(jié)構(gòu)所在的運行時內(nèi)存位置)就叫做靜態(tài)解析。JVM的動態(tài)鏈接還支持運行期轉(zhuǎn)化為直接引用萌狂。也可以叫做Late Binding,晚期綁定档玻。
4、方法返回地址
方法正常退出會把返回值壓入調(diào)用者的棧幀的操作數(shù)棧茫藏,PC計數(shù)器的值就會調(diào)整到方法調(diào)用指令后面的一條指令误趴。這樣使得當前的棧幀能夠和調(diào)用者連接起來,并且讓調(diào)用者的棧幀的操作數(shù)棧繼續(xù)往下執(zhí)行务傲。方法的異常調(diào)用完成凉当,主要是JVM跑出的異常枣申,如果異常沒有被不貨主,或者遇到athrow字節(jié)碼指令顯示拋出看杭,那么就沒有返回值給調(diào)用者忠藤。
二、字節(jié)碼指令集
了解了棧幀的數(shù)據(jù)結(jié)構(gòu)之后泊窘,繼續(xù)擴展到字節(jié)碼指令集的擴展熄驼。那么字節(jié)碼指令又是由哪些元素構(gòu)成,以及會怎樣地影響我們的當前方法的棧幀的出棧烘豹、入棧操作的呢瓜贾。從結(jié)構(gòu)到用途開始詳述一部分指令集(主要是從作用范圍將指令集劃分為兩類:局部變量表和操作數(shù)棧傳遞數(shù)據(jù)的指令集,只在操作數(shù)棧中操作的指令集)携悯。
1祭芦、構(gòu)成元素
字節(jié)碼的指令,是由一個字節(jié)長度的助記符表示的操作碼(Opcode)以及其隨后的需要操作的若干參數(shù)構(gòu)成憔鬼。有的指令并不一定需要參數(shù)龟劲。但這里注意不要混淆一個概念,這里的參數(shù)和操作數(shù)(oprends)不是同一個概念轴或。這里的arguments(參數(shù))是靜態(tài)的值昌跌,編譯期就存儲在編譯后的字節(jié)碼中,而Oprends(操作數(shù))的值第一節(jié)介紹的操作數(shù)棧中運行期才知道值的數(shù)據(jù)結(jié)構(gòu)照雁。不知道講清楚沒有蚕愤,但發(fā)現(xiàn)很多譯文以及文章都會混淆指令集的”參數(shù)”和操作數(shù)棧的”操作數(shù)”。如果這里不夠清晰饺蚊,那么下面繼續(xù)看萍诱,后面具體的指令集的例子,就清楚了污呼。
對于操作參數(shù)的數(shù)量及長度都是由Opcode決定的裕坊,如果需要操作的長度超出了一個字節(jié),就會按照高位在前的字節(jié)序存儲燕酷。并且字節(jié)碼指令流都是單字節(jié)對齊籍凝,所以超出單字節(jié)的操作參數(shù)會需要預(yù)留“位置”來實現(xiàn)對齊。
這里還需要我們記住的一點是苗缩,Opcode是由一個字節(jié)長度的助記符表示静浴,JVM 規(guī)范制定中需要很謹慎小心得“節(jié)約”指令的命名。對于一些boolean挤渐、short、byte双絮、char的操作都是講數(shù)據(jù)轉(zhuǎn)化成int數(shù)據(jù)進行操作的浴麻,這樣就可以使用同一條指令來操作更多的數(shù)據(jù)類型得问。下面可以看到一些指令的例子。
2软免、指令
按照JVM規(guī)范中宫纬,將字節(jié)碼指令按照用途劃分成加載和存儲指令、運算指令膏萧、類型轉(zhuǎn)換指令漓骚、對象創(chuàng)建指令、操作數(shù)棧管理指令榛泛、方法調(diào)用和返回指令以及同步指令等等蝌蹂。看起來頗多曹锨。這里我們按照指令的操作范圍劃分為兩種:局部變量表和操作數(shù)棧傳遞數(shù)據(jù)的指令以及只在操作數(shù)棧中操作的指令孤个。
這里先簡單列舉一下Class文件中對于Java的類型描述。以下是Java基礎(chǔ)類型和數(shù)組沛简、Object的表述齐鲤。對于類或者接口,類型描述其實是將如java.lang.String 變成了java/lang/String 椒楣。用斜線分隔给郊。
1、局部變量表和操作數(shù)棧傳遞數(shù)據(jù)的指令:
ILOAD, LLOAD, FLOAD, DLOAD以及ALOAD指令都是從局部變量表中獲取參數(shù)壓入到操作數(shù)棧的捧灰,其中ILOAD包括了load boolean淆九、char、short凤壁、byte和int類型的操作吩屹。FLOAD, DLOAD 指令操作的數(shù)據(jù)需要占用兩個槽(slot i 及i+1)。ALOAD 是load 對象或者數(shù)組類型拧抖。ISTORE,LSTORE煤搜,ASTORE等操作是從操作數(shù)棧棧頂壓入局部變量表的指令。
2唧席、只在操作數(shù)棧操作數(shù)據(jù)的指令:
2.1 棧操作:POP 指令把值壓到棧頂擦盾。還有DUP、SWAP指令
2.2 常量值推入棧頂:ACONST_NULL 把null值推入淌哟,ICONST_0 把int 0推入棧頂迹卢,其他指令不一一列舉了
2.3運算操作:xADD, xSUB, xMUL, xDIV 以及xREM。對應(yīng)著+徒仓,-腐碱,*,/ ,%的運算症见。X分別對應(yīng)前面提到的基本數(shù)據(jù)類型喂走。
2.4 類型轉(zhuǎn)換:I2F, F2D, L2D 等等是對類型轉(zhuǎn)換的操作。
2.5 對象操作:如NEW 指令就將一個對象引用入棧谋作。
2.6 讀寫Fields:GETFIELD芋肠,PUTFIELD。對于static屬性的操作有:GETSTATIC 遵蚜,PUTSTATIC
2.7 調(diào)用Methods:對方法的調(diào)用帖池,構(gòu)造函數(shù)操作的時候,會操作所有方法參數(shù)入棧吭净。如INVOKESTATIC睡汹、INVOKEINTERFACE等。
2.8 讀寫數(shù)組值:xALOAD以及xASTORE 攒钳。x對應(yīng)的是I, L, F, D ,A, ?B, C , S等類型數(shù)據(jù)的數(shù)組的索引帮孔、值入棧出棧的操作。
2.9 跳轉(zhuǎn)操作:TABLESWITCH不撑、LOOKUPSWITCH 指令對應(yīng)的是switch的操作指令文兢。作為條件判斷if、do while焕檬、continue 等的跳轉(zhuǎn)指令也是直接在操作數(shù)棧中進行的姆坚。
2.10 返回指令:RETURN 以及xRETURN、前者是對應(yīng)方法返回void類型的操作实愚,后者是對應(yīng)x類型的返回值兼呵,返回給方法調(diào)用者的指令。
三腊敲、例子
下面來結(jié)合例子來看下字節(jié)碼指令在虛擬機棧中的操作击喂,更進一步理解部分字節(jié)碼指令的含義。
package bytecode; ?
/**?
?* Created by yunshen.ljy on 2015/6/16.?
?*/ ?
public class Coffee { ?
? ? int bean; ?
? ? public int getBean() { ?
? ? ? ? return this.bean; ?
? ? } ?
? ? public void setBean(int bean) { ?
? ? ? ? this.bean = bean; ?
? ? } ?
} ?
然后查看字節(jié)碼:
1碰辅、getBean 方法如下:
0: ?aload_0 ?
1: ?getfield ? ?#2; //Field bean:I ?
4: ?ireturn ?
第一行指令是當方法被調(diào)用懂昂,也就是方法的棧幀創(chuàng)建時,將獲取局部變量表索引值為0 的值(也就是this)没宾,入操作數(shù)棧凌彬。
第二行指令,將這個值(也就是this對應(yīng)的值)出棧循衰,賦給this對象的 bean field铲敛。
第三行指令,將this.f 出棧会钝,并且將值返回給調(diào)用者(這里ireturn 是int類型)伐蒋。
2、 setBean() 方法字節(jié)碼:
0: ?aload_0 ?
1: ?iload_1 ?
2: ?putfield ? ?#2; //Field bean:I ?
5: ?return ?
第一行指令和getBean 方法一樣,都是將this如操作數(shù)棧先鱼。
第二行指令是將已經(jīng)初始化(棧幀創(chuàng)建徒蟆,也就是方法調(diào)用時初始化)的參數(shù)bean 的值入操作數(shù)棧。
第三行指令將這兩個值出棧型型,并且存儲這個int值存儲到到bean屬性的引用,也就是this.bean中全蝶。
第四行指令闹蒜,在源碼中沒有return 語句,但是在編譯后的字節(jié)碼中抑淫,會自動生成一個return 指令绷落,消除當前方法(current method)的棧幀并且返回給調(diào)用者。
當然始苇,如果沒有程序?qū)崿F(xiàn)自己的構(gòu)造器的話砌烁,編譯后的類還有個默認的public 構(gòu)造器。Coffee () { super(); }
3催式、構(gòu)造器的字節(jié)碼如下:
0: ?aload_0 ?
1: ?invokespecial ? #1; //Method java/lang/Object."<init>":()V ?
4: ?return ?
第一行指令和getBean 方法一樣函喉,都是將this如操作數(shù)棧。
第二行指令荣月,將值(this)出棧,并調(diào)用Object class的<init>方法管呵,其實也就是因為隱式調(diào)用了super()方法,而Coffee的父類是Object哺窄。這里的<init>方法是編譯后的類對應(yīng)的構(gòu)造器的方法捐下,編譯器會為每個構(gòu)造器生成一個<init>方法。
第三行指令萌业,同之前的幾個return命令一樣坷襟,返回給調(diào)用者。