避免在 VM 启动时使用 class 的 lambda 和流

Avoiding lambda and stream usage for a class used at VM Startup

通过 java.lang.module 我在 class 文档中阅读了以下内容:

@implNote ... is used at VM startup and so deliberately
avoids using lambda and stream usages in code paths used during
startup.

这里避免使用 lambda 和流的原因是什么?它们可能产生的影响是什么?

插图有助于更好地理解,但不是在这里寻求意见。

不依赖 lambda 和流(广泛使用 lambda)有助于避免在 VM bootstrap 上进行冗余工作。这反过来又减少了启动时间和内存占用。

invokedynamic JDK 中的机器相当复杂。它涉及许多 java.lang.invoke.* class 与方法句柄、Lambda 元工厂等相关的需要加载和初始化的内容。此外,对于 link invokedynamic 字节码,JVM 使用 ObjectWeb ASM 框架动态创建适配器。在 运行 时间内生成这样的 classes 也需要时间和 space.

让我们测量一下在非常基本的场景中使用 lambda 而不是 inner class 的开销。我创建了两个类似的 classes,除了实例化内部 class 或 lambda:

class Inner {
    public static void main(String[] args) {
        Runnable r = new Runnable() { public void run() {} };
        r.run();
    }
}

class Lambda {
    public static void main(String[] args) {
        Runnable r = () -> {};
        r.run();
    }
}

然后我 运行 都打开了 class 加载日志:

java -Xlog:class+load:file=inner.log Inner
java -Xlog:class+load:file=lambda.log Lambda

inner.log

[0.011s][info][class,load] opened: C:\Program Files\Java\jdk-9\lib\modules
[0.022s][info][class,load] java.lang.Object source: jrt:/java.base
[0.022s][info][class,load] java.io.Serializable source: jrt:/java.base
...
[0.136s][info][class,load] Inner source: file:/C:/Andrei/
[0.136s][info][class,load] java.lang.Shutdown source: jrt:/java.base
[0.136s][info][class,load] java.lang.Shutdown$Lock source: jrt:/java.base

lambda.log

[0.011s][info][class,load] opened: C:\Program Files\Java\jdk-9\lib\modules
[0.022s][info][class,load] java.lang.Object source: jrt:/java.base
[0.022s][info][class,load] java.io.Serializable source: jrt:/java.base
...
[0.159s][info][class,load] Lambda$$Lambda/1282788025 source: Lambda
[0.159s][info][class,load] java.lang.invoke.InnerClassLambdaMetafactory source: jrt:/java.base
[0.159s][info][class,load] java.lang.invoke.MethodHandleImpl$IntrinsicMethodHandle source: jrt:/java.base
[0.159s][info][class,load] java.lang.invoke.SimpleMethodHandle source: jrt:/java.base
[0.159s][info][class,load] sun.invoke.util.Wrapper source: jrt:/java.base
[0.160s][info][class,load] java.lang.invoke.LambdaForm$MH/100555887 source: java.lang.invoke.LambdaForm
[0.160s][info][class,load] java.lang.invoke.LambdaForm$MH/1983747920 source: java.lang.invoke.LambdaForm
[0.160s][info][class,load] java.lang.Shutdown source: jrt:/java.base
[0.161s][info][class,load] java.lang.Shutdown$Lock source: jrt:/java.base

完整输出为here。正如我们所见,Inner 需要 136 毫秒和 537 次加载 classes,而 Lambda 需要 161 毫秒和 620 次加载 classes。

因此,在这个简单的示例中,避免使用单个 lambda 有助于节省 25 毫秒的启动时间,同时减少 83 class 加载。

编辑

我描述的开销由两部分组成:

  1. 加载和初始化 java.lang.invoke.* classes - 这是常量部分,只需完成一次。
  2. 链接特定的 lambda 调用站点 - 这需要调用 LambdaMetafactory bootstrap 方法并生成 运行 调用目标方法的时间适配器。这需要为每个 lambda 完成,因此这部分开销与代码中的 lambda 数量成正比。