Java字節(jié)碼,你還可以搲的更深一些!
Java真的是長(zhǎng)盛不衰,擁有頑強(qiáng)的生命力。其中,字節(jié)碼機(jī)制功不可沒(méi)。字節(jié)碼,就像是 Linux 的 ELF。有了它,JVM直接搖身一變,變成了類似操作系統(tǒng)的東西。
要學(xué)習(xí)字節(jié)碼,不能僅僅靠看枯燥的文檔。本文會(huì)介紹幾個(gè)有用的工具,可以非常容易的上手,來(lái)實(shí)際觀測(cè)class文件這個(gè)小魔獸,助你搲的更深一些。
1、字節(jié)碼結(jié)構(gòu)
1.1、基本結(jié)構(gòu)
在開始之前,我們先簡(jiǎn)要的介紹一下class文件的內(nèi)容。這個(gè)結(jié)構(gòu),可以使用jclasslib工具來(lái)查看。
上圖是class文件基本內(nèi)容。這部分內(nèi)容枯燥乏味,關(guān)于它的細(xì)節(jié)在Java的官方都能非常容易的找到。
如下圖,展示了一個(gè)簡(jiǎn)單方法的字節(jié)碼描述,我們可以看到真正的執(zhí)行指令在整個(gè)文件結(jié)構(gòu)中的具體位置。
1.2、實(shí)際觀測(cè)
為了讓大家避免避免枯燥的二進(jìn)制對(duì)比分析,直接定位到真正的數(shù)據(jù)結(jié)構(gòu),這里介紹一個(gè)小工具,使用這種方式學(xué)習(xí)字節(jié)碼會(huì)節(jié)省很多時(shí)間。
這個(gè)工具就是asmtools,執(zhí)行下面的命令,將看到類的 JCED 語(yǔ)法結(jié)果。
輸出的結(jié)果類似于下面的結(jié)構(gòu),它與我們上面介紹的字節(jié)碼組成是一一對(duì)應(yīng)的,對(duì)照官網(wǎng)或者書籍,學(xué)習(xí)速度飛快。
了解了類的文件組織方式,下面我們來(lái)看一下,類文件在加載到內(nèi)存中以后,是一個(gè)什么表現(xiàn)形式。
2、內(nèi)存表示
準(zhǔn)備以下代碼,使用javac -g InvokeDemo.java進(jìn)行編譯。然后使用java命令執(zhí)行。程序?qū)⒆枞趕leep函數(shù)上,我們來(lái)看一下它的內(nèi)存分布。
為了更加明顯的看到這個(gè)過(guò)程,下面介紹一下 jhsdb 這個(gè)工具,這是在 Java9 之后 JDK 先加入的調(diào)試工具,我們可以在命令行使用 jhsdb hsdb 來(lái)啟動(dòng)它。注意,要加載相應(yīng)的進(jìn)程時(shí),必須確保是同一個(gè)版本的應(yīng)用進(jìn)程,否則會(huì)產(chǎn)生報(bào)錯(cuò)。
attach啟動(dòng)后的Java進(jìn)程后,可以在 Class Browser 菜單查看加載的所有類信息。我們?cè)谒阉骺蜉斎?nbsp;InvokeDemo,找到要查看的類。
@符號(hào)后面的,就是具體的內(nèi)存地址,我們可以復(fù)制一個(gè),然后在Inspector 視圖查看具體的屬性??梢源篌w認(rèn)為這就是類在方法區(qū)的具體存儲(chǔ)。
在Inspector視圖中,我們找到方法相關(guān)的屬性 _methods,可惜的是它無(wú)法點(diǎn)開,也無(wú)法查看。
接下來(lái)可以使用命令行來(lái)檢查這個(gè)數(shù)組里面的值。打開菜單中Console,然后輸入examine命令??梢钥吹竭@個(gè)數(shù)組里的內(nèi)容,對(duì)應(yīng)的地址就是Class視圖中的方法地址。
我們可以在Inspect視圖看到方法所對(duì)應(yīng)的內(nèi)存信息,這確實(shí)是一個(gè)Method方法的表示。
相比較起來(lái),對(duì)象就簡(jiǎn)單的,它只需要保存一個(gè)到達(dá)Class對(duì)象的指針即可。我們需要先從對(duì)象視圖進(jìn)入,然后找到它,一步步進(jìn)入Inspect視圖。
由以上的這些分析,我們可以得出下面這張圖。執(zhí)行引擎想要運(yùn)行某個(gè)對(duì)象的方法,需要先在棧上找到這個(gè)對(duì)象的引用,然后再通過(guò)的對(duì)象的指針,找到相應(yīng)的方法字節(jié)碼。
3、方法調(diào)用指令
關(guān)于方法的調(diào)用,Java一共提供了5個(gè)指令,用來(lái)調(diào)用不同類型的函數(shù)。
- invokestatic
- invokevirtual
- invokeinterface 和上面這條指令類似,不過(guò)是作用于接口類。
- invokespecial 用于調(diào)用私有實(shí)例方法、構(gòu)造器,以及super關(guān)鍵字等。
- invokedynamic 用于調(diào)用動(dòng)態(tài)方法。
我們依然使用上面的代碼片段看一下前四個(gè)指令的使用場(chǎng)景。代碼中包含一個(gè)接口I,一個(gè)抽象類Abs,一個(gè)實(shí)現(xiàn)和繼承了兩者的類InvokeDemo。
參考Java的類加載機(jī)制,在class文件被加載到方法區(qū)以后,就完成了從符號(hào)引用到具體地址的轉(zhuǎn)換過(guò)程。
我們可以看一下編譯后的main方法字節(jié)碼。尤其需要注意的是對(duì)于接口方法的調(diào)用。使用實(shí)例對(duì)象直接調(diào)用,和強(qiáng)制轉(zhuǎn)化成接口調(diào)用,所調(diào)用的字節(jié)碼指令分別是 invokevirtual 和invokeinterface,它們是不同的。
另外還有一點(diǎn),和我們想象中的不同,大多數(shù)普通方法調(diào)用,使用的是 invokevirtual 指令,它其實(shí)是和invokeinterface 一類的,都屬于虛方法調(diào)用。很多時(shí)候,JVM需要根據(jù)調(diào)用者的動(dòng)態(tài)類型,來(lái)確定調(diào)用的目標(biāo)方法,這就是動(dòng)態(tài)綁定的過(guò)程。
invokevirtual指令有多態(tài)查找的機(jī)制,該指令的運(yùn)行時(shí)解析過(guò)程步驟如下:
- 找到操作數(shù)棧頂?shù)牡谝粋€(gè)元素所指向的對(duì)象的實(shí)際類型,記做c。
- 如果在類型c中找到與常量中的描述符和簡(jiǎn)單名稱都相符的方法,則進(jìn)行訪問(wèn)權(quán)限校驗(yàn),如果通過(guò)則返回這個(gè)方法的直接引用,查找過(guò)程結(jié)束,不通過(guò)則返回java.lang.IllegalAccessError。
- 否則,按照繼承關(guān)系從下往上依次對(duì)c的各個(gè)父類進(jìn)行第二步的搜索和驗(yàn)證過(guò)程。
- 始終沒(méi)找到合適的方法,拋出java.lang.AbstractMethodError異常。這就是java語(yǔ)言中方法重寫的本質(zhì)。
相對(duì)比,invokestatic指令,加上invokespecial指令,就屬于靜態(tài)綁定過(guò)程。
所以靜態(tài)綁定,指的是能夠直接識(shí)別目標(biāo)方法的情況,而動(dòng)態(tài)綁定指的是需要在運(yùn)行過(guò)程中根據(jù)調(diào)用者的類型來(lái)確定目標(biāo)方法的情況。
可以想象,相對(duì)于靜態(tài)綁定的方法調(diào)用來(lái)說(shuō),動(dòng)態(tài)綁定的調(diào)用就更加耗時(shí)一些。由于方法的調(diào)用非常的頻繁,JVM對(duì)動(dòng)態(tài)調(diào)用的代碼進(jìn)行了比較多的優(yōu)化。比如使用方法表來(lái)加快對(duì)具體方法的尋址,以及使用更快的緩沖區(qū)來(lái)直接尋址( 內(nèi)聯(lián)緩存)。
4、invokedynamic
有時(shí)候在寫一些python腳本或者js腳本的時(shí)候,會(huì)特別羨慕這些動(dòng)態(tài)語(yǔ)言。如果把查找目標(biāo)方法的決定權(quán),從虛擬機(jī)轉(zhuǎn)嫁給用戶代碼,我們就會(huì)有更高的自由度。
我們單獨(dú)把invokedynamic抽離出來(lái)介紹,是因?yàn)樗容^復(fù)雜。和反射類似,它用于一些動(dòng)態(tài)的調(diào)用場(chǎng)景,但它和反射有著本質(zhì)的不同,效率也比反射要高的多。
這個(gè)指令通常在lambda語(yǔ)法中出現(xiàn),我們來(lái)看一下一小段代碼。
使用javap -p -v 命令可以在main方法中看到invokedynamic指令。
另外,我們?cè)趈avap的輸出中找到了一些奇怪的東西。
BootstrapMethods屬性在Java1.7以后才有,位于類文件的屬性列表中,這個(gè)屬性用于保存 invokedynamic 指令引用的引導(dǎo)方法限定符。
和上面介紹的四個(gè)指令不同,invokedynamic并沒(méi)有確切的接收對(duì)象,取而代之的,是一個(gè)叫做 CallSite 的對(duì)象。
其實(shí),invokedynamic指令的底層,是使用方法句柄(MethodHandle)來(lái)實(shí)現(xiàn)的。方法句柄是一個(gè)能夠被執(zhí)行的引用,它可以指向靜態(tài)方法和實(shí)例方法,以及虛構(gòu)的get和set方法,從IDE中可以看到這些函數(shù)。
句柄類型(MethodType)就是我們對(duì)方法的具體描述,配合方法名稱,能夠定位到一類函數(shù)。訪問(wèn)方法句柄和調(diào)用原來(lái)的指令是基本一致的,但它的調(diào)用異常,包括一些權(quán)限檢查,是在運(yùn)行時(shí)才能被發(fā)現(xiàn)的。
lambda語(yǔ)言實(shí)際上是通過(guò)方法句柄來(lái)完成的,在調(diào)用鏈上自然也多了一些調(diào)用步驟,那么在性能上,是否就意味著lambda性能低呢?對(duì)于大部分“非捕獲”的lambda表達(dá)式來(lái)說(shuō),JIT編譯器的逃逸分析能夠優(yōu)化這部分差異,性能和傳統(tǒng)方式無(wú)異;但對(duì)于“捕獲型”的表達(dá)式來(lái)說(shuō),就需要通過(guò)方法句柄,不斷的生成適配器,性能自然就低了很多(不過(guò)和便捷性相比,一丁點(diǎn)性能損失是可接受的)。
除了lambda表達(dá)式,我們還沒(méi)有其他的方式來(lái)產(chǎn)生invokedynamic指令。但是我們可以使用一些外部的字節(jié)碼修改工具,比如ASM,來(lái)生成一些帶有這個(gè)指令的字節(jié)碼,這通常能夠完成一些非??岬墓δ?,比如完成一門弱類型檢查的JVM-Base語(yǔ)言。
END
本文從Java字節(jié)碼的頂層結(jié)構(gòu)介紹開始,通過(guò)一個(gè)實(shí)際代碼,了解了類加載以后,在JVM內(nèi)存里的表現(xiàn)形式,并了解了jhsdb對(duì)Java進(jìn)程的觀測(cè)方式。
我們了解到Java7之后的invokedynamic指令,它實(shí)際上是通過(guò)方法句柄來(lái)實(shí)現(xiàn)的。和我們關(guān)系最大的就是Lambda語(yǔ)法,了解了這些原理,可以忽略那些對(duì)Lambda性能高低的爭(zhēng)論,要盡量寫一些“非捕獲”的Lambda表達(dá)式。
什么?你問(wèn)什么叫非捕獲?那就需要你自己搲了。
作者簡(jiǎn)介:小姐姐味道 (xjjdog),一個(gè)不允許程序員走彎路的公眾號(hào)。聚焦基礎(chǔ)架構(gòu)和Linux。十年架構(gòu),日百億流量,與你探討高并發(fā)世界,給你不一樣的味道。