Java Project Leyden如何提高JVM启动速度&技术前瞻

之前研究了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.Objectjava.lang.Classjava.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社区的繁荣。

相关推荐
mldong6 小时前
一个 App,十三套后端:手机审批端 uni-jeeflow-app 开源了
java·架构
tqs_123457 小时前
MySQL RR隔离级别死锁|Gap间隙锁、临键锁,订单并发范围查询死锁根因方案
java
逆境不可逃7 小时前
Pi Agent 学习笔记:多个工具怎样并行执行
java
Flynt8 小时前
Java 27 升级实测:默认值动得比新特性多,有个老参数会让 JVM 直接起不来
java·jvm·后端
Bs_MoneyMagnet8 小时前
基于springboot+vue的个人健康管理系统的设计与实现 源码+文档
java·vue.js·spring boot·后端·vue3·springboot3·计算机毕业设计
星云API技术支持8 小时前
企业微信二次开发:群权限设置、成员管理与群资料维护的接口组合实践
java·前端·企业微信
niucloud-admin8 小时前
JAVA V6 多商户商城 开发文档——job 计划任务开发
java·python·github
海宇AI10 小时前
零信任架构实战:基于海宇柠檬查出险-登记证构建自动化车抵贷核保网关
java·人工智能·架构·自动化
cfm_291411 小时前
单例模式详解
java
我叫张土豆11 小时前
本体论、DDD、SDD、TDD:AI 编程时代的四层认知栈
java·人工智能