Java 平台、JDK 与工程版本基线
一份 Java 服务的构建日志里,常会同时出现下面几组版本:
java -version -> 25.0.4+7
javac -version -> 25.0.4
class major -> 61
生产 JVM -> 17.0.20+8这里没有版本冲突。JDK 25 提供编译器,编译参数把产物限制为 Java 17 的 class 格式,生产再由 JDK 17 中的 JVM 装载。真正危险的情况,是团队只记录“项目使用 Java 17”,却没有记录谁启动了构建、哪个编译器生成了 class、使用了哪些目标 API,以及生产进程最终加载了哪一套运行时。
工程版本基线要把这些事实逐项固定下来。版本号只是其中一项,发行版、补丁、操作系统与 CPU、构建 Toolchain、编译目标、测试 JVM、生产镜像、更新来源和回退条件都属于同一份基线。
Java 程序经过哪些平台对象
一段源码成为运行中的服务,至少经过下面这条链:
Java 源码
└─ 构建工具进程:Maven / Gradle 由某个 JVM 启动
└─ 编译 Toolchain:具体 JDK 中的 javac
└─ class / JAR:携带 classfile 版本并引用 API
└─ 目标运行时:具体 JDK 或运行时镜像中的 JVM
└─ 进程:加载 class、依赖、Agent 与本地库链路上还会遇到几个名称相近的对象:
| 名称 | 在工程里代表什么 | 需要记录什么 |
|---|---|---|
| Java SE | 语言、虚拟机和标准 API 的规范集合 | 目标规范版本,例如 17、21、25 |
| OpenJDK | 开源实现项目及其源码社区 | 源码项目不能代替二进制供应商身份 |
| JDK | 编译、运行、打包、诊断工具组成的发行物 | 厂商、完整补丁、OS、CPU、来源或镜像摘要 |
| JRE / runtime image | 执行应用所需的运行时集合;现代部署也常使用厂商运行包或 jlink 定制镜像 | 模块组成、供应来源、更新方式 |
| JVM | 装载并执行 class 的虚拟机实现 | VM 名称、启动参数、容器与主机约束 |
| LTS | 供应商对某一产品线作出的长期维护承诺 | 适用产品、许可、更新通道、支持期限 |
Java SE 规范索引给出语言与虚拟机契约;它不规定团队必须购买哪一种 JDK,也不替某个发行版承诺补丁周期。Oracle、Eclipse Temurin、Amazon Corretto 等发行物可以实现相同 Java SE 版本,但更新节奏、支持平台、许可和镜像构成仍需分别确认。
先从运行时读取事实
下面的操作适合装有 Docker Engine 的 Linux 开发机。使用普通登录用户执行,id -u 应返回非零值;该用户还需要通过 Docker 用户组、rootless Docker 或组织提供的受控方式访问 Docker 守护进程。容器使用宿主 UID/GID 运行,这只能约束容器进程身份,不能说明 Docker 守护进程本身以非 root 身份运行。
id -u
id -g
docker version --format 'client={{.Client.Version}} server={{.Server.Version}}'
export JDK25_IMAGE='eclipse-temurin:25.0.4_7-jdk@sha256:e787e08ef76f4c16866108cd7f9fcd96a68eef3ac6cc76866897d4d02d5a2262'
export HOST_UID="$(id -u)"
export HOST_GID="$(id -g)"
test "$HOST_UID" -ne 0 || {
echo '请切换到能够访问 Docker 的普通用户' >&2
exit 1
}
docker run --rm --user "$HOST_UID:$HOST_GID" \
"$JDK25_IMAGE" \
sh -lc 'java -version; javac -version; uname -m; \
java -XshowSettings:properties -version 2>&1 \
| grep -E "java.runtime.version|java.specification.version|java.vendor|os.arch"'关键输出形态如下。补丁号、架构与 VM 描述应以实际镜像为准:
openjdk version "25.0.4" ... LTS
OpenJDK Runtime Environment Temurin-25.0.4+7 ...
javac 25.0.4
x86_64
java.runtime.version = 25.0.4+7-LTS
java.specification.version = 25
java.vendor = Eclipse Adoptium
os.arch = amd64java.specification.version 是 Java SE 版本,java.runtime.version 是完整运行版本,java.vendor 标识供应商,os.arch 与 uname -m 分别来自 JVM 属性和容器操作系统。四项应同时进入构建或启动证据。命令若在拉取镜像前失败,先区分 registry 访问、镜像摘要变化、CPU 平台不匹配和 Docker 权限;不要改用浮动的 latest 来绕过判断。Eclipse Temurin 官方镜像页列出了镜像变体,Adoptium 发布页用于核对对应发行物。
长期基线关注迁移断层,不罗列全部版本
Java SE 26 已作为特性版本发布。下表仍集中在 8、11、17、21、25,因为它们是 Oracle 产品路线中的长期支持版本,也是存量系统最常见的工程基线;它不是 Java 已发布版本全集,更不表示应用必须按表格逐站升级。Oracle Java SE Support Roadmap列出的 LTS、许可与支持日期适用于相应 Oracle 产品和客户范围,采用其他发行版时应回到该供应商的维护政策。
| Java SE | class major | 跨版本迁移时应先检查的断层 | 常见工程定位 |
|---|---|---|---|
| 8 | 52 | 位于模块系统之前,老应用可能依赖 JDK 内部 API、随 JDK 提供的 Java EE/CORBA 组件或旧应用服务器 | 认证产品、遗留依赖暂时无法迁移时保留 |
| 11 | 55 | 跨过 Java 9 模块化;JDK 11 移除了 JAXB、JAX-WS、JAF、CORBA 等模块和相关工具 | 从 8 迁出时必须处理的兼容断层 |
| 17 | 61 | JDK 内部元素强封装,历史反射、字节码增强器和 sun.* 依赖会集中暴露 | 生态共同支持到 17 时可作为有效基线 |
| 21 | 65 | 默认 UTF-8 变化已经生效;虚拟线程正式可用,但采用它属于独立的并发设计变更 | 新系统常见候选,仍需核对完整生态 |
| 25 | 69 | Security Manager 已永久禁用;旧启动参数可能在 main 之前失败 | 已正式发布的长期候选,取决于发行版与生态交集 |
JVMS classfile 版本表定义 major 映射。跨代行为分别可在 JEP 320:移除 Java EE 与 CORBA 模块、JEP 403:强封装 JDK 内部元素、JEP 400:默认 UTF-8、JEP 444:虚拟线程和 JEP 486:永久禁用 Security Manager核对。
从 Java 8 直接迁到 21 或 25,JDK 11 和 17 引入的断层仍会作用于应用。NoClassDefFoundError: javax/xml/bind/JAXBContext 通常要求显式引入受维护的 JAXB 依赖并执行真实 XML 路径;已经删除的模块无法靠旧的 --add-modules 恢复。InaccessibleObjectException 需要定位越过模块边界的库,优先升级依赖;过渡性的 --add-opens 应绑定责任依赖和退出版本。
字符集变化更隐蔽。文件能打开,只说明 I/O 调用完成;签名原文、CSV、批处理和外部协议应显式使用契约规定的 Charset,再比较新旧运行时产生的最终字节。虚拟线程、GC、紧凑对象头和 AOT 也应作为独立变更验证,避免一次发布同时改变运行时版本和执行模型。
从共同支持集合里确定工程基线
新系统选择 Java 版本时,先列硬约束,再求共同支持集合。版本数字只有落入这个集合才有比较价值。
候选 Java SE 版本
├─ JDK 发行版:OS / CPU / libc / 基础镜像 / 加密要求
├─ 应用框架:正式支持的最低与最高 Java
├─ 构建链:Maven / Gradle / 编译、测试、打包插件
├─ 运行组件:APM Agent / 字节码增强 / JNI / 数据库驱动
├─ 维护条件:补丁来源 / 生产许可 / 支持期限
└─ 发布条件:验证成本 / 数据兼容 / 回退镜像
↓
共同支持的版本集合例如,框架和构建插件都支持 21、25,而生产必需的 APM Agent 只正式支持 21,那么候选集合暂时只有 21。这个结论来自组件组合,并不评价 25 本身的稳定性。Agent 支持 25 后,还要重新核对发行版维护条件、容器平台、必要能力和迁移成本。
基线记录至少包含这些字段:
| 范围 | 建议保存的值 |
|---|---|
| 规范目标 | Java SE 版本、允许使用的 Preview/Incubator 策略 |
| 构建 JDK | 供应商、完整补丁、OS/CPU、镜像 tag 与摘要 |
| 编译契约 | Toolchain 选择、--release、编译/测试插件版本、依赖最低 Java |
| 运行 JDK | 生产镜像摘要、运行模块、启动参数、Agent 与本地库 |
| 维护 | 补丁来源、许可适用范围、升级负责人、退出条件 |
| 验证与回退 | 最低 JVM 测试、性能基线、旧镜像、数据与消息向后兼容条件 |
补丁升级会生成新的版本身份。持续使用 21、25 或 latest 等浮动标签,意味着相同 Git 提交可能在不同时间得到不同的工具或运行时。受控更新通常采用“固定摘要 → 验证新摘要 → 更新基线”的顺序。
--release、Toolchain 与运行 JVM 各管一层
公开示例包含 RuntimeBaseline.java 和 ApiLeak.java。前者打印运行时身份;后者使用 Thread.threadId(),语法可由 Java 17 编译,但该方法在 Java 17 标准 API 中不存在。
用两个容器复现 class 与 API 边界
继续在安装 Docker 的 Linux 开发机上,以普通用户从仓库根目录执行。源码只读挂载,编译输出写入当前用户拥有的临时目录。退出 shell 时 trap 会清理该目录;需要保留诊断文件时,先复制到项目约定的制品目录,再取消清理。
export LAB_SRC="$PWD/docs/.vuepress/public/examples/backend-development/java-version-baseline"
export LAB_OUT="$(mktemp -d)" || exit 1
export HOST_UID="$(id -u)"
export HOST_GID="$(id -g)"
export JDK25_IMAGE='eclipse-temurin:25.0.4_7-jdk@sha256:e787e08ef76f4c16866108cd7f9fcd96a68eef3ac6cc76866897d4d02d5a2262'
export JDK17_IMAGE='eclipse-temurin:17.0.20_8-jdk@sha256:a27c79d44326d5f689668df5fedfee487652066d2a91e172747056cc7fbee6fc'
trap 'rm -rf -- "$LAB_OUT"' EXIT
test "$HOST_UID" -ne 0 || exit 1
test -r "$LAB_SRC/RuntimeBaseline.java" || exit 1
test -r "$LAB_SRC/ApiLeak.java" || exit 1
mkdir -p "$LAB_OUT"/{major69,major61,source-target,release} || exit 1
run_jdk25() {
docker run --rm --user "$HOST_UID:$HOST_GID" \
--mount "type=bind,src=$LAB_SRC,dst=/src,readonly" \
--mount "type=bind,src=$LAB_OUT,dst=/out" \
"$JDK25_IMAGE" "$@"
}
run_jdk17() {
docker run --rm --user "$HOST_UID:$HOST_GID" \
--mount "type=bind,src=$LAB_OUT,dst=/out,readonly" \
"$JDK17_IMAGE" "$@"
}第一轮使用 JDK 25 默认目标编译,再让 JDK 17 装载:
run_jdk25 sh -lc '
set -eu
javac -d /out/major69 /src/RuntimeBaseline.java
javap -verbose /out/major69/RuntimeBaseline.class | grep "major version"
'
if run_jdk17 java -cp /out/major69 RuntimeBaseline \
2>"$LAB_OUT/major69.err"; then
echo '预期 JDK 17 拒绝 major 69,但程序运行成功' >&2
exit 1
fi
grep 'UnsupportedClassVersionError' "$LAB_OUT/major69.err" || exit 1
grep '69.0' "$LAB_OUT/major69.err" || exit 1
grep '61.0' "$LAB_OUT/major69.err" || exit 1输出中的 major version: 69 表示 class 要求 Java 25。JDK 17 随后报告它最多识别 61.0。若没有得到这个错误,先检查两个函数引用的镜像、输出目录是否复用了旧 class,以及实际加载的类路径。
第二轮限制产物为 Java 17:
run_jdk25 sh -lc '
set -eu
javac --release 17 -Xlint:all -Werror \
-d /out/major61 /src/RuntimeBaseline.java
javap -verbose /out/major61/RuntimeBaseline.class | grep "major version"
'
run_jdk17 java -cp /out/major61 RuntimeBaseline关键结果是 major version: 61,以及 JDK 17 成功输出 spec=17 runtime=17.0.20+8 vendor=Eclipse Adoptium ...。javac 工具规范说明,--release 17 会同时选择 Java 17 支持的语言规则、标准 API 和 class 格式。
单独组合 -source 17 -target 17 只会限制语法与 class 格式。下面的编译能够生成 major 61,却把 Java 17 没有的 threadId() 写入了调用点:
run_jdk25 javac -source 17 -target 17 \
-d /out/source-target /src/ApiLeak.java
if run_jdk17 java -cp /out/source-target ApiLeak \
2>"$LAB_OUT/api-leak.err"; then
echo '预期 Java 17 触发 NoSuchMethodError,但程序运行成功' >&2
exit 1
fi
grep 'NoSuchMethodError.*threadId' "$LAB_OUT/api-leak.err" || exit 1
if run_jdk25 javac --release 17 \
-d /out/release /src/ApiLeak.java \
2>"$LAB_OUT/release.err"; then
echo '预期 --release 17 在编译期拒绝 threadId()' >&2
exit 1
fi
grep 'threadId' "$LAB_OUT/release.err" || exit 1
test ! -f "$LAB_OUT/release/ApiLeak.class" || exit 1这组结果把两个问题分开了:class 格式决定旧 JVM 能否解析文件,目标 API 决定链接到调用点时能否找到成员。--release 能控制 JDK 标准 API,无法降低第三方依赖的 class 版本,也不会验证 Agent、JNI 或厂商特有行为。
Multi-Release JAR 还包含运行时视图。JEP 238允许在 META-INF/versions/N 中为较新运行时提供 class。核验时应同时检查根目录、版本目录、Multi-Release: true Manifest 和所有承诺运行时;“JAR 中最高 major”无法直接说明它的最低运行版本。classfile 与类加载细节继续见class 文件、字节码与类加载。
构建工具 JVM 与 Toolchain
在包含 Maven Wrapper 或 Gradle Wrapper 的项目根目录,以普通用户执行下面的观察命令。Wrapper 固定构建工具版本;它仍需要一套 JVM 来启动,任务实际使用的编译器又可能来自另一套 Toolchain。
test -x ./mvnw && ./mvnw -v
./mvnw -q \
org.apache.maven.plugins:maven-toolchains-plugin:3.3.0:display-discovered-jdk-toolchains
test -x ./gradlew && ./gradlew --version
./gradlew -q javaToolchains./mvnw -v 和 ./gradlew --version 中的 Java/JVM 行表示构建工具启动身份;Toolchain 输出应另外列出候选 JDK 的版本、供应商、架构和安装位置。Wrapper 缺失说明项目还没有固定构建入口,应先按团队构建规范补齐;Toolchain 列表为空时,检查 CI 镜像是否安装了候选 JDK、自动探测路径与显式配置,不要让流水线静默退回启动 JVM。
下面两段配置表达同一个目标:编译工具使用 Java 25,公开产物只使用 Java 17 的语言、class 格式与标准 API。版本值应取自团队基线,不从开发机自动生成。
<!-- pom.xml -->
<properties>
<maven.compiler.release>17</maven.compiler.release>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-toolchains-plugin</artifactId>
<version>3.3.0</version>
<executions>
<execution>
<goals><goal>select-jdk-toolchain</goal></goals>
</execution>
</executions>
<configuration>
<version>25</version>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.16.0</version>
</plugin>
</plugins>
</build>// build.gradle
java {
toolchain {
languageVersion = JavaLanguageVersion.of(25)
}
}
tasks.withType(JavaCompile).configureEach {
options.release = 17
}Maven Toolchains 指南说明,感知 Toolchain 的编译、测试、Javadoc 等插件可以使用选中的 JDK;Maven 进程本身以及不感知 Toolchain 的插件仍可能使用启动 JVM。Maven Compiler Plugin 的 --release 示例给出了属性与插件配置方式。Gradle Java Toolchains 文档也区分运行 Gradle 的 JVM、编译器、测试 Launcher 与 Javadoc 工具。
因此,JAVA_HOME、IDE Project SDK 或一次 mvn -v 都不足以证明最终产物。构建日志还要保存编译 executable、编译/测试插件版本、测试实际 Launcher、--release 和最低运行 JVM 上的测试结果。Spring Boot 的构建插件与版本迁移见测试、打包与升级。
存量升级按可退回的阶段推进
Oracle JDK 迁移指南建议先让现有应用运行在新 JDK 上,再更新第三方库、重新编译并使用 jdeps 检查依赖。这个顺序能把运行时变化与源码变化拆开。
| 阶段 | 本轮改变的主要对象 | 需要留下的结果 | 可用退回点 |
|---|---|---|---|
| 保存旧基线 | 不改代码,收集真实构建与运行身份 | JDK、镜像、启动参数、Agent、依赖锁和旧测试可复现 | 当前生产制品 |
| 旧制品运行在新 JDK | 先换运行时,不重新编译 | 启动与关键路径通过;模块、字符集、TLS、Agent 行为可解释 | 旧运行镜像 |
| 更新依赖与 Agent | 替换不支持目标 JDK 的组件 | jdeps --jdk-internals、增强、驱动和低频路径通过 | 上一组依赖锁 |
| 固定 Toolchain 重编译 | 固定编译与测试 JDK、--release | 源码、依赖 class、序列化与协议兼容 | 旧制品和旧构建镜像 |
| 相同负载比较 | 发布单一 JDK 迁移候选 | 业务结果、错误率、延迟、CPU、RSS、GC、连接池与 Agent 达标 | 旧镜像、旧配置、旧依赖仓库 |
依赖更新和源码修改往往需要多轮迭代。每轮都保存依赖锁、构建镜像、产物摘要和测试结果,失败才能落到具体变量。源码兼容、二进制兼容、运行时兼容与行为兼容也要分别成立:编译通过只覆盖源码的一部分,健康检查只触发少量类和业务分支,反射、Agent、JNI、序列化、日期字节、证书与批处理需要在目标运行时真正执行。
JDK 迁移候选不宜同时启用虚拟线程、新 GC、紧凑对象头或 AOT,也不夹带破坏性的数据库 Schema、消息和序列化变化。GC 与参数进入收集器、日志与选择,进程问题进入JVM 诊断,同流量比较进入性能基线与回归。
灰度前还要验证旧实例能否读取新实例产生的数据、消息和序列化格式。旧镜像、旧配置与旧依赖仓库保持可取得,回退才是一条真实路径。环境晋级见开发、测试、预发与生产环境边界,跨服务灰度与故障传播见链路上下文、灰度与故障传播。
按错误发生的层次处理版本故障
UnsupportedClassVersionError 出现在 class 装载阶段。先读取错误里的“产物版本”和“当前 JVM 上限”,再用完整 JDK 对具体 class 执行 javap -verbose。精简生产镜像没有诊断工具时,把同一失败制品复制到受控诊断环境,不在运行容器中临时安装软件:
javap -verbose -classpath app.jar com.example.Failing \
| grep -E 'major version|minor version'生产基线为 17 时,修复点通常在构建 Toolchain、--release 17、依赖 class 或 MR-JAR 视图。生产已经批准 25 而进程仍报告只支持 major 61,则应检查部署镜像、入口脚本与调度平台。临时下载另一套 java 覆盖 PATH 会破坏基线证据,后续发布还会复发。
| 现象 | 第一组证据 | 处理方向 |
|---|---|---|
UnsupportedClassVersionError | 错误中的 major、javap -verbose、生产 java -version | 对齐 Toolchain、--release、依赖 class 与运行镜像 |
NoSuchMethodError / AbstractMethodError | 实际 classpath、依赖锁、调用点 API、最低 JVM | 排除 API 泄漏或运行时依赖漂移,并执行真实分支 |
JAXB/JAX-WS 的 NoClassDefFoundError | 依赖树、JDK 11 移除项、XML/SOAP 路径 | 显式引入受维护依赖,重新验证编解码 |
InaccessibleObjectException 或 Agent 失败 | jdeps --jdk-internals、模块开放参数、Agent 支持矩阵 | 升级越界依赖;过渡开放参数绑定退出版本 |
| 文件、签名或排序结果变化 | 新旧最终字节、Charset、Locale、ZoneId | 固定协议语义,用脱敏样本比较 |
JVM 在 main 前拒绝 Security Manager | 原启动参数、目标 JDK、旧沙箱责任 | 迁到进程/容器隔离与应用授权后再升级 |
修复完成后,用最低受支持 JVM 重跑启动、集成测试和关键业务路径,并让流水线比较构建 JVM、Toolchain、测试 Launcher 与生产运行时。故障解决记录应能回答四个问题:哪一个 class 或组件超出了基线,谁生成或引入了它,哪条验证在发布前拦截同类问题,以及旧制品与数据能否继续被回退版本读取。
权威资料与规范地址
下面的地址分别用于核对规范、发行版、迁移断层和构建工具行为。支持期限、许可与最新补丁具有供应商和产品范围,采用前应以所选发行版的官方页面为准。
Java 平台、发行与支持
| 资料 | 用途 |
|---|---|
| Java SE 规范索引 | 语言、虚拟机与标准 API 规范入口 |
| Oracle Java SE Support Roadmap | Oracle 产品线的 LTS、许可与支持范围 |
| Adoptium Temurin 发布页 | Temurin 发行物与平台选择 |
| Eclipse Temurin 官方容器镜像 | Temurin 容器标签、变体与用法 |
class、编译与跨版本变化
| 资料 | 用途 |
|---|---|
| JVMS:classfile 版本表 | Java 版本与 class major 映射 |
javac 工具规范 | --release、-source 与 -target 语义 |
| JEP 238:Multi-Release JAR | JAR 的多运行时版本视图 |
| JEP 320:移除 Java EE 与 CORBA 模块 | JDK 11 模块移除项 |
| JEP 403:强封装 JDK 内部元素 | JDK 内部 API 的强封装变化 |
| JEP 400:默认 UTF-8 | 默认字符集变化 |
| JEP 444:虚拟线程 | 虚拟线程正式特性说明 |
| JEP 486:永久禁用 Security Manager | Security Manager 的目标版本行为 |
Toolchain 与迁移
| 资料 | 用途 |
|---|---|
| Maven Toolchains 指南 | Maven 启动 JVM 与插件 Toolchain 的关系 |
Maven Compiler Plugin 的 --release 示例 | Maven 编译目标的属性与插件配置 |
| Gradle Java Toolchains | Gradle 的编译器、测试 Launcher 与工具选择 |
| Oracle JDK 迁移准备指南 | 存量应用运行、依赖更新、重编译与 jdeps 顺序 |
