之前研究了GraalVM的Native Image,主要优势是可以脱离jre,直接编译为原生代码,大幅提高启动速度,降低内存占用。OpenJDK也有一个类似目标的项目Project Leyden,这篇文章就来简单做一个技术前瞻。
Project Leyden介绍
Project Leyden 是 OpenJDK 社区主导的一项长期项目,旨在解决 Java 程序在启动时间、达到峰值性能时间和资源占用方面的痛点。
核心目标:缩短 Java 程序启动时间、减少达到峰值性能所需时间、降低内存占用。
目前Project Leyden已经有了可以使用的JDK EA版本可供下载尝鲜。Leyden Early-Access Builds,感兴趣的同学可以自行下载来感受一下。
Project Leyden目前的进展
首先我们要知道Project Leyden的主要目的就是提高Java应用的启动速度,让程序在更短的时间内达到峰值性能。为什么需要这个项目呢,这里就不得不提高JVM了。
众所周知,Java常常被人诟病过于臃肿,启动速度很慢。
Java虽然被称为静态语言,但是Java本身是非常动态的。Java可以在运行时生成类,可以通过反射动态生成对象,运行时修改对象内部的状态,在较早的Java版本,反射甚至可以修改final
字段。还可以通过jdk proxy
以及cglib
进行动态代理,JIT会动态生成编译代码,并且会去优化重新生成,等等其他一系列动态操作。
拥有这些动态的能力,就一定有取舍,JVM在启动时需要做很多事,解析类文件,验证类,解析常量池,读取并解析配置文件,以及初始化一些日志记录。如果用到了诸如spring这类框架,还需要使用反射扫描类路径,进行各种各样的配置,读取很多的类进行自动配置。
这里就出现了一个问题,事实上大部分应用程序每一次启动,都在做相同的事,解析相同的类,扫描相同的类路径,进行相同的配置。每一次启动的流程都是一样的,但是却每次都需要做一遍。
所以Project Leyden的作用就是可以提前记录下程序启动的流程,并缓存起来,再下次启动时就不需要再走一遍相同的流程,从而提高启动速度以及降低达到峰值性能的时间。
JVM现在已经实现了CDS机制,Class Data Sharing可以实现类的缓存,如果一个程序想要加载类,在CDS Archive里已经有了对应的类,那么就可以直接读取CDS Archive,相比读取类,加载类速度更快,提高了启动速度。
JVM启动时,在进入main函数前,也有很多启动的流程,分配HotSpot的内存空间,启动GC线程,JIT编译线程,加载Java Bootstrap类,java.lang.Object
,java.lang.Class
,java.lang.String
,以及ClassLoader,java.lang.Thread
等等。然后通过JNI的FindClass
来找到包含main函数的类,最后执行main函数。

Project Leyden的目标就是将类加载以及jvm启动初始化的流程合并在一起,并将时间缩短。也就是AOT缓存
AOT(Ahead Of Time)缓存
Project Leyden 扩展了类数据共享(CDS)功能,之前是程序在调用defineClass的时候,才会去到CDS去找对应的类;Leyden基于上次保存的类也将是你将来使用的相同的类的设想,因此它将不会等着你一个个请求,再一个个加载,而是直接把所有的类缓存到AOT缓存中,启动时直接一起加载。

这样有一个好处,就是一次性加载所有的缓存的类,我们可以将这些类都给定一个稳定的内存地址,那么他们互相直接的链接也可以提早进行加载,也就是JEP 483: Ahead-of-Time Class Loading & Linking提到的,提前加载和链接类。
提前链接可以实现很多的优化,父类和接口可以被提前链接到一起,关于类字段的布局可以被提前计算(对于之前提到的Project Valhalla更加重要,因为Project Valhalla想要实现真正的值类型,对于field的布局计算要求更高,目前的Java类中的对象field只是一个个指针),常量池也可以被提前解析。
Lambda invokedynamic优化
关于invokedynamic指令的实现,网上已经有很多的文章,这里就简述一下。在JVM第一次执行到invokedynamic指令时,将会调用一个被称为bootstrap
方法,它将会生成一个类,称为CallSite
,然后获取一个MethodHandle
(可以类比为C/C++中的函数指针)指向生成的那个类的方法,然后调用MethodHandle
。后续执行到相同的invokedynamic指令时,将会直接调用MethodHandle
,就不会再执行前面的bootstrap
函数进行初始化了。

Project Leyden所做的优化就是希望将这段初始化操作提取出去,Project Leyden有一个概念叫做Training run(预训练)
,在预训练阶段,Leyden 会观察应用程序行为并记录高频调用的方法及动态代理/反射元数据,生成优化的 CDS 归档文件。对于lambda函数,预训练将会记录接口的方法名,方法类型,是否可以序列化,每次调用时捕获的参数,等等一大堆数据。

记录下来之后,在构建上文提到的AOT缓存时
,就可以使用保存的数据直接完成bootstrap
阶段,生成CallSite
以及MethodHandle
,这样在部署运行时,就可以直接进入已经链接好的CallSite
,提高效率。

AOT函数Profile数据优化
上文提到了Project Leyden有一个Training run
的过程,来生成AOT缓存。这里还有一个JEP草案JEP draft: Ahead-of-Time Method Profiling,通过引入AOT函数的Profiling数据,提前让jvm基于Profiling的结果来进行优化,提高启动速度。
这样可以实现对于一些在Training run
就已经被识别为Hot Code
,也就是热点代码的函数,就可以直接通过AOT缓存在启动时不再以字节码解释运行,而是直接加载AOT编译后的代码,这样就可以大幅提高启动时的性能,更快的进入代码优化后的状态。
并且JVM的HotSpot也允许去优化和再优化的选项,即使我们使用Training run
获得的结果并不准确,有些不太热的代码,在实际运行时变热了,JIT编译也可以进行编译,同样可以达到峰值性能;或者在Training run
编译优化时做出的假设并不对,导致编译后代码出现问题,JIT也可以进行去优化并重新编译。
以上就是我个人对于Project Leyden的研究和总结,Project Leyden 在缩短 Java 应用启动时间和资源占用方面已取得实质性突破,EA版本的jdk也已经可以尝试。每一次去跟随前沿进程时都会感叹Java虚拟机的黑科技实在是太多了,正是有了前人的努力才使得JVM虚拟机如此优秀以及Java社区的繁荣。