1. 项目背景
业务场景:某金融科技公司的中间件团队接到一个任务——公司自研的 RPC 框架在高并发场景下偶发线程泄漏,用尽各种诊断工具后,团队负责人怀疑是 JDK 底层线程池的某个边缘行为触发了问题。但线上运行的 Temurin 镜像不带 debug 符号,无法用 gdb 打断点跟踪 HotSpot 内部。他们需要一套"能调试 JVM 源码"的开发环境。
痛点:
- 黑盒困境:生产环境的 JDK 镜像都是 Release 构建,没有调试符号,调用栈里只看到
libjvm.so的地址偏移量,无法对应到源码行数。出问题只能靠 Google 和猜测。 - 源码恐惧:OpenJDK 源码树超过百万行,
src/目录下几千个文件,新人打开一看直接放弃——不知道从哪看起,不知道哪些目录对应什么功能。 - 编译门槛:网上说"编译 OpenJDK 需要 4 小时",很多人被劝退。实际上现代机器配合
make images增量编译只需 10 到 20 分钟。但首次配置依赖(boot JDK、autoconf、C++ 编译器)确实容易踩坑。
本章目标是让读者"弄脏手"——在本地或容器中编译出一份带调试符号的 fastdebug 镜像,同时建立 OpenJDK 源码目录的心智地图。这将成为后续所有源码级分析的"基础设施"。
2. 项目设计
(小胖的工位上,他正盯着一份hs_err_pid.log发愁。)
小胖:大师救命!线上线程池炸了,日志里只有一段V [libjvm.so+0x8a3b2f]这种天书。运维说看不懂,让我自己 debug JVM——可我连 JDK 源码长啥样都不知道啊。难道写 Java 的人还要去编译 C++?
大师(坐下):别慌,这正是我们要解决的问题。先给你看一张地图——OpenJDK 源码目录的"城市规划"。
jdk/ ├── src/ # 所有源码 │ ├── java.base/ # Java 基础模块(String, Object, ClassLoader...) │ ├── java.util.concurrent/ # JUC 并发包 │ ├── jdk.compiler/ # javac 编译器 │ ├── jdk.jcmd/ # jcmd, jps, jstat 等诊断工具 │ ├── hotspot/ # HotSpot VM C++ 源码 │ │ ├── share/ # 跨平台共享代码 │ │ │ ├── runtime/ # 线程、Safepoint、句柄 │ │ │ ├── oops/ # oop, Klass, MarkWord │ │ │ ├── gc/ # GC 实现 (g1, z, parallel, serial) │ │ │ ├── compiler/ # JIT 编译器 (c1, c2/opto) │ │ │ ├── interpreter/ # 字节码解释器 │ │ │ ├── classfile/ # 类文件解析 │ │ │ ├── memory/ # metaspace 与内存管理 │ │ │ └── services/ # JMX, NMT 等 │ │ └── cpu/ # 平台相关 (x86, aarch64...) │ └── utils/ # 工具 (IGV 可视化等) ├── make/ # 构建系统 (autoconf/make) ├── test/ # 测试 (jtreg, hotspot, jdk) └── doc/ # 文档 (building.md 编译指南)小白(翻了翻):src/java.base里面确实是 Java 写的,但src/hotspot/share/runtime/thread.cpp这种就是 C++ 了?也就是说,我们在 IDE 里写new Thread().start(),最终会调到这个.cpp文件里的JavaThread::start()?
大师:精确。Java 层的java.lang.Thread.start()是一个 native 方法,在底层通过 JNI 调用 HotSpot 的JVM_StartThread,再由Threads::create_vm创建真正的 OS 线程。如果你有 debug 符号,就可以在 gdb 里从 Java 调用一路断点到 C++ 层。
技术映射:Java 层 ↔ 用户界面(点餐 App),native 声明 ↔ 订单系统接口,C++ 实现 ↔ 后厨实际做菜。
小胖:那我现在就想编译一个能 debug 的 JDK!听说要装一堆依赖,还经常报错——我上次在 Ubuntu 上试了一下午都没成功。
大师:今天我们会把它跑通。你先搞清楚三个概念:
- Boot JDK:要编译 JDK N,需要一份 N-1 版本的 JDK 来运行构建系统。比如编译 JDK 21,需要 JDK 20 或 JDK 17 作为 Boot JDK。
- 构建类型:
release(生产用,优化全开,无符号)、fastdebug(优化适中,有符号和断言,推荐日常开发)、slowdebug(几乎无优化,极慢但最完整调试信息)。 - configure → make:第一步
bash configure检测系统环境生成构建配置;第二步make images执行实际编译并生成 JDK 镜像。
技术映射:Boot JDK ↔ 先有鸡才能生蛋(前一个版本 JDK 编译下一个版本);fastdebug ↔ “半成品鸡块”,已经熟了但还能看到纹理,方便检验。
小白:我有个疑问——为什么不直接用 GDB attach 到生产环境的 JDK 上?你说了 release 构建不带符号。
大师:不是绝对不能,但非常痛苦。release 构建会做内联、去符号、指令重排等优化,你在 gdb 里看到的调用栈可能是残缺的,print变量可能被优化掉。而且生产环境通常不允许用调试器 attach。所以我们提倡平时就维护一份 fastdebug 镜像,遇到疑难问题时用这套镜像复现后再下断点。
小胖:那编译一次大概要多久?我笔记本只有 8 核 16G 内存,会跑炸吗?
大师:8 核 16G 足够。首次全量编译约 15-30 分钟;增量编译(改一个.cpp文件)通常 1-3 分钟。这里有个窍门——make命令后面加JOBS=8可以指定并行编译线程数;另外CONF=linux-x86_64-server-fastdebug确保你用正确的配置目录。
技术映射:增量编译 ↔ 只重新炒改过的菜,不用重做整桌饭;JOBS↔ 并行打饭窗口数,合理设置才能最大化吞吐。
3. 项目实战
3.1 环境准备
| 组件 | 最低版本 | 说明 |
|---|---|---|
| 操作系统 | Ubuntu 22.04 / macOS 13+ / WSL2 | 推荐 Linux 环境,macOS 也可 |
| Boot JDK | JDK 20 或 JDK 21 | 运行构建系统的 JDK |
| 编译器 | GCC 10+ 或 Clang | C++ 编译器 |
| 依赖库 | CUPS, freetype, alsa 等 | doc/building.md有完整列表 |
| Git | 2.30+ | 克隆 OpenJDK 仓库 |
| 磁盘空间 | ≥ 15GB | 源码 + 编译产物 |
Ubuntu 快速安装依赖:
sudoapt-getupdatesudoapt-getinstall-y\build-essential\autoconf\zip\unzip\libx11-dev libxext-dev libxrender-dev libxrandr-dev libxtst-dev libxt-dev\libcups2-dev\libfreetype6-dev\libasound2-dev\libffi-dev\fontconfig3.2 分步实现
步骤一:克隆 OpenJDK 源码
目标:获取 OpenJDK 官方源码仓库,并切换到目标分支。
# 克隆 JDK 主线仓库(浅克隆以加速,--depth=1)gitclone--depth=1https://github.com/openjdk/jdk.gitcdjdk# 查看当前版本catmake/conf/version-numbers.conf|head-5# 输出类似:# DEFAULT_VERSION_FEATURE=24# DEFAULT_VERSION_INTERIM=0# DEFAULT_VERSION_UPDATE=1# DEFAULT_VERSION_PATCH=0# 如果想编译特定 LTS 版本(如 JDK 21),可以切换 tag# git fetch --unshallow# git checkout jdk-21-ga步骤二:Configure 配置
目标:运行 configure 脚本,生成编译配置。
# 确保 Boot JDK 已安装且 JAVA_HOME 指向正确版本java-version# openjdk version "21.0.5" ...# 运行 configure(fastdebug 构建,带调试信息)bashconfigure\--with-debug-level=fastdebug\--with-jvm-variants=server\--with-native-debug-symbols=internal\--disable-warnings-as-errors关键参数说明:
| 参数 | 含义 |
|---|---|
--with-debug-level=fastdebug | 构建类型:release / fastdebug / slowdebug |
--with-jvm-variants=server | JVM 变体:server / client / minimal |
--with-native-debug-symbols=internal | 符号嵌入二进制内部而非单独.debug文件 |
--disable-warnings-as-errors | 避免旧版编译器报 warning 中断编译 |
--with-jvm-features=dtrace,jfr | 可选:开启 DTrace/JFR 特性 |
可能遇到的坑:
configure: error: Could not find freetype!:安装libfreetype-dev后用--with-freetype=system指定系统库路径。configure: error: Cannot find boot jdk:Boot JDK 版本太旧。编译 JDK N 必须用 ≥N-1 的 JDK;如果装了多版本,用--with-boot-jdk=/path/to/jdk-21显式指定。- macOS 上
xcrun: error:需要 Xcode Command Line Tools:xcode-select --install。 - Windows WSL2 路径问题:不要在
/mnt/c/下编译(跨文件系统性能极差且权限模型不同),在 WSL 本地目录(如~/jdk/)下操作。
步骤三:执行编译
目标:运行 make images 生成完整的 JDK 镜像。
# 编译(首次约 15-30 分钟)# === Windows Powershell ===$env:JOBS=16;makeimages# 编译完成后查看产物lsbuild/linux-x86_64-server-fastdebug/images/jdk/bin/# 应包含 java, javac, jcmd 等所有可执行文件验证 debug 构建是否成功:
# 启动编译好的 JDK./build/linux-x86_64-server-fastdebug/images/jdk/bin/java-version# 输出应包含:OpenJDK 64-Bit Server VM (fastdebug)# 检查 debug 符号file./build/linux-x86_64-server-fastdebug/images/jdk/lib/server/libjvm.so# 输出应有 "not stripped"(未剥离符号)# 用 gdb 验证可调试gdb--args./build/linux-x86_64-server-fastdebug/images/jdk/bin/java-version# 在 gdb 中执行:# (gdb) b JavaThread::JavaThread# 如果断点设置成功且显示源码行号,说明 debug 构建成功步骤四:增量编译验证
目标:验证修改一个源文件后增量编译的速度。
# 修改一个无害的注释(在 thread.cpp 中)echo"// 这是增量编译测试">>src/hotspot/share/runtime/thread.cpp# 增量编译(通常 1-3 分钟)makeimages# 验证修改已生效./build/linux-x86_64-server-fastdebug/images/jdk/bin/java-version步骤五:用 jcmd 验证自定义构建
# 启动自定义构建的 JDK 运行一个简单程序./build/linux-x86_64-server-fastdebug/images/jdk/bin/java\-XX:+PrintFlagsFinal\-version2>&1|grep-E"(debug|fastdebug|product)"# 检查 debug 特有的参数是否可用./build/linux-x86_64-server-fastdebug/images/jdk/bin/java\-XX:+UnlockDiagnosticVMOptions\-XX:+PrintAssembly\-version# 如果输出机器码,说明 hsdis 反汇编插件可用3.3 完整验证脚本
#!/bin/bash# verify_build.sh - 验证 OpenJDK 编译成果JDK_HOME="./build/linux-x86_64-server-fastdebug/images/jdk"echo"=== 1. 版本信息 ==="$JDK_HOME/bin/java-versionecho""echo"=== 2. VM 信息 ==="$JDK_HOME/bin/jcmd$$VM.version2>/dev/null||\$JDK_HOME/bin/java-Xinternalversionecho""echo"=== 3. Debug 符号检查 ==="file$JDK_HOME/lib/server/libjvm.so|grep-o"not stripped"echo""echo"=== 4. 堆空间信息 ==="$JDK_HOME/bin/java-XX:+PrintFlagsFinal-version2>&1|\grep-E"(InitialHeapSize|MaxHeapSize)"echo""echo"=== 5. 编译器信息 ==="$JDK_HOME/bin/java-XX:+PrintFlagsFinal-version2>&1|\grep-E"TieredCompilation|CICompilerCount"echo""echo"=== 6. 运行简单测试 ==="cat>/tmp/TestJDK.java<<'EOF' public class TestJDK { public static void main(String[] args) { System.out.println("Runtime: " + Runtime.version()); System.out.println("Available Processors: " + Runtime.getRuntime().availableProcessors()); System.out.println("Max Memory: " + Runtime.getRuntime().maxMemory() / 1024 / 1024 + " MB"); } } EOF$JDK_HOME/bin/javac /tmp/TestJDK.java-d/tmp/$JDK_HOME/bin/java-cp/tmp/ TestJDKecho""echo"=== 全部验证通过 ==="可能遇到的坑(补充):
make报Out of memory错误:减少并行线程数JOBS=4,或者给虚拟机/容器分配更多内存。- 编译产物占用大量磁盘空间:
build/目录可能超过 5GB,可定期删除旧构建配置rm -rf build/linux-*。 - Windows 上编译:通常不推荐直接在 Windows 上编译,建议通过 WSL2 操作。如果必须 Windows 原生编译,需要 VS 2022 + Cygwin,复杂度过高。
4. 项目总结
4.1 优点与缺点
| 维度 | 优点 | 缺点 |
|---|---|---|
| 调试能力 | fastdebug + 内部符号,gdb 可直接在 VM 层打断点 | 构建时间较长(首次 15-30 分钟),CI 流水线需要缓存优化 |
| 源码导航 | 建立src/hotspot/*到功能模块的心智地图 | 源码组织方式与业务代码完全不同,OOP 设计模式学习曲线陡峭 |
| 增量编译 | 改一行代码 1-3 分钟即可验证,迭代极快 | Makefile 依赖链复杂,偶尔需要make clean才能正确增量 |
| 构建系统 | autoconf/make 成熟稳定,跨平台支持好 | configure 命令行参数上百个,容易遗漏 |
| 容器化 | 可在 Dockerfile 中固化依赖,团队共享一致的构建环境 | 容器内编译需要较多资源(内存 ≥8GB),小型 CI 机器可能不够 |
4.2 适用场景
- JVM 源码级排障:需要打断点跟踪 HotSpot 内部行为(如线程创建、GC 触发逻辑)。
- 自研 JDK 增强:修改 GC 参数默认值、增加自定义 JVM Flag、定制类加载行为。
- 安全审计:需要阅读加密算法、TLS 相关的本地实现是否包含后门或漏洞。
- 性能极值优化:修改编译器优化管线(如内联策略、循环展开阈值)。
- 学习 OpenJDK 设计模式:通过编译和调试深入理解 Handle/oop/Klass 体系。
不适用场景:
- 单纯"调参就能解决"的问题不需要编译 JDK,调整
-Xmx、-XX:+UseG1GC等参数即可。 - 团队没有 C++ 经验且也不想学习 C++ 的开发——源码阅读应聚焦
src/java.base的 Java 层。
4.3 注意事项
| 类型 | 详细说明 |
|---|---|
| 版本兼容 | Boot JDK 版本必须 ≥ N-1;用 JDK 17 编译 JDK 21 是合法的(N-3),但需加--with-jtreg等额外配置 |
| 构建速度 | 首次构建建议在 SSD 上执行;ccache可显著加速二次构建:configure 时加--with-ccache |
| 符号策略 | internal符号嵌入二进制中,文件较大但方便 gdb 自动加载;external生成单独的.debuginfo文件,需要手动指定路径 |
| 许可证 | 从 OpenJDK 仓库编译的二进制产物受 GPLv2+CPE 许可约束,分发时需保留许可证文件 |
4.4 常见踩坑经验
案例 1:configure 检测不到 X11 库但项目不需要 GUI
某后端团队编译 JDK 时 configure 报错Cannot find X11 libraries。服务端 JDK 确实不需要图形库,但 configure 默认会检查。修复:加--with-x=no或安装libx11-dev。
案例 2:WSL2 跨文件系统编译慢 10 倍
同事在/mnt/c/Users/xxx/jdk(Windows 文件系统)下编译,全程耗时 2 小时。根因:WSL2 通过 9p 协议访问 Windows 文件系统,IO 延迟极高。修复:将源码 clone 到~/jdk(WSL 本地 ext4 文件系统),编译时间降至 18 分钟。
案例 3:fastdebug 镜像的assert导致生产事故
某团队为了方便,在预发环境也跑了 fastdebug 镜像。一个正常情况下不会触发的assert在边界条件下被命中,导致 JVM 直接 abort。根因:fastdebug 构建启用了ASSERT宏,许多"理论上不可能"的路径会在命中断言时直接崩溃。铁律:fastdebug 仅用于开发和自测,绝不能上预发/生产。
4.5 思考题
进阶题:
make images和make hotspot有什么区别?如果你只想修改 HotSpot 源码并快速验证,应该用哪个命令?请尝试只编译 HotSpot 模块并对比两者的耗时差异。实战题:你编译的 fastdebug JDK 启动时间比线上 Release JDK 慢多少?请用
time java -version分别测量并分析差异来源(提示:fastdebug 关闭了哪些优化?)。
答案提示:思考题 1 答案见本章"步骤四"增量编译部分;思考题 2 答案见第 22 章 JIT 编译分层相关内容。
下一章预告:第 3 章将深入
.class文件的二进制结构,手把手教你用javap读懂字节码,揭开装箱、泛型擦除、lambda 的底层实现。
延伸阅读与资源
Redis 8 实战精讲:从 CRUD 到源码,构建高可用缓存系统
Redis 实战修炼与原理进阶
Python 3实战精进:从脚本到高并发订单引擎
MongoDB 实战进阶与内核修炼
python入门:Rquests从菜鸟脚本到企业级SDK的网络实战圣经
Milvus向量数据库实战修炼:从 0 到 1精通向量检索与生产落地
后端工程师的 AI 转型第一课:Ollama 与私有化大模型实战
10倍开发者的 Dify 魔法书:从零构建全栈 AI 应用
后端工程师转型AI第一课-Ollama 与私有化大模型实战
大型语言模型(LLM) vLLM 高性能推理落地实战
Agent开发之LlamaIndex 实战修炼与源码进阶
大语言模型Transformers 实战修炼与源码剖析