前言

从 JDK 8 升级到 JDK 17 或 21 是一次覆盖编译、依赖、容器、运行参数和性能基线的生产变更。本文以兼容性审计、灰度验证和回滚准备为主线,说明如何在利用新特性的同时控制升级风险。

升级 JDK 是一次生产变更

本文范围与延伸阅读

本文的重点是升级实施,包括兼容性审计、容器与 GC 验证、灰度和回滚,不重复解释每个语言特性或收集器原理。语言特性的整体对比见Java基础:从语法、集合、并发到JDK 8/17/21新特性的系统梳理,JVM 内存和 GC 原理见JVM:从内存结构、类加载、GC到JDK 8/17/21演进的系统梳理

JDK 8 到 17/21 的升级不只是把镜像基础版本替换掉。它涉及编译目标、第三方依赖、反射与内部 API、字节码 agent、TLS、安全策略、GC、容器资源识别、启动参数和性能基线。正确方式是先完成兼容性审计,再分环境灰度验证,最后逐步扩容,而不是线上报错后不断增加 --add-opens

版本 面试应掌握的代表能力 升级关注点
JDK 8 Lambda、Stream、Optional、默认方法、CompletableFuture、Metaspace 仍常见,但后续安全/性能支持策略要评估
JDK 17 LTS record、sealed class、文本块、switch 表达式、强封装、G1 改进 反射访问 JDK 内部、旧框架/agent 兼容
JDK 21 LTS 虚拟线程、record pattern、pattern matching for switch、结构化并发为预览 阻塞模型、pinning、库/监控兼容和压测

语言特性不应牺牲可维护性

record 适合不可变 DTO、命令和查询结果,不适合需要 JPA 代理、可变生命周期或复杂继承的实体。sealed class 可约束状态机或支付方式等有限类型集合,让 switch 覆盖更完整。Stream 适合清晰的集合转换,不适合隐藏 IO、远程调用、复杂异常处理或超大集合的无界 materialize。

虚拟线程降低“每个阻塞任务一个平台线程”的成本,适合大量独立、阻塞式 IO,例如高并发 HTTP 聚合。但它不会增加数据库连接、外部 API 配额或 CPU;并发必须仍受连接池、信号量、限流和超时约束。synchronized 长时间持有、native 调用等可能导致 pinning,应通过 JFR/监控和压测验证。

兼容性审计清单

  1. 固定 JDK 发行版和补丁版本,构建环境、CI、运行镜像保持一致。
  2. 升级 Spring Boot、Spring Framework、Hibernate、Netty、Lombok、Mockito、Byte Buddy、监控 Agent 等对 JDK 敏感依赖。
  3. 使用 jdeps、依赖树和启动日志排查 JDK 内部 API、非法反射和重复 jar。
  4. 检查 --add-opens--add-exports-XX 参数,删除已废弃参数,临时绕过必须有移除计划。
  5. 复核 TLS、证书、加密算法、默认 charset、时区、Locale、DNS 和代理配置。
  6. 复核 Docker/Kubernetes 下的 -Xmx、CPU quota、内存限制、OOM 策略和优雅停机。

典型问题是编译通过、启动失败:JDK 17+ 强封装使旧依赖反射访问 JDK 私有包报 InaccessibleObjectException;或者运行时因 transitive dependency 降级而出现 NoSuchMethodError。优先升级依赖,不要把所有问题归咎于 JDK。

GC、容器和性能验证

JDK 8 常见 CMS 已在后续版本移除,G1 是现代 LTS 中常用默认收集器;JDK 17/21 还可根据延迟目标评估 ZGC。GC 选型先看堆大小、分配速率、P99 停顿和 CPU 成本,再压测验证,不要仅因“新 GC 更先进”直接切换。

容器环境要避免 -Xmx 接近 cgroup memory limit。堆之外还有 Metaspace、Direct Memory、线程栈、Code Cache、JNI/Native、文件缓存等;虚拟线程虽降低栈内存压力,也可能让下游阻塞任务数量大幅上升。监控 RSS、OOMKilled、GC、线程数、连接池和任务队列,而不是只看 Java Heap。

灰度路径

1
2
3
4
5
6
7
依赖与参数审计
-> 单测/集成测试(目标 JDK)
-> 基准与压测(旧新版本对比)
-> 预发长稳与故障注入
-> 小流量灰度
-> 指标对比与逐步扩容
-> 保留快速回滚镜像

对比指标包括启动耗时、P50/P99、错误率、CPU、RSS、GC pause/频率、连接池等待、线程池拒绝和业务成功率。升级中若出现回归,先收集 JFR、GC 日志、线程 dump 和依赖版本证据,再决定调参数、升级库或回滚,不要只凭感觉修改 JVM flag。

高频面试题

问题 回答要点
为什么升级到 JDK 17 会有反射异常? JPMS 强封装限制访问 JDK 内部包;优先升级依赖,--add-opens 仅作临时过渡。
虚拟线程能替代线程池吗? 不能替代下游限流、连接池和 CPU 隔离;适合大量阻塞 IO,仍需控制并发和超时。
JDK 升级如何验证? 目标 JDK 下跑测试、压测、预发长稳、小流量灰度,对比延迟、GC、RSS、错误和业务指标。
为什么容器内 Xmx 不能设为 limit? JVM 还有堆外内存、Metaspace、线程栈等,容易被 cgroup OOMKilled。
record 适合 Entity 吗? 通常适合不可变 DTO/值对象;JPA Entity 往往需要代理、无参构造和可变状态,需谨慎。