本文从「JDK 是什么」讲到「Maven 如何调用编译器」,把 Java 的编译、运行、版本关系和 Maven 的工作原理串成一条完整的链路。适合想彻底搞懂构建底层机制的开发者。
目录
- [JDK、JRE、javac、java 到底是什么](#JDK、JRE、javac、java 到底是什么)
- [Java 的两步:编译与运行](#Java 的两步:编译与运行)
- [核心:字节码版本与 JVM 向下兼容](#核心:字节码版本与 JVM 向下兼容)
- [控制编译版本:source/target 与 release](#控制编译版本:source/target 与 release)
- [Maven 的本质:一个调用工具链的 Java 进程](#Maven 的本质:一个调用工具链的 Java 进程)
- [版本一致性:为什么 Maven 的 JDK 要和项目对齐](#版本一致性:为什么 Maven 的 JDK 要和项目对齐)
- 常见错误速查
- [一图总览 & 核心结论](#一图总览 & 核心结论)
一、JDK、JRE、javac、java 到底是什么
这是所有问题的地基,先分清楚三个东西的包含关系:
JDK(Java Development Kit,开发工具包)
├── javac ← 编译器:.java → .class
├── jar ← 打包工具:.class + 资源 → .jar
├── javadoc ← 文档生成工具
└── JRE(Java Runtime Environment,运行环境)
└── java(JVM) ← 运行 .class
| 名称 | 是什么 | 能编译吗 | 能运行吗 |
|---|---|---|---|
| JDK | 开发全套:编译工具 + 运行环境 | ✅ 有 javac |
✅ 有 JVM |
| JRE | 只含运行环境 | ❌ 没有 javac |
✅ 有 JVM |
| javac | 编译器,属于 JDK | ------ | ------ |
| java | JVM 启动器,运行字节码 | ------ | ------ |
关键认知:
javac是 JDK 的一部分 ,所以javac的版本就等于 JDK 的版本。装 JDK 8,里面就是 JDK 8 的javac;装 JDK 17,就是 JDK 17 的javac。- 一台机器可以同时安装多个 JDK ,于是会有多个
javac并存。 - JRE 里没有
javac,所以纯 JRE 只能运行、不能编译。
二、Java 的两步:编译与运行
Java 是「先编译、再运行」的语言,两步用不同的工具:
你写的 编译器 产物 运行
Hello.java ──javac──► Hello.class ──java──► 程序运行
(源代码) (字节码) (JVM 执行)
- 编译 :
javac Hello.java→ 生成Hello.class。.class里是字节码(bytecode),一种 JVM 才能看懂的中间格式,既不是源码也不是机器码。 - 运行 :
java Hello→ JVM 加载.class,把字节码翻译成机器码执行。
字节码带来「一次编译,到处运行」:同一个
.class在 Windows / Mac / Linux 的 JVM 上都能跑。
三、核心:字节码版本与 JVM 向下兼容
这是理解所有版本问题的关键。每个 .class 文件头部都写着一个「字节码版本号」,标明它是哪个 Java 版本编出来的、需要至少哪个版本的 JVM 才能运行:
| Java 版本 | class 字节码版本号 |
|---|---|
| Java 7 | 51 |
| Java 8 | 52 |
| Java 11 | 55 |
| Java 17 | 61 |
| Java 21 | 65 |
唯一规则:运行时 JVM 的版本必须 ≥ class 的字节码版本。
- 用 JDK 17 编出来的 class(61)→ 放到 JDK 8 的 JVM(最高认 52)→ ❌ 报
UnsupportedClassVersionError。 - 用 JDK 8 编出来的 class(52)→ 放到 JDK 17 的 JVM → ✅ 正常运行。
一句话总结:JVM 向下兼容,不向上兼容(高版本 JVM 能跑低版本字节码,反之不行)。
由此还能推出一条编译器铁律:javac 只能产出「≤ 自己版本」的字节码,不能编出比自己更高的版本 。让 JDK 7 的 javac 编出 Java 11,逻辑上不可能。
四、控制编译版本:source/target 与 release
现实中常出现「我机器装 JDK 17,但项目要发布到 JDK 8 的服务器」这种错配场景。javac 提供参数来控制「编出什么版本」,这也是 pom.xml 里版本配置的来源。
4.1 老参数:-source 和 -target
bash
javac -source 8 -target 8 Hello.java
-source 8:按 Java 8 的语法规则检查源码。用了 Java 9+ 的新语法 → 编译报错。-target 8:生成 Java 8 版本号(52)的字节码。
漏洞(交叉编译陷阱) :这俩管不住「你用了哪些 API」。用 JDK 17 的 javac 加 -source 8 -target 8 时,编译器手里拿的仍是 JDK 17 的类库 。于是你写了 List.of()(Java 9 才有的方法):
-source 8不报错(语法没问题);- 编译通过,class 也是版本 52;
- 但拿到真正的 JDK 8 上运行 → ❌
NoSuchMethodError(JDK 8 的List没有of())。
4.2 新参数:--release(JDK 9 引入,推荐)
bash
javac --release 8 Hello.java
--release 8 一次性锁死三样东西:
- 语法按 Java 8;
- 字节码版本按 52;
- API 也限定为 Java 8 里真实存在的 ------再写
List.of(),编译阶段就直接报错。
4.3 对比
| 配置 | 管语法 | 管字节码版本 | 管 API 范围 |
|---|---|---|---|
source / target(老) |
✅ | ✅ | ❌(用当前 JDK 的 API,有陷阱) |
release(JDK 9+) |
✅ | ✅ | ✅(最安全) |
结论 :只要 JDK ≥ 9,优先用
release取代source+target。
4.4 这些配置是「强制」的
pom 里的版本配置不是参考,而是会被翻译成真实的 javac 参数强制执行:语法不符直接报错、字节码严格按指定版本产出。若要求的版本超过编译器自身能力(如 JDK 8 配 release 17),不会降级将就,而是直接编译失败。
五、Maven 的本质:一个调用工具链的 Java 进程
把前面的底层知识铺好后,Maven 就很好理解了。
5.1 Maven 自己是个 Java 程序
Maven 本身是一个 Java 进程,必须跑在某个 JVM 上。 它自己不编译、不打包,而是委托给底层的编译器和打包逻辑------它的角色是「指挥官 / 编排者」,不是「编译器」。
5.2 它如何「调用」编译
一个容易误解的细节:Maven 默认不是 fork 一个外部 javac 命令行进程 ,而是通过 JDK 内置的编译器 API(javax.tools / JavaCompiler) 来编译,这个编译器就在运行 Maven 的那个 JDK 里。只有显式配置 <fork>true</fork> 时,才会真的启动一个独立的 javac 外部进程。
打包同理:maven-jar-plugin 默认用 Java 代码(基于 JDK 类库)完成,而不是去 fork 命令行 jar。
但无论是「调 API」还是「fork 进程」,用的编译器/工具版本都来自运行 Maven 的那个 JDK。 所以「用什么版本的 JDK 跑 Maven,就用什么版本的编译器」这个结论始终成立。
5.3 运行 Maven 要用 JDK,不是 JRE
因为编译需要 javac,而 javac 只在 JDK 里。所以编译类的 Maven 任务必须用完整 JDK 来跑 。IntelliJ 设置里那个「Runner JRE」选项虽叫 JRE,实际应选一个完整 JDK(这只是历史命名)。
5.4 pom 版本参数 = 传给编译器的参数
pom.xml 里的版本配置,本质就是告诉 Maven「编译时给编译器传什么参数」:
xml
<!-- 老写法:相当于 javac -source 8 -target 8 -->
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<!-- 新写法(推荐):相当于 javac --release 8 -->
<properties>
<maven.compiler.release>8</maven.compiler.release>
</properties>
六、版本一致性:为什么 Maven 的 JDK 要和项目对齐
把整条链串起来:
运行 Maven 的 JDK
│ 决定用哪个编译器
▼
编译器按 pom 的版本参数编译
│ 产出
▼
带版本号的 .class
│ 运行
▼
某个版本的 JVM 执行(JVM 版本必须 ≥ class 版本)
三种情况:
- 运行 Maven 的 JDK 太低 (如 JDK 8)但 pom 要
release 17→ 编译器编不出更高版本 → ❌ 编译失败。 - 运行 Maven 的 JDK 太高 (JDK 17)、pom 用老的
source/target 8、运行又在 JDK 8 → ⚠️ 交叉编译陷阱,编译过但运行崩。 - 三者对齐 (编译 JDK = 目标版本 = 运行 JVM)→ ✅ 最稳,没有版本错配意外。
实践建议:让以下三处指向同一个 JDK 版本------
- Maven Runner 的 JDK(IntelliJ → Build Tools → Maven → Runner → JRE);
- pom 的编译目标版本 (
release或source/target); - IDEA 的 Project SDK(Project Structure → Project → SDK)。
七、常见错误速查
| 报错 / 现象 | 原因 | 解决 |
|---|---|---|
UnsupportedClassVersionError |
用高版本 JDK 编的 class,拿到低版本 JVM 运行 | 运行用更高 JVM,或编译时降低目标版本 |
NoSuchMethodError(运行时) |
交叉编译陷阱:用了高版本 API,运行在低版本 | 改用 release 参数锁死 API |
release version 17 not supported |
编译器版本低于要求的目标版本 | 用 ≥ 目标版本的 JDK 跑 Maven |
invalid flag: --release |
用了 JDK 9 之前的 javac,不认识 --release |
升级 JDK,或改用 source/target |
编译需要 javac 却报找不到 |
用纯 JRE 跑了 Maven | 改用完整 JDK |
八、一图总览 & 核心结论
┌──────────────────────────────────────────────────────────┐
│ Maven(Java 进程,跑在 JVM 上) │
│ 不自己编译,委托给 ↓ │
├──────────────────────────────────────────────────────────┤
│ 运行 Maven 的 JDK(必须是 JDK,不能是纯 JRE) │
│ ├── 编译器(版本 = 该 JDK 版本) │
│ │ └─ 受 pom 的 source/target/release 控制产出版本 │
│ └── 打包逻辑(jar) │
└──────────────────────────────────────────────────────────┘
│ 编译 │ 运行
.java ───► .class(带字节码版本号)───────► JVM 执行
▲ ▲
版本号必须 ≤ 运行它的 JVM 版本 ───────┘
核心结论(最该记住的 7 条)
javac是 JDK 的一部分 ,JDK 什么版本,javac就是什么版本;JRE 里没有javac。- Java 先编译(
javac)成字节码.class,再由 JVM(java)运行。 - 每个
.class带字节码版本号,JVM 必须 ≥ 它才能跑------向下兼容,不向上兼容。 javac只能产出 ≤ 自己版本的字节码,编不出更高版本。- pom 的
source/target/release是强制的编译参数 ;release比source/target多锁了 API,更安全。 - Maven 是个 Java 进程,自己不编译/打包 ,默认调用「运行它的那个 JDK」内置的编译器 API(非 fork 命令行
javac),版本随该 JDK 走;运行 Maven 要用 JDK。 - 让「Maven Runner JDK、pom 目标版本、IDEA Project SDK」三者一致,避免一切版本错配的坑。