淺談JVM字節(jié)碼執(zhí)行模型和字節(jié)碼指令集

一個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)用者。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末生年,一起剝皮案震驚了整個濱河市婴程,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌晶框,老刑警劉巖排抬,帶你破解...
    沈念sama閱讀 217,277評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異授段,居然都是意外死亡蹲蒲,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評論 3 393
  • 文/潘曉璐 我一進店門侵贵,熙熙樓的掌柜王于貴愁眉苦臉地迎上來届搁,“玉大人,你說我怎么就攤上這事】溃” “怎么了宴胧?”我有些...
    開封第一講書人閱讀 163,624評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長表锻。 經(jīng)常有香客問我恕齐,道長,這世上最難降的妖魔是什么瞬逊? 我笑而不...
    開封第一講書人閱讀 58,356評論 1 293
  • 正文 為了忘掉前任显歧,我火速辦了婚禮,結(jié)果婚禮上确镊,老公的妹妹穿的比我還像新娘士骤。我一直安慰自己,他們只是感情好蕾域,可當我...
    茶點故事閱讀 67,402評論 6 392
  • 文/花漫 我一把揭開白布拷肌。 她就那樣靜靜地躺著,像睡著了一般旨巷。 火紅的嫁衣襯著肌膚如雪巨缘。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,292評論 1 301
  • 那天契沫,我揣著相機與錄音带猴,去河邊找鬼。 笑死懈万,一個胖子當著我的面吹牛拴清,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播会通,決...
    沈念sama閱讀 40,135評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼口予,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了涕侈?” 一聲冷哼從身側(cè)響起沪停,我...
    開封第一講書人閱讀 38,992評論 0 275
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎裳涛,沒想到半個月后木张,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,429評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡端三,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,636評論 3 334
  • 正文 我和宋清朗相戀三年舷礼,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片郊闯。...
    茶點故事閱讀 39,785評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡妻献,死狀恐怖蛛株,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情育拨,我是刑警寧澤谨履,帶...
    沈念sama閱讀 35,492評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站熬丧,受9級特大地震影響笋粟,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜析蝴,卻給世界環(huán)境...
    茶點故事閱讀 41,092評論 3 328
  • 文/蒙蒙 一矗钟、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧嫌变,春花似錦、人聲如沸躬它。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽冯吓。三九已至倘待,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間组贺,已是汗流浹背凸舵。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留失尖,地道東北人啊奄。 一個月前我還...
    沈念sama閱讀 47,891評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像掀潮,于是被迫代替她去往敵國和親菇夸。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,713評論 2 354

推薦閱讀更多精彩內(nèi)容