Java 启动优化
Java 启动优化通过复用类信息或提前编译,减少进程开始服务前的工作。本文以 Java 17、21 为范围,先说明 CDS 共享归档,再比较 JIT 与 Native Image。最后解释动态特性的适配要求和构建期初始化的影响。
1. 面试核心
先记住这几条,再读正文- CDS 归档复用。 CDS 将预处理类信息保存为共享归档,HotSpot 启动时复用它,同机 JVM 还可以共享只读页面。
- JIT 与 AOT。 JIT 根据本次运行的类型和分支信息编译热点代码,AOT 则在运行前生成机器码。卡 · CDS
- Native Image 场景。 Native Image 在构建时分析可达代码并生成原生程序,适合重视冷启动和初始内存的短生命周期应用。卡 · GraalVM Native Image
- 动态访问声明。 Native Image 的动态访问目标需要在构建时被静态分析发现,或通过元数据明确声明。
- 初始化时机。 Native Image 构建期初始化产生的静态状态会进入产物,环境相关配置应留到运行期读取。卡 · 构建期初始化
2. CDS
多个 JVM 启动时,会反复处理同一组基础类。CDS 是 Class Data Sharing,即类数据共享。它把预处理后的类信息写入共享归档,让 HotSpot 在启动时通过内存映射复用。
这样可以减少重复的类解析工作,同机多个 JVM 还能共享只读页面,节省内存。Oracle JDK 17、21 自带默认 CDS 归档,其中包含一组核心类。
Application CDS,也称 AppCDS,可以把应用类纳入归档。Java 17、21 支持在应用退出时生成动态归档,再供后续启动使用:
java -XX:ArchiveClassesAtExit=app.jsa -cp app.jar com.example.Main
java -XX:SharedArchiveFile=app.jsa -Xlog:cds=info -cp app.jar com.example.Main
第一条命令运行应用,并在正常退出时生成 app.jsa。第二条命令加载这份归档,-Xlog:cds=info 用来输出实际加载情况。生成归档会实际运行应用,应使用隔离的测试环境。动态归档记录本次运行加载的应用类,因此还要覆盖真实启动过程。
应用仍由完整的 HotSpot JVM 运行,保留动态类加载、反射和 JIT。CDS 的收益取决于应用规模,以及启动时花在类处理上的时间。长期服务若很少再加载类,稳定吞吐的变化就需要单独测量。
3. CDS 归档兼容性
CDS 归档与生成它的应用和 JDK 配套,发布时需要一起管理。生成与加载归档应使用相同的 JDK 构建,并保持操作系统和 CPU 架构兼容。应用 JAR、类路径或影响归档的 JVM 选项改变后,应重新生成归档。
默认的 -Xshare:auto 会在归档可用时加载它,失效时仍允许应用启动。因此,应用成功启动还需要配合日志确认归档是否生效。测试归档可用性时可使用 -Xshare:on,加载失败会让 JVM 退出。
4. CDS 与 JIT
CDS 和 JIT 处理的是不同阶段的工作,因此可以一起使用。CDS 减少类信息的重复处理,JIT 则在程序运行时编译热点代码。JIT 是即时编译,热点指执行频繁、值得优化的代码。
JIT 会收集本次运行的类型和分支信息,这些信息称为运行画像。它据此生成更适合当前负载的机器码。优化假设失效后,可以退出对应的优化代码,回到解释执行,这称为去优化。之后再按新的运行信息重新编译。
这让服务在减少类处理开销的同时,继续适应真实负载。代价是保留启动后的预热和编译开销,并增加归档生成、存储与兼容验证的工作。
5. GraalVM Native Image
GraalVM Native Image 把代码编译提前到构建阶段,这属于 AOT,也就是提前编译。它从程序入口追踪能够调用到的代码,再把这些代码和所需运行时编入原生可执行文件:
native-image -jar app.jar
生成的产物以独立原生程序启动,自带所需的内存管理机制。因为一部分运行准备已经在构建时完成,它可以减少冷启动工作和初始内存占用。这里的冷启动指新进程从启动到开始服务的过程。
这些收益适合以下短生命周期或频繁启动的应用:
- CLI,也就是命令行工具,常在完成一次任务后退出。
- 函数计算会频繁创建新实例,启动时间会影响请求延迟。
- 按需伸缩服务需要快速增加实例,首批请求的体验尤其重要。
迁移后,构建时间、产物体积和调试方式都会变化。稳定吞吐也要用目标负载测量,再判断启动收益是否值得这些成本。
6. 闭世界假设
Native Image 构建时需要确定运行所需的全部代码与资源,这称为闭世界假设。静态分析只能保留它能发现的目标,动态访问若无法被推断,就需要补充元数据。元数据在这里指声明运行时要访问哪些类、成员或资源的配置。
迁移时,要检查以下动态访问目标:
- 反射和按名称加载的类,需要明确实际访问哪些类与成员。
- 动态代理需要明确代理接口,JNI 则需要声明本地代码访问的 Java 元素。JNI 是 Java 与本地代码交互的接口。
- 序列化需要声明运行时要恢复的对象类型。
- 资源文件需要纳入构建产物,例如配置文件和模板。
GraalVM for JDK 21 使用 reflect-config.json 声明反射访问:
[
{
"name": "com.example.PaymentHandler",
"allDeclaredConstructors": true
}
]
示例声明 PaymentHandler 的全部已声明构造器可通过反射调用。构建时,用 -H:ReflectionConfigurationFiles=reflect-config.json 加载这份配置。
框架可以生成一部分配置,Tracing Agent 则在应用运行时记录发生过的动态访问。它记录的是测试实际走过的分支,未执行的分支仍需补充测试或配置。否则,缺失的反射目标会在部署后第一次使用时才暴露问题。
任意新字节码的运行期加载超出闭世界边界。因此,自定义类扫描、插件、运行时字节码生成和第三方库的动态行为,都要在迁移时逐项验证。
7. 构建期初始化
Native Image 可以在构建时执行部分类的初始化,减少启动时的工作。初始化后保留下来的对象会进入 image heap,也就是随原生程序保存的初始堆对象。静态字段若在这时读取环境变量,产物就会保留构建机上的结果。
检查初始化代码时,应特别关注以下环境相关状态:
- 时区和环境变量会随部署环境改变。
- 证书内容和机器路径可能只在构建机上有效。
- 随机数若在构建时生成并作为普通数值保存,多次运行就会复用同一个结果。
这些类应根据实际需求放到运行时初始化。例如,下面将 EnvironmentConfig 延后到程序运行时:
native-image --initialize-at-run-time=com.example.EnvironmentConfig -jar app.jar
这样,类中的环境读取会发生在部署机器上,而不是在构建时完成。纯计算且结果与环境无关的类,可以评估 --initialize-at-build-time。
配置时要写明类名或包范围,确认哪些类提前初始化、哪些类延后执行。部署前应在不同环境中运行同一产物,改变配置值,验证程序读取的是当前运行环境。
8. 三种方案的比较
选择方案时,先看应用更重视启动速度还是长期吞吐,再比较部署方式和适配成本:
| 方案 | 运行形态 | 主要收益 | 额外成本 |
|---|---|---|---|
| HotSpot JIT | 运行时编译热点代码。 | 根据运行画像优化。 | 启动后的预热与编译。 |
| CDS 与 JIT | HotSpot 与共享归档。 | 减少类处理,共享只读页面。 | 归档兼容性验证。 |
| Native Image | 独立原生程序。 | 减少冷启动工作。 | 闭世界适配与构建。 |
性能验证要使用目标框架和真实请求,并分别记录以下运行与交付指标:
- 测量进程启动到 ready 的时间,也就是服务能够接收请求前的等待时间。
- 测量首批请求耗时,观察初始化与预热对它们的影响。
- 比较稳定吞吐与 P99 延迟,后者表示 99% 的请求耗时不超过的值。
- 记录 RSS,也就是进程驻留在物理内存中的页面总量。
- 比较构建耗时、产物体积与发布复杂度,判断交付成本是否可接受。
- 验证日志、性能分析和故障诊断方式,确认遇到问题时能取得所需证据。
简单程序的启动时间只能说明很小一段执行过程,最终选择要以真实应用的结果为准。
9. 常见问题
启动时间、动态能力和归档兼容性需要分别判断,下面说明几个容易混淆的地方。
9.1 AOT 一定比 JIT 快吗?
两者的收益取决于应用运行多久,以及负载是否稳定。AOT 提前编译,减少运行时准备;JIT 则利用本次真实的类型和分支信息进行优化。短任务更重视启动,长期服务还要比较稳定吞吐。Java 17、21 可以用 CDS 减少类处理,同时保留 JIT 的运行期优化。
9.2 Native Image 支持反射吗?
Native Image 支持构建时已知的反射目标。目标可以由静态分析识别,也可以通过元数据声明。若目标类或成员到运行时才从任意外部配置中确定,就需要先明确所有允许的目标,再纳入构建。
9.3 CDS 归档能跨 JDK 版本复用吗?
CDS 归档需要与生成时的 JDK 构建匹配,应随应用和运行环境一起管理。应用、JDK、操作系统、CPU 架构或关键选项变化后,都要重新验证兼容性并按需生成归档。部署时用 -Xlog:cds=info 确认实际加载结果。
10 · 面试题卡片
10.1 JIT 和 AOT 有什么区别
出现公司:去哪儿、淘天、拼多多
考察重点:解释编译发生的时机,以及运行画像如何影响启动和稳定性能。区分类信息归档与原生编译产物。
参考回答
JIT 在程序运行时编译热点代码,AOT 在运行前编译代码,两者利用的信息和承担成本的时机不同。
JIT 能收集真实的执行热度、类型和分支信息,再据此优化,代价是预热时间与运行期编译资源。AOT 把编译工作提前到构建阶段,可以减少启动准备,但静态分析未必能代表本次负载。
Java 17、21 的 CDS 归档的是类信息,热点机器码仍由 JIT 生成。Native Image 则通过闭世界分析生成原生程序。它使用的反射目标、资源和其他动态访问,需要在构建时被发现或声明。
10.2 什么应用适合 Native Image
出现公司:淘天
考察重点:根据进程生命周期和扩缩容要求判断启动收益,并评估动态特性、第三方库与诊断能力的适配成本。
相关内容:GraalVM Native Image至三种方案的比较。
参考回答
CLI、函数计算、频繁冷启动或快速扩缩容的服务适合评估 Native Image,因为启动与初始内存占比较高。
如果应用依赖运行时插件、动态生成类或难以枚举的反射目标,就要投入更多适配工作。第三方库也需要验证,确认这些动态访问能在构建时声明。
选择时应使用目标框架和真实请求,测量启动到 ready、首批请求和稳定吞吐。还要比较 P99、RSS、构建与发布成本,并确认故障诊断能力满足需求。
10.3 Native Image 为何会读到构建机的配置,怎样处理?
出现公司:暂无已核验的公开来源(本站自测)
考察重点:解释构建期读取的配置如何进入产物,以及怎样把环境相关类延后到运行期初始化。
相关内容:构建期初始化。
参考回答
类在构建期初始化时读取了环境配置,结果随静态状态进入产物,因此运行时仍读到构建机的值。
可以用 --initialize-at-run-time=com.example.EnvironmentConfig 延后这类初始化。这样,环境变量和路径会在程序实际运行时读取。随后用同一产物在不同环境中运行,改变配置值,确认读取结果随运行环境变化。