invokedynamic指令的個(gè)人理解

試著在3000字以內(nèi)講清自己對(duì)Java 7中引入的invokedynamic
的理解。這篇文章主要省去了一些對(duì)Java API與JVM規(guī)范的引述,并側(cè)重于如何去理解以及該指令本身的使用的基本思路,而不是講解具體的使用規(guī)范。
文章如有缺漏錯(cuò)誤之處,歡迎反饋。
此處假定讀者掌握以下知識(shí):
Java語(yǔ)言
JVM的基本運(yùn)行模型
基本的字節(jié)碼指令
ASM的基本結(jié)構(gòu)與簡(jiǎn)單使用
概述
invokedynamic
這一指令由兩個(gè)詞語(yǔ)組成,即invoke(v., 調(diào)用)與dynamic(adv. & adj., 動(dòng)態(tài)的),顧名思義,這條指令可以訪問一個(gè)在運(yùn)行時(shí)動(dòng)態(tài)地確定而非硬編碼于類文件中的目標(biāo)。
簡(jiǎn)單來說,這條指令的執(zhí)行包括以下幾個(gè)步驟:
如果是第一次執(zhí)行,調(diào)用指令附加參數(shù)中給出的一個(gè)方法(
BootstrapMethod
)來確定該指令的行為,如調(diào)用哪一個(gè)方法或者是訪問哪一個(gè)字段。按照第一次執(zhí)行時(shí)確定的行為執(zhí)行具體操作。
也就是說,invokedynamic
指令的行為是在第一次被使用時(shí)才被確定的。
方法句柄MethodHandle
方法句柄代表了一個(gè)對(duì)類成員的基本操作,包括讀寫字段和調(diào)用方法等。可以認(rèn)為,一個(gè)方法句柄包含了一個(gè)具體成員的位置與其執(zhí)行的具體操作兩個(gè)屬性。
可以通過由java.invoke.MethodHandles.lookup()
方法獲取的MethodHandles.Lookup
實(shí)例中提供的工廠方法獲取MethodHandle
實(shí)例。那些工廠方法大致可以分為兩類,一類是名稱類似findXXX()
的方法,支持使用類似于反射的方式獲取方法句柄;另一類工廠方法的名稱類似于unflectXXX()
,支持為已有的Field
或Method
對(duì)象指定具體操作以將其轉(zhuǎn)換為方法句柄。
第一類工廠方法的形式大多類似于findXXX(class, name, type)
,三個(gè)參數(shù)分別為定義目標(biāo)成員的類相應(yīng)的Class
實(shí)例、目標(biāo)成員的名稱與目標(biāo)成員的確切類型(或方法簽名)。唯一的例外是findConstructor
方法,因?yàn)樗恍枰@式地指明名稱。在獲取操作方法的方法句柄時(shí),需要使用一個(gè)MethodType
實(shí)例來表示方法簽名,這個(gè)實(shí)例可以通過工廠方法MethodType.methodType()
獲取。
第二類工廠方法的形式比較簡(jiǎn)單,只接受一個(gè)Field
或Method
實(shí)例,此處不再細(xì)說。
另外,這些工廠方法在執(zhí)行時(shí)通常會(huì)檢查曾獲取所用的Lookup
實(shí)例的類是否能訪問目標(biāo)成員,如果失敗則拋出一個(gè)IllegalAccessException
。在Java 9及以后的版本中,我們可以使用privateLookupIn()
工廠方法獲取可以訪問調(diào)用類以外的其他類的私有成員的Lookup
實(shí)例。對(duì)于第二類工廠方法,我們可以預(yù)先在用到的反射對(duì)象上調(diào)用setAccessible(true)
來禁用這一訪問檢查,或許這也是Java 8中唯一可以獲取任意方法句柄的方案。
MethodHandles
類中也提供了多個(gè)工廠方法以獲取或變換一些方法句柄,此處不再贅述。
調(diào)用站點(diǎn)CallSite
調(diào)用站點(diǎn)是一個(gè)方法句柄的容器,方法句柄只有被包含在調(diào)用站點(diǎn)中時(shí)才可以在invokedynamic
指令中使用。
調(diào)用站點(diǎn)可以是可變的(MutableCallSite
,VolatileCallSite
),也可以是不可變的(ConstantCallSite
)。不可變的調(diào)用站點(diǎn)可能會(huì)更高效,因?yàn)镴VM可以對(duì)其進(jìn)行一些優(yōu)化。
也可以創(chuàng)建自己的CallSite
子類以實(shí)現(xiàn)一些自定義邏輯,如在調(diào)用次數(shù)超過一定值的前后提供不同的方法句柄。
BootstrapMethod
BootstrapMethod
,簡(jiǎn)稱“BSM”,是一個(gè)用于在運(yùn)行時(shí)確定invokedynamic
指令的具體行為的方法。當(dāng)然,Java 11中引入的動(dòng)態(tài)常量也使用了相同的技術(shù),但是這超出了本文的范圍,此處不再詳述。
一個(gè)BootstrapMethod
通常是一個(gè)靜態(tài)方法,前三個(gè)參數(shù)的類型必須依次為:
MethodHandles.Lookup
String
MethodType
這些參數(shù)后面還可以附加幾個(gè)參數(shù)用于傳遞一些附加信息。Java 10及之前的版本中,附加的參數(shù)類型可以為int
、float
、long
、double
、String
、MethodType
或MethodHandle
。同時(shí),該方法必須返回一個(gè)CallSite
實(shí)例。下方是一個(gè)簡(jiǎn)單的BootstrapMethod
的定義:
Java 11中也允許借助動(dòng)態(tài)常量技術(shù)使用其他類型的附加參數(shù),以后會(huì)對(duì)其進(jìn)行專門講解。 在執(zhí)行該方法時(shí),傳入的參數(shù)依次是:
由
invokedynamic
所在類通過MethodHandles.lookup()
工廠方法獲取的MethodHandle
實(shí)例;為
invokedynamic
指令指定的名稱;描述該
invokedynamic
行為的方法描述符附加的零至多個(gè)參數(shù) JVM標(biāo)準(zhǔn)中規(guī)定,也可以使用構(gòu)造器作為
BootstrapMethod
,只要那個(gè)構(gòu)造器能夠構(gòu)造出一個(gè)CallSite類型的對(duì)象。具體實(shí)現(xiàn)與使用靜態(tài)方法類似,此處不再贅述。 有必要說明,通過附加參數(shù)給出的MethodHandle
不可以訪問invokedynamic
指令所在類不可訪問的成員,否則在類的解析階段會(huì)因?yàn)樵L問檢查出錯(cuò)而拋出IllegalAccessError
。
invokedynamic
指令的格式
JVM字節(jié)碼中invokedynamic
指令的格式非常簡(jiǎn)單:
其中,兩個(gè)index
字節(jié)共同組成了一個(gè)指向常量池中一個(gè)CONSTANT_InvokeDynamic_info
結(jié)構(gòu)的索引,該結(jié)構(gòu)直接或間接地提供了以下信息:
invokedynamic
的名稱與對(duì)應(yīng)的方法描述符;BootstrapMethod
方法信息,可以指定一個(gè)靜態(tài)方法或構(gòu)造器;BootstrapMethod
方法的附加參數(shù)。 某種意義上也就是說,這三項(xiàng)信息是invokedynamic
方法的固定參數(shù)。
在ASM中使用invokedynamic
指令
Core API
可以由visitInvokeDynamicInsn()
方法獲取或創(chuàng)建invokedynamic
指令,其定義如下:
name
:該invokedynmaic
指令的名稱,只是傳入BootstrapMethod
一個(gè)常量,可以按需要(隨便)設(shè)定;descriptor
:描述invokedynmaic
指令行為方法一個(gè)方法描述符,應(yīng)與BootstrapMethod
返回的CallSite
實(shí)際相應(yīng)的方法簽名相符;bootstrapMethodHandle
:一個(gè)Handle
實(shí)例,提供的信息與MethodHandle
相似,指定了BootstrapMethod
具體實(shí)現(xiàn)的位置;bootstrapMethodArguments
:傳入BootstrapMethod
的附加參數(shù),可以為Integer
、Float
、Long
、Double
、String
、org.objectweb.asm.Type
和org.objectweb.asm.Handle
幾種類型。真正傳入BootstrapMethod
時(shí)基本類型的封裝類會(huì)被拆箱為基本類型,而ASM提供的Type
與Handle
類分別會(huì)被轉(zhuǎn)換為包含同樣信息的MethodType
與MethodHandle
實(shí)例。Java 11和ASM 7.0之后也可以傳入org.objectweb.asm.ConstantDynamic
來指定一個(gè)在運(yùn)行時(shí)動(dòng)態(tài)獲取的常量,它的值在調(diào)用BootstrapMethod
時(shí)會(huì)被計(jì)算出并作為參數(shù)傳入其中。其中
Handle
類是ASM提供的用于記錄MethodHandle
實(shí)例屬性的一個(gè)類,可以通過以下構(gòu)造器獲?。?/p>
tag
:用于描述該Handle
類型的一個(gè)數(shù)學(xué),決定了其對(duì)應(yīng)的MethodHandle
的行為,可以將ASM庫(kù)中Opcodes
接口中名為的H_XXX
字段(如Opcodes.H_GETFIELD
)傳入,在對(duì)JVM有所了解的前提下從名稱分析其含義還是比較簡(jiǎn)單的。owner
:包含目標(biāo)成員的類的內(nèi)部名稱。name
:目標(biāo)成員的名稱。descriptor
:描述該invokedynamic
指令行為的方法描述符。isInterface
:包含目標(biāo)成員的類是否是接口。
Tree API
Tree API中的InvokeDynamicInsnNode
對(duì)應(yīng)一個(gè)invokedynamic
指令,使用方法與Core API相似,此處不再贅述。
invokedynamic
指令的應(yīng)用
在Java語(yǔ)言中invokedynamic
指令兩個(gè)最常見個(gè)用途是實(shí)現(xiàn)Lambda表達(dá)式與方法引用,具體的實(shí)現(xiàn)方式超出了本文的范圍,本文中不再詳述。
此處我們真正要探討的是invokedynamic
自身的應(yīng)用。 舉個(gè)例子,假設(shè)一個(gè)應(yīng)用程序需要從一個(gè)配置文件中獲取真正的Main
類,那么這個(gè)應(yīng)用的入口類可以用反射這樣實(shí)現(xiàn):
如果不使用反射呢?我們也可以生成一個(gè)使用invokedynamic
的入口類! 可以使用以下代碼生成main()
方法的字節(jié)碼:
這時(shí),可以這樣實(shí)現(xiàn)BootstrapMethod
:
或許這個(gè)例子有些牽強(qiáng),但這確實(shí)在一個(gè)簡(jiǎn)單的情景下為我們展示了invokedynamic
指令的基本用法。
另一個(gè)比較接近實(shí)際的例子是自己在上個(gè)月做的AccessingPath編譯器中實(shí)現(xiàn)的使用字節(jié)碼訪問私有成員的功能。因?yàn)橹苯邮褂梅瓷涞男阅茌^低,那里使用了invokedynamic
來訪問私有字段與方法。具體實(shí)現(xiàn)可以在?https://github.com/lovexyn0827/MessMod/tree/master/src/main/java/lovexyn0827/mess/util/access?CompiledPath
與BytecodeHelper.addInvoker()
下找到。