
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.so0x8a3b2f]这种天书。运维说看不懂让我自己 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 层 ↔ 用户界面点餐 Appnative 声明 ↔ 订单系统接口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命令后面加JOBS8可以指定并行编译线程数另外CONFlinux-x86_64-server-fastdebug确保你用正确的配置目录。技术映射增量编译 ↔ 只重新炒改过的菜不用重做整桌饭JOBS↔ 并行打饭窗口数合理设置才能最大化吞吐。3. 项目实战3.1 环境准备组件最低版本说明操作系统Ubuntu 22.04 / macOS 13 / WSL2推荐 Linux 环境macOS 也可Boot JDKJDK 20 或 JDK 21运行构建系统的 JDK编译器GCC 10 或 ClangC 编译器依赖库CUPS, freetype, alsa 等doc/building.md有完整列表Git2.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 主线仓库浅克隆以加速--depth1gitclone--depth1https://github.com/openjdk/jdk.gitcdjdk# 查看当前版本catmake/conf/version-numbers.conf|head-5# 输出类似# DEFAULT_VERSION_FEATURE24# DEFAULT_VERSION_INTERIM0# DEFAULT_VERSION_UPDATE1# DEFAULT_VERSION_PATCH0# 如果想编译特定 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 ...# 运行 configurefastdebug 构建带调试信息bashconfigure\--with-debug-levelfastdebug\--with-jvm-variantsserver\--with-native-debug-symbolsinternal\--disable-warnings-as-errors关键参数说明参数含义--with-debug-levelfastdebug构建类型release / fastdebug / slowdebug--with-jvm-variantsserverJVM 变体server / client / minimal--with-native-debug-symbolsinternal符号嵌入二进制内部而非单独.debug文件--disable-warnings-as-errors避免旧版编译器报 warning 中断编译--with-jvm-featuresdtrace,jfr可选开启 DTrace/JFR 特性可能遇到的坑configure: error: Could not find freetype!安装libfreetype-dev后用--with-freetypesystem指定系统库路径。configure: error: Cannot find boot jdkBoot JDK 版本太旧。编译 JDK N 必须用 ≥N-1 的 JDK如果装了多版本用--with-boot-jdk/path/to/jdk-21显式指定。macOS 上xcrun: error需要 Xcode Command Line Toolsxcode-select --install。Windows WSL2 路径问题不要在/mnt/c/下编译跨文件系统性能极差且权限模型不同在 WSL 本地目录如~/jdk/下操作。步骤三执行编译目标运行 make images 生成完整的 JDK 镜像。# 编译首次约 15-30 分钟# Windows Powershell $env:JOBS16;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\-version21|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/jdkecho 1. 版本信息 $JDK_HOME/bin/java-versionechoecho 2. VM 信息 $JDK_HOME/bin/jcmd$$VM.version2/dev/null||\$JDK_HOME/bin/java-Xinternalversionechoecho 3. Debug 符号检查 file$JDK_HOME/lib/server/libjvm.so|grep-onot strippedechoecho 4. 堆空间信息 $JDK_HOME/bin/java-XX:PrintFlagsFinal-version21|\grep-E(InitialHeapSize|MaxHeapSize)echoecho 5. 编译器信息 $JDK_HOME/bin/java-XX:PrintFlagsFinal-version21|\grep-ETieredCompilation|CICompilerCountechoecho 6. 运行简单测试 cat/tmp/TestJDK.javaEOF 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/ TestJDKechoecho 全部验证通过 可能遇到的坑补充make报Out of memory错误减少并行线程数JOBS4或者给虚拟机/容器分配更多内存。编译产物占用大量磁盘空间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 仓库编译的二进制产物受 GPLv2CPE 许可约束分发时需保留许可证文件4.4 常见踩坑经验案例 1configure 检测不到 X11 库但项目不需要 GUI某后端团队编译 JDK 时 configure 报错Cannot find X11 libraries。服务端 JDK 确实不需要图形库但 configure 默认会检查。修复加--with-xno或安装libx11-dev。案例 2WSL2 跨文件系统编译慢 10 倍同事在/mnt/c/Users/xxx/jdkWindows 文件系统下编译全程耗时 2 小时。根因WSL2 通过 9p 协议访问 Windows 文件系统IO 延迟极高。修复将源码 clone 到~/jdkWSL 本地 ext4 文件系统编译时间降至 18 分钟。案例 3fastdebug 镜像的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 实战修炼与源码剖析