JDK 8到21迁移指南:参数变化、GC演进与虚拟线程实战
前言
手上一批老服务还跑在 JDK 8 上,Oracle 的免费更新早就停了,趁着最近有空把几个服务迁到 21 试了试,过程不算复杂,但坑主要在两个地方:老参数会直接启动失败,还有依赖库的字节码兼容。把迁移过程和注意事项记录下,方便后来者。
站内 tomcat-jvm 那篇有 JDK21 的参数对照,oom 那篇有 OOM 新旧形态对照,这篇侧重怎么迁。
1、先清掉会启动失败的参数
升级前把启动脚本全量搜一遍,下面这些参数在新版是硬错误,带着它们直接起不来:
| 参数 | 状态 | 处理 |
|---|---|---|
-XX:PermSize / -XX:MaxPermSize |
JDK 8 就移除了,启动报错 | 删掉;要限制元空间改 -XX:MaxMetaspaceSize=256m |
-XX:+UseConcMarkSweepGC |
JDK 14 移除,启动报错 | 删掉,用默认 G1;或者换 -XX:+UseZGC |
-XX:+UseParNewGC |
跟着 CMS 一起退役了 | 删掉 |
-XX:+UseBiasedLocking |
JDK 15 弃用 | 删掉,偏向锁已经从 JVM 里移除 |
-XX:+UseCompressedOops 手工声明 |
没意义了 | 删掉 |
-verbose:gc -Xloggc:xxx |
JDK 9 起被统一日志接管 | 改 -Xlog:gc*:file=gc.log:time:filecount=5,filesize=50m |
-XX:+PrintGCDetails 等 Print 系列 |
并进统一日志了 | -Xlog:gc* 的标签和修饰符替代 |
一条命令把生产脚本里的过时参数全搜出来:
|
|
2、GC 怎么选
JDK 8 时代拼 ParNew + CMS 是在吞吐和停顿之间手工找平衡,现在简单了:
| 你的现状 | 迁移动作 |
|---|---|
| ParNew + CMS 加一堆新生代调参 | 全删,用默认 G1;压测后按 P99 目标加 -XX:MaxGCPauseMillis=200 |
| 大堆(16G 以上)又对停顿敏感 | 分代 ZGC:-XX:+UseZGC -XX:+ZGenerational,停顿亚毫秒级,堆再大也不涨 |
| Parallel GC 跑批 | 保留 -XX:+UseParallelGC,论吞吐它还是最强的 |
| 堆大于 1G 手工分一半给新生代的老经验 | 作废,G1/ZGC 自己管,-Xmn、SurvivorRatio 别再带了 |
我的建议是先什么都不配跑默认 G1 看数据,不够再调。老版本那一套手工调参经验,在新的收集器上基本都是噪音。
3、依赖兼容才是真正的坑
JVM 本身向后兼容做得不错,升级翻车基本都翻在依赖上:
- ASM/CGLIB 版本:JDK 21 的类文件版本是 65,老版本 ASM/CGLIB 碰到新字节码直接
UnsupportedClassVersionError。Spring Boot 3.2+ 和 Hibernate 6+ 内置的新字节码库没问题;还停在 Spring 5.x + 老 CGLIB 的得先把框架升上去 - JDK 内部 API:
sun.misc.Unsafe、sun.misc.BASE64Encoder这些内部 API 在 JDK 16+ 强封装了,--add-opens只能救急不是长久之计。用jdeps --jdk-internals app.jar一次扫出所有引用 - 移除的模块:JAXB/JAX-WS/activation(JDK 11 移除)、Nashorn(JDK 15 移除)、SecurityManager(JDK 17 弃用、24 移除)——有引用的补依赖或者换实现
|
|
4、虚拟线程
虚拟线程是 JDK 21 最值得用的东西。
|
|
用的时候注意几点:
- 不要池化虚拟线程,它便宜到用完就扔,池化反而出问题
- pinning 坑:synchronized 块里阻塞会把虚拟线程钉在载体线程上卸载不掉(JDK 21 的问题,JDK 24 的 JEP 491 才解除)。IO 阻塞的临界区改用
ReentrantLock,或者加-Djdk.tracePinnedThreads=full排查哪里被钉住了 - ThreadLocal 慎用:几十万虚拟线程每个一份副本,内存放大得厉害,跨层传上下文后面走 Scoped Values
- 限流别再用线程池大小,虚拟线程下改
Semaphore
5、迁移步骤
我是按这个顺序走的,一次只动一个变量:
- CI 上先加一条 JDK 21 的构建流水,保证能编译、测试是绿的,先不发布
- 挑个无状态、依赖简单的服务先切 21,观察 GC 日志和 P99 一到两周
- 按第 1 节的清单清参数,GC 一律先用默认 G1,老参数别翻译着带过去
- 没问题再按依赖复杂度从低到高滚动升级,保留快速回退 8 的能力
- 全量上到 21 之后,再逐个服务试点虚拟线程。先升级、后改造,别一步到位
总结
迁移的活儿就集中在两块:清参数(第二节清单,半小时的事)和核对依赖(内部 API 和字节码库,这才是大头)。GC 先用默认 G1 起步,别把 8 时代的调参经验翻译过去;升完之后虚拟线程是最大的红利,记得先把 synchronized 里的阻塞段换成 ReentrantLock。
- 原文作者:Anttu
- 原文链接:https://anTtutu.github.io/post/2023-11-08-jdk8-to-21-migration-guide/
- 版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可,非商业转载请注明出处(作者,原文链接),商业转载请联系作者获得授权。