在《How Does Android Support Java 8》中,提到Android目前還沒支援Java8,所以目前在Android中寫的Lambda其實會在編譯時desugar,來將Lambda實作回覆Anonymous inner class (AIC)的作法。
不過,仍然會好奇Java8是如何實作Lambda。Java7時有一個新引入指令碼:invokedynamic,Java8就是用其來完成bytecode的Lambda實作。
在介紹invodedynamic前,需要先大概講述一下相關的指令。
Invoke instructions
以下是一個簡單的範例,囊括所有舊的invoke instructions:
public class Sample extends ArrayList implements Iterable { |
以下則是相關的bytecode,省略Constant pool和其他不重要的部分:
Classfile .../Sample.class |
在開始說明之前,先提一下如何看註解的部分,以第一個invokespecial為例:
// Method java/util/ArrayList."<init>":()V |
這一段用於描述當前的invoke instruction要執行的函示,分成三個部分:
- Class name:java/lang/Object。
- Method name:”
“。 - Description:()V,()內代表傳入的參數,而V是回傳類型,整合起來表示是一個不帶參數且回傳Void的函示。更詳細的符號含義,可以參考官方在解釋Class檔案格式的文件。
大致了解後,各個invoke instruction的簡介如下:
- invokestatic
invokestatic #2 // Method sMethod:(I)V |
用於static函示呼叫,特點是在編譯時期就決定好執行的函示,以及使用的類別,可說是所見即所得的指令。
- incokevirtual
invokevirtual #3 // Method get:(I)Ljava/lang/Object; |
用於一般類別可繼承的函示呼叫,特點是在執行期間,才會決定要使用父類或是當前類別,來呼叫對應的函示。
- invokespecial:
invokespecial #3 // Method method:(I)V |
用於constructor、private和super函示呼叫,特點是在這些情況下,使用的是哪個類別的函示都是已知的,所以也可以當成是invokevirtual的特例。
- invokeinterface
invokeinterface #5, 1 // InterfaceMethod java/lang/Iterable.iterator:()Ljava/util/Iterator; |
用於interface函示呼叫。特點是執行時是透過interface,而不是一般類別。
可以觀察到這四個指令在使用時,都會明確指定使用的函示,輸入的參數及回傳的類型。即使是彈性最大的invokevirtual。
雖然這四種指令的組合,可以處理如Java此類強型別的語言,但遇到動態語言時,將顯得左右支絀。不過JVM為了 支援動態,還是有其折衷的做法,以Groovy舉例如下:
class DynamicSample { |
編譯後的class檔:
public class DynamicSample implements GroovyObject { |
執行過程如下:
透過
$getCallSiteArray
函示取得CallSiteArray。CallSiteArray內含三個CallSite,名稱分別是plus、add、add。由於此時的CallSite是AbstractCallSite,於是還沒指向任何實際操作的部分。而有plus是因為Groovy會把
+
的操作以plus函示執行。透過取得的AbstractCallSite執行
callStatic
。callStatic
會在DynamicSample內找尋可以接受兩個int或String的函示並執行,於是呼叫到add
。add
內再透過call
執行實際的相加操作。
簡單來說就是,編譯器直接在編譯時加入動態呼叫函示的邏輯。
可以感覺到加入的內容會因為程式本身,而有不同變化,複雜度相對提高,所以如果要簡化一些重複性的操作,就是在bytecode層面需要做些調整。
Invokedynamic
接著,我們在編譯時加上--indy
,來看如果使用invokedynamic,編譯後的內容會變成如何:
public class DynamicSample implements GroovyObject { |
可以看到原本編譯時會加入的$getCallSiteArray
,僅剩下$getStaticMetaClass
。而原本在main()
裡的操作也變得很單一。
由於add
用了新的指令invokedynamic,以下直接與原本的bytecode對照,來看有什麼變化:
// Before invokedynamic |
快速對照後,原本需要透過invokestatic和invokeinterfac才可完成的操作,縮減為只有invokedynamic。與此相對,BootstrapMethod被加入:
BootstrapMethods: |
忽略掉IndyInterface.bootstrap
的輸入參數,直接看回傳值就是上面曾經提過的CallSite。
所以新的實現邏輯和原本是類似的:在執行階段走到invokedynamic時,會呼叫BootstrapMethods的操作來取得CallSite,裡面包含要指定的函示然後執行。
與原本做法的主要差異在於:
- 編譯時期由編譯器產生的函示減少。
- 編譯後所需要的指令減少。
- 將操作介面統一標準化,降低bytecode的複雜度。
透過invokedynamic,簡化了動態語言在JVM上的實現,但invokedynamic是如何促成Java可使用Lambda取代傳統callback的實作?以下先從原本的callback實作開始講起。
Anonymous Inner Class (AIC)
首先範例程式如下:
public class CallbackSample { |
非常簡單的Runnable,直接來看看bytecode是如何實作:
... |
我們看到了一個特別的inner class,CallbackSample$1
,這是編譯器在編譯時會自動產生的class檔。此class檔會對應到程式碼中的AIC,有幾個AIC,就會有幾個這樣的class產生出來。
Lambda in Java
接著將範例改用Lambda風格:
public class LambdaSample { |
編譯成bytecode則是:
{ |
這裡我們看到與前面Groovy類似的結構,為了實現動態函示呼叫,Java做了幾個變化:
- 新增LambdaMetafactory、CallSite和MethodHandle等新的類別。
- 將Lambda實際操作的部分包裝成一個獨立的靜態函示
lambda$new$0
。 - 新增BootstrapMethods區塊給invokedynamic使用。
整個執行的流程如下:
- invokedynamic呼叫到BootstrapMethods內的LambdaMetafactory。
- 將呼叫
lambda$new$0
的指令invokestatic com/example/LambdaSample.lambda$new$0:()V
當成參數傳給LambdaMetafactory,此指令會被包裝成MethodHandle,可當成函示本身。 - MethodHandle會再被包裝進CallSite回傳。
Generate class on fly
到這可能會有個疑問,之前編譯AIC會產生的class檔去哪了?在執行時期,LambdaMetafactory.metafactory
會同時產生一個如同AIC做法的inner class,樣子大概如下:
final class LambdaSample$$Lambda$1 implements Runnable { |
所以前面流程,在進入LambdaMetafactory.metafactory
後,應該改成如下:
- 產生動態類別
LambdaSample$$Lambda$1
。 - 將呼叫Constructor或
get$Lambda()
的操作包裝成MethodHandle。 - 將此MathodHandle放進CallSite回傳。
接著就可以透過CallSite,執行MethodHandle取得LambdaSample$$Lambda$1
的物件,並指定給範例中的變數runnable
。這樣執行run
時,就可以呼叫到編譯器替我們產生的靜態函示lambda$main$0()
,執行我們原本設計好的操作。想更詳細了解這個過程,可以直接看LambdaMetafactory的原始碼。
另外,可能會令人好奇的是,為何在執行期間才建立class?實際上就算建在檔案中,也可以透過CallSite來使用。原因主要可能有以下兩個:
- 在執行期間產生的類別不用依賴實際的class檔,其signature也存在於constant pool,降低記憶體空間。且非檔案就沒有讀檔到建立物件的過程,在效能上也會有提升。
- 轉移到執行期間,可降低bytecode與JVM之間的相依性,當JVM日後能直接支援動態函示時,將可無痛的棄用invokedynamic。
What’s more
在開頭提到Android藉由desugar,把Lambda以傳統的callback實作代替,所以在編譯時期會自動建立對應的class。以Android來說,就是如下一般的Lambda類別:
-$$Lambda$Blur$vkhPi2gkU05Je_-6VHMwbOrEmug; |
產生類別本身不是大問題,問題是如果Lambda內的實作都一樣,在編譯後依然是產生不一樣的類別;如果開發者大量使用Lambda,將可能造成多餘的類別產生。
為了解決此問題,Jack Wharton在2017年的Driodcon UK有提到,Android新的編譯工具D8中,會將後面那串亂碼,以Lambda實作內容,透過SHA1 Base64的方式產生,並由此來避免重複的Lambda實作。