深入JDK源碼探索Java語言Lambda具體實現(xiàn)

當我剛剛開始學習Java的Lambda的時候噪裕,我曾經(jīng)完全誤解了它的實現(xiàn)方式嚎幸,并且前段時間發(fā)現(xiàn)居然有些朋友和當時的我一樣珊随。

比如下面這個例子:

class Foobar {
    Runnable foobar() {
        return () -> {};
    }
}

在最初的時候,我猜想它也許會像這樣實現(xiàn):

class Foobar {
    Runnable foobar() {
        return new Runnable() {
            @Override
            public void run() {
                return;
            }
        };
    }
}

實際上污淋,并不是顶滩。

當然了,如果真是這樣做了也算是一種實現(xiàn)方式寸爆,但是Java這里做出了更加巧妙的設計礁鲁。

這個函數(shù)在Java8中經(jīng)過編譯后的字節(jié)碼是這樣的:

  java.lang.Runnable foobar();
    descriptor: ()Ljava/lang/Runnable;
    flags:
    Code:
      stack=1, locals=1, args_size=1
         0: invokedynamic #2,  0              // InvokeDynamic #0:run:()Ljava/lang/Runnable;
         5: areturn
      LineNumberTable:
        line 3: 0

乍一看似乎莫名其妙,run方法其實也實現(xiàn)在了這個類里:

  private static void lambda$foobar$0();
    descriptor: ()V
    flags: ACC_PRIVATE, ACC_STATIC, ACC_SYNTHETIC
    Code:
      stack=0, locals=0, args_size=0
         0: return
      LineNumberTable:
        line 3: 0

它們通過InvokeDynamic這個字節(jié)碼指令以及一個BootstrapMethod才在兩者之間建立起聯(lián)系:

  0: #16 invokestatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
    Method arguments:
      #17 ()V
      #18 invokestatic Foobar.lambda$foobar$0:()V
      #17 ()V

看到這里的朋友赁豆,大概應該都是用過MethodHandle的吧仅醇,不然的話上面這段可能不太好理解。

用Java代碼大致模擬一下魔种,運行時的邏輯大概像是這樣:

class Foobar {
    @SuppressWarnings("unchecked")
    Runnable foobar() throws Throwable {
        return (Runnable) LambdaMetafactory.metafactory(
            MethodHandles.lookup(),
            "run",
            MethodType.methodType(Runnable.class),
            MethodType.methodType(void.class),
            MethodHandles.lookup().findStatic(Foobar.class, "lambda$foobar$0", MethodType.methodType(void.class)),
            MethodType.methodType(void.class)
        ).dynamicInvoker().invoke();
    }
    private static void lambda$foobar$0() {
        System.out.println("Hello");
        return;
    }
}

事實上析二,JVM的InvokeDynamic比起Java的Lambda表達式和方法引用表達式有著更為久遠的歷史,詳見JSR-292节预。

InvokeDynamic這一條指令叶摄,首先會通過一個BootstrapMethod,獲取到一個CallSite實例安拟,BootstrapMethod至少需要Lookup准谚、StringMethodType三個參數(shù)去扣,以及任意多個靜態(tài)參數(shù),相關細節(jié)見虛擬機規(guī)范invokedynamic樊破。

LambdaMetafactory中除了metafactory之外還有一個altMetafactory用于更加復雜的Lambda愉棱,本文暫不討論。

此時已經(jīng)可以了解到metafactory所需的六個參數(shù)分別代表什么哲戚,這里是它在Java8中的源碼奔滑。

這個方法很簡潔,只有短短幾行:

    public static CallSite metafactory(MethodHandles.Lookup caller,
                                       String invokedName,
                                       MethodType invokedType,
                                       MethodType samMethodType,
                                       MethodHandle implMethod,
                                       MethodType instantiatedMethodType)
            throws LambdaConversionException {
        AbstractValidatingLambdaMetafactory mf;
        mf = new InnerClassLambdaMetafactory(caller, invokedType,
                                             invokedName, samMethodType,
                                             implMethod, instantiatedMethodType,
                                             false, EMPTY_CLASS_ARRAY, EMPTY_MT_ARRAY);
        mf.validateMetafactoryArgs();
        return mf.buildCallSite();
    }

validateMetafactoryArgsInnerClassLambdaMetafactory從它的超類AbstractValidatingLambdaMetafactory中繼承而來的一個方法顺少,其作用只是進行一些校驗操作朋其,如果發(fā)現(xiàn)錯誤就會拋出LambdaConversionException王浴。它的代碼在這里

這里buildCallSite的源碼梅猿,它大致可以分為三個部分:

    @Override
    CallSite buildCallSite() throws LambdaConversionException {
        final Class<?> innerClass = spinInnerClass();
        if (invokedType.parameterCount() == 0 && !disableEagerInitialization) {
            // ...
            final Constructor<?>[] ctrs = /* ... */;
            Object inst = ctrs[0].newInstance();
            return new ConstantCallSite(MethodHandles.constant(samBase, inst));
            // ...
        } else {
            // ...
            if (!disableEagerInitialization) {
                UNSAFE.ensureClassInitialized(innerClass);
            }
            return new ConstantCallSite(MethodHandles.Lookup.IMPL_LOOKUP.findStatic(innerClass, NAME_FACTORY, invokedType));
            // ...
        }
    }

disableEagerInitializationjdk.internal.lambda.disableEagerInitialization選項有關氓辣。

首先會在spinInnerClass中定義一個源碼中不存在的類,在這里用到了asm框架袱蚓。

比方說這個例子:

class Foobar {
    Runnable foo() {
        return () -> {};
    }
    Callable<?> bar() {
        return () -> this;
    }
}
class Main {
    public static void main(String[] args) {
        new Foobar().foo();
        new Foobar().bar();
    }
}

可以在運行的時候加上-Djdk.internal.lambda.dumpProxyClasses這個選項钞啸。

它在運行時定義的兩個類大致是這樣的:

final class Foobar$$Lambda$1 implements java.lang.Runnable
  minor version: 0
  major version: 52
  flags: ACC_FINAL, ACC_SUPER, ACC_SYNTHETIC
Constant pool:
  // ...
{
  private Foobar$$Lambda$1();
    descriptor: ()V
    flags: ACC_PRIVATE
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #10                 // Method java/lang/Object."<init>":()V
         4: return

  public void run();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=0, locals=1, args_size=1
         0: invokestatic  #17                 // Method Foobar.lambda$foo$0:()V
         3: return
    RuntimeVisibleAnnotations:
      0: #12()
}
final class Foobar$$Lambda$2 implements java.util.concurrent.Callable
  minor version: 0
  major version: 52
  flags: ACC_FINAL, ACC_SUPER, ACC_SYNTHETIC
Constant pool:
  // ...
{
  private final Foobar arg$1;
    descriptor: LFoobar;
    flags: ACC_PRIVATE, ACC_FINAL

  private Foobar$$Lambda$2(Foobar);
    descriptor: (LFoobar;)V
    flags: ACC_PRIVATE
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: invokespecial #13                 // Method java/lang/Object."<init>":()V
         4: aload_0
         5: aload_1
         6: putfield      #15                 // Field arg$1:LFoobar;
         9: return

  private static java.util.concurrent.Callable get$Lambda(Foobar);
    descriptor: (LFoobar;)Ljava/util/concurrent/Callable;
    flags: ACC_PRIVATE, ACC_STATIC
    Code:
      stack=3, locals=1, args_size=1
         0: new           #2                  // class Foobar$$Lambda$2
         3: dup
         4: aload_0
         5: invokespecial #19                 // Method "<init>":(LFoobar;)V
         8: areturn

  public java.lang.Object call();
    descriptor: ()Ljava/lang/Object;
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: getfield      #15                 // Field arg$1:LFoobar;
         4: invokespecial #27                 // Method Foobar.lambda$bar$1:()Ljava/lang/Object;
         7: areturn
    RuntimeVisibleAnnotations:
      0: #22()
}

發(fā)現(xiàn)沒有,這個Runnable調(diào)用得是Foobar的靜態(tài)方法喇潘,并且它里面也并不持有外部引用体斩,但是如果使用匿名內(nèi)部類的實現(xiàn)方式,就有可能造成毫無意義的內(nèi)存泄漏颖低。

現(xiàn)在咱們再回過頭來看看buildCallSite的實現(xiàn)絮吵,它判斷了invokedType.parameterCount()是否等于零,如果為零說明不會捕獲任何外部變量忱屑,那么這樣的話蹬敲,也就沒有必要每次調(diào)用都為此而創(chuàng)建一個新的對象,但如果不為零的話想幻,則會通過get$Lambda獲取這個函數(shù)式接口的實例粱栖。

做一個簡單的實驗:

class Main {
    public static void main(String[] args) {
        assert(new Foobar().foo() == new Foobar().foo());
        assert(new Foobar().bar() == new Foobar().bar());
    }
}

開啟-ea選項后,以上程序?qū)⒃诘诙螖嘌詴r崩潰脏毯,因此對于無捕獲的Lambda表達式來說闹究,并不會重復創(chuàng)建對象,而用匿名內(nèi)部類的方式則需要手動實現(xiàn)復用食店。

經(jīng)過這一套流程渣淤,我們終于獲得了CallSite的實例,它將與這一個位置綁定吉嫩,重復調(diào)用則可以復用這同一個CallSite价认。

現(xiàn)在可以發(fā)現(xiàn),Java實際對Lambda和方法引用的實現(xiàn)自娩,是經(jīng)過一層一層的封裝用踩,從而優(yōu)化了許多細節(jié),但是還不止如此忙迁。

注意一下我上面說過的話脐彩,Java中InvokeDynamic的歷史比Lambda更長,JVM是一種語言無關的運行環(huán)境姊扔,只要按照規(guī)范的要求惠奸,編譯出合法的字節(jié)碼,那么無論任何語言都可以在JVM平臺上運行恰梢,包括Groovy這種動態(tài)語言佛南,有了InvokeDynamic這條指令梗掰,可以讓動態(tài)語言在JVM平臺上發(fā)展得更好。

InvokeDynamic這條字節(jié)碼指令它最大的意義是嗅回,它使得JVM新語言的實現(xiàn)者獲得了更大的自由及穗,每個語言的設計者可以用自己的方式創(chuàng)建CallSite,然后通過自己的編譯器去調(diào)用自己所定義的BootstrapMethod妈拌,而在這一整個過程中都不需要JVM為其提供任何額外的支持拥坛。

那么最后,我來編寫一段示例代碼尘分,作為這篇文章的收尾:

import jdk.internal.org.objectweb.asm.*;
import jdk.internal.org.objectweb.asm.tree.*;
import java.lang.instrument.*;
import java.lang.invoke.*;
import java.security.*;
class Main {
    public static void main(String[] args) {
        getLambda().run();
    }
    private static native Runnable getLambda();
}
class Premain extends ClassVisitor implements ClassFileTransformer {
    public static void premain(String arg, Instrumentation instrumentation) {
        instrumentation.addTransformer(new Premain());
    }
    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
        if (!"Main".equals(className)) {
            return classfileBuffer;
        }
        ClassWriter cw = new ClassWriter(Opcodes.ASM5);
        super.cv = cw;
        new ClassReader(classfileBuffer).accept(this, ClassReader.EXPAND_FRAMES);
        return cw.toByteArray();
    }
    private Premain() {
        super(Opcodes.ASM5, null);
    }
    @Override
    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
        if ("getLambda".equals(name)) {
            return null;
        }
        return super.visitMethod(access, name, desc, signature, exceptions);
    }
    @Override
    public void visitEnd() {
        MethodVisitor mv = super.visitMethod(Opcodes.ACC_PRIVATE | Opcodes.ACC_STATIC | Opcodes.ACC_SYNTHETIC, "getLambda", "()Ljava/lang/Runnable;", null, null);
        mv.visitCode();
        Label l0 = new Label();
        mv.visitLabel(l0);
        mv.visitInvokeDynamicInsn("run", "()Ljava/lang/Runnable;", new Handle(Opcodes.H_INVOKESTATIC, "Premain", "buildCallSite", "(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;"));
        Label l1 = new Label();
        mv.visitLabel(l1);
        mv.visitInsn(Opcodes.ARETURN);
        Label l2 = new Label();
        mv.visitLabel(l2);
        mv.visitMaxs(1, 0);
        mv.visitEnd();
        super.visitEnd();
    }
    public static CallSite buildCallSite(MethodHandles.Lookup caller, String invokedName, MethodType invokedType) {
        return new ConstantCallSite(MethodHandles.constant(Runnable.class, (Runnable) () -> System.out.println("Hello, world!")));
    }
}

有興趣的同學猜惋,可以通過靈活使用這一條InvokeDynamic指令,在JVM這個廣闊的平臺上培愁,定制自己更喜歡的語言著摔。

?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市定续,隨后出現(xiàn)的幾起案子谍咆,更是在濱河造成了極大的恐慌,老刑警劉巖私股,帶你破解...
    沈念sama閱讀 223,126評論 6 520
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件摹察,死亡現(xiàn)場離奇詭異,居然都是意外死亡倡鲸,警方通過查閱死者的電腦和手機供嚎,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,421評論 3 400
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來峭状,“玉大人克滴,你說我怎么就攤上這事∮糯玻” “怎么了劝赔?”我有些...
    開封第一講書人閱讀 169,941評論 0 366
  • 文/不壞的土叔 我叫張陵,是天一觀的道長胆敞。 經(jīng)常有香客問我着帽,道長,這世上最難降的妖魔是什么移层? 我笑而不...
    開封第一講書人閱讀 60,294評論 1 300
  • 正文 為了忘掉前任仍翰,我火速辦了婚禮,結(jié)果婚禮上幽钢,老公的妹妹穿的比我還像新娘。我一直安慰自己傅是,他們只是感情好匪燕,可當我...
    茶點故事閱讀 69,295評論 6 398
  • 文/花漫 我一把揭開白布蕾羊。 她就那樣靜靜地躺著,像睡著了一般帽驯。 火紅的嫁衣襯著肌膚如雪龟再。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,874評論 1 314
  • 那天尼变,我揣著相機與錄音利凑,去河邊找鬼。 笑死嫌术,一個胖子當著我的面吹牛哀澈,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播度气,決...
    沈念sama閱讀 41,285評論 3 424
  • 文/蒼蘭香墨 我猛地睜開眼割按,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了磷籍?” 一聲冷哼從身側(cè)響起适荣,我...
    開封第一講書人閱讀 40,249評論 0 277
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎院领,沒想到半個月后弛矛,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,760評論 1 321
  • 正文 獨居荒郊野嶺守林人離奇死亡比然,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,840評論 3 343
  • 正文 我和宋清朗相戀三年丈氓,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片谈秫。...
    茶點故事閱讀 40,973評論 1 354
  • 序言:一個原本活蹦亂跳的男人離奇死亡扒寄,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出拟烫,到底是詐尸還是另有隱情该编,我是刑警寧澤,帶...
    沈念sama閱讀 36,631評論 5 351
  • 正文 年R本政府宣布硕淑,位于F島的核電站课竣,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏置媳。R本人自食惡果不足惜于樟,卻給世界環(huán)境...
    茶點故事閱讀 42,315評論 3 336
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望拇囊。 院中可真熱鬧迂曲,春花似錦、人聲如沸寥袭。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,797評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至杰扫,卻和暖如春队寇,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背章姓。 一陣腳步聲響...
    開封第一講書人閱讀 33,926評論 1 275
  • 我被黑心中介騙來泰國打工佳遣, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人凡伊。 一個月前我還...
    沈念sama閱讀 49,431評論 3 379
  • 正文 我出身青樓零渐,卻偏偏與公主長得像,于是被迫代替她去往敵國和親窗声。 傳聞我的和親對象是個殘疾皇子相恃,可洞房花燭夜當晚...
    茶點故事閱讀 45,982評論 2 361

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

  • 夜鶯2517閱讀 127,728評論 1 9
  • 版本:ios 1.2.1 亮點: 1.app角標可以實時更新天氣溫度或選擇空氣質(zhì)量,建議處女座就不要選了笨觅,不然老想...
    我就是沉沉閱讀 6,908評論 1 6
  • 我是黑夜里大雨紛飛的人啊 1 “又到一年六月拦耐,有人笑有人哭,有人歡樂有人憂愁见剩,有人驚喜有人失落杀糯,有的覺得收獲滿滿有...
    陌忘宇閱讀 8,547評論 28 53
  • 兔子雖然是枚小碩 但學校的碩士四人寢不夠 就被分到了博士樓里 兩人一間 在學校的最西邊 靠山 兔子的室友身體不好 ...
    待業(yè)的兔子閱讀 2,611評論 2 9