编译JDK17这件事我第一次完整跑通是在一台配置不算高的Linux服务器上整整折腾了一个下午中间configure报错三次make到一半磁盘满了最后看到“Finished building OpenJDK 17 for target default”那行字的时候那种成就感比写完一整年代码还实在。后来帮团队配置过定制JDK、也给新手做过不少次环境演示发现很多人卡住的其实不是编译这个动作本身而是对整个JDK构建体系的陌生。这篇文章我就把从零编译一个JDK17的全过程掰开揉碎包括环境准备、源码获取、configure参数、make构建、产物验证和问题排查每一步都讲清楚为什么这么做遇到坑怎么爬出来。这个内容适合谁正在学JVM底层原理、想自定义JDK功能做实验的开发者或者公司需要基于OpenJDK做二次定制、但之前没碰过构建系统的工程师。不需要你有多深的C功底只要懂Java基础、会用命令行跟着走一遍就能跑通。编译一个JDK17不是炫技它是理解Java生态最底层那几块砖头的好办法也是很多高级定制工作的第一步。1. 编译JDK17的整体思路这件事到底在做什么1.1 需求拆解从“想要一个JDK”到“自己造一个JDK”日常我们用JDK都是直接下载官方编译好的发行版解压配置环境变量就用。自己编译JDK17核心思路完全不一样——我们要做的是把OpenJDK的源代码用一套完整的构建工具链在你的本机环境中编译成可运行的JDK二进制产物。这本质上是一个C和一个Java混合的大型项目构建过程。OpenJDK17的源码里既有HotSpot虚拟机的C代码大约几百万行也有Java类库的源码java.base、java.util这些模块的.java文件还有一堆用shell、makefile、python写的构建脚本。编译要完成的事情是先用一个已有的JDK叫Boot JDK把Java部分的类库编译出来再用系统的C/C编译器把HotSpot虚拟机编出来最后把两者合到一起生成完整的JDK目录。这就像自己动手组装一台电脑Boot JDK是“临时借来的CPU”用来启动第一批程序系统的gcc/g是“螺丝刀”源码是“零件”最后装出来的整机就是编译好的JDK17。1.2 自己编译的收益与适用场景花几个小时跑编译图什么我归纳下来有三类核心价值。第一类是底层理解。你会在configure的时候和一堆参数打交道会看到各个版本JDK的目录差异会知道JDK内部lib目录里哪些是动态库、哪些是静态数据能直观感受到Java生态赖以运行的那层native代码是什么样的。第二类是定制能力。很多公司和研究机构会对OpenJDK源码做patch比如修改GC参数默认值、调整JIT编译策略、内置自己公司的证书、定制类加载行为。这些全部要求你先把源码编译通过才有继续改的基础。第三类是环境破局。某些受限环境里不能随便安装预编译的软件包或者官方下载的JDK不兼容特定的CPU架构/操作系统版本比如特殊的ARM处理器、老的glibc版本这时候自己编译就成了唯一出路。话说回来如果你只是想日常开发写代码直接用官方发行版就行没必要自己编——这个选择不丢人编译这件事本来就是给有特殊需求的人准备的。2. 编译前的环境准备工具链齐不齐决定成败2.1 硬件与操作系统要求编译JDK17是个吃资源但不挑食的活。我在2核4G的机器上编过虽然慢但能跑完在8核16G的云服务器上编过大约20分钟搞定。大致建议内存至少4GB推荐8GB及以上。内存不够的典型症状是编译过程中出现“cannot allocate memory”或者直接被OOM killer杀掉进程。磁盘源码加构建产物加临时文件预留10GB以上最稳妥。源码解压后大概2GB构建目录按debug级别不同能到3-6GB。CPU核心数make默认会吃满所有核心核心越多越快但要注意别让机器彻底卡死。操作系统Linux是体验最好的macOS也行Windows官方虽然支持但坑多尤其需要安装一堆额外工具我的建议是新手别从Windows开始。2.2 安装依赖工具少一个包都过不了configure在Linux上OpenJDK的configure阶段会检查大量外部依赖。不同发行版包名略有差异我以CentOS/RHEL系和Debian/Ubuntu系分别列一份常用清单Debian/Ubuntu系sudo apt-get install -y build-essential \ autoconf make gcc g cpio \ libx11-dev libxext-dev libxrender-dev libxrandr-dev \ libxtst-dev libxt-dev libfontconfig1-dev libfreetype6-dev \ libcups2-dev libasound2-dev \ zip unzipCentOS/RHEL系用yum或dnfsudo yum install -y gcc gcc-c make autoconf cpio \ libX11-devel libXext-devel libXrender-devel libXrandr-devel \ libXtst-devel libXt-devel fontconfig-devel freetype-devel \ cups-devel alsa-lib-devel \ zip unzip这些包看起来多其实分几类编译核心gcc/g/make/autoconf/cpio——C编译器和构建工具。图形/AWT相关libX11、libXrender这些JDK里java.awt包要调底层X11接口编译时没有这些头文件会报错。字体相关freetype、fontconfig——JDK图形渲染需要。打印相关cups——java.awt.print用得上。音频相关alsa-lib——javax.sound.sampled的支持。2.3 Boot JDK怎么选JDK编译JDK的老规矩构建OpenJDK17需要一个“上一代”或者“同代更早”的JDK作为起点叫Boot JDK。官方文档明确要求编译JDK17Boot JDK必须是JDK16或JDK17的某个版本。原因是JDK的类库代码里会出现只在新版本里才有的语言特性和API用一个太老的Boot JDK编不过去。有个非常容易踩的坑如果你机器上默认的java版本是JDK11或者JDK8直接跑configure报错就是“Boot JDK too old”。解决方法是下载一个JDK16或17的发行版然后通过configure的--with-boot-jdk参数指过去。我自己习惯的路径是export PATH/opt/jdk-17.0.2/bin:$PATH java -version确认当前默认java已经是17之后再进源码目录跑configure。另外多说一句Boot JDK不需要和你编译的目标完全一致但最好也是同一主版本线比如编JDK17就用Boot JDK 16或者17避免一些隐性问题。3. 源码获取与configure配置构建系统开始接管3.1 下载OpenJDK17源码的两种方式OpenJDK17的官方源码托管在Mercurial上同时也维护了Git镜像。现在做开发环境我用Git居多简单干净git clone https://github.com/openjdk/jdk17.git jdk17 cd jdk17 git checkout jdk-17.0.28如果想体验和社区同步的工作流也可以装Mercurial再拉hg clone https://hg.openjdk.java.net/jdk-updates/jdk17u jdk17u两种都行区别不大。我推荐Git是因为tag管理直观、切换版本方便。源码目录很大文件数很多下载慢是正常的不用慌。拿到之后看一眼顶层目录会看到make、src、doc这些文件夹还有一个关键的configure脚本——它是整个构建的入口。3.2 configure参数详解踩过的坑都在这很多人编译JDK第一次就挂在configure上。configure的作用就是检查系统环境、探测所有依赖、生成构建配置。最基础的调用方式bash configure如果你只是想在标准环境下快速跑一个release版JDK出来直接这么敲就够了。但实际中我几乎一定会额外加几个参数因为默认配置带了很多调试信息产物又大又慢bash configure \ --with-boot-jdk/path/to/jdk-17 \ --with-jvm-variantsserver \ --enable-ccache \ --disable-debugging这里几个参数的意思--with-boot-jdk指定Boot JDK路径如果环境变量PATH里是对的这步可以省略但写出来更保险。--with-jvm-variantsserverJDK的虚拟机有server和client两种变体现在其实还加了很多实验变体server适合服务端场景一般默认就是它显式写出来是为了明确意图。--enable-ccache启用编译器缓存你可能会在迭代编译时反复改代码有了ccache能省掉大量重复编译时间。--disable-debugging关掉调试信息产物体积能小不少速度也有提升。还有两个参数看你实际需求--with-target-bits64 # 64位/32位 --with-native-debug-symbolsnone # 不需要本地调试符号时不生成如果你是纯好奇编译过程建议加--disable-debugging能节省差不多一半的编译时间。如果你想后续用GDB去调试JVM源码就不能关还得反过来加--enable-debug --with-native-debug-symbolsinternal。configure跑完后屏幕最后会打印出一段配置摘要包含JVM变体、编译级别、使用的编译器版本、Boot JDK路径等信息建议花三十秒认真看完很多问题都能在这里提前发现。3.3 configure阶段常见报错处理我把自己在configure阶段真正遇到过的几条报错贴出来报错Boot JDK is too old原因很简单当前PATH里的Java版本过低。解决方法export PATH/opt/jdk-17.0.2/bin:$PATH重新运行configure之前用java -version确认版本configure会读取PATH里的java所以环境变量没改对参数传了都不好使。报错AUTOCONF版本太老或不兼容有些老发行版自带的autoconf版本太低OpenJDK要求autoconf 2.69以上。解决方式是升级Ubuntu上apt-get install autoconf或者编译安装新版autoconf到/usr/local然后让它优先于系统版本。报错freetype版本过低或未找到JDK图形部分强依赖FreeType2而且需要带--with-freetype-include和--with-freetype-lib指定路径的情况也见过不少。多数场景装好libfreetype6-dev就能过。报错Unknown architecture / 不支持的CPU这个多半是在非常规架构上编译。我需要提醒一句x86_64和aarch64是主流支持的平台其他平台如MIPS、RISC-V在不同版本支持下差异很大编不通过别硬扛先查当前版本的支持列表。configure能跑通整个编译流程就已经成功了七成。剩下的事情基本就是倒杯茶等make跑完。4. 执行make构建核心编译流程全解析4.1 为什么make之后会有“全量再编译”的过程configure通过后源码目录里会多出一个build目录里面按配置生成了构建文件。真正的编译命令只需要一句make images注意网上有些教程让人直接敲make这也能编但生成的是中间产物不一定包含完整可运行的JDK镜像。make images才是生成完整JDK二进制image的入口。实际经历中make和make images的关系是前者构建中间产物后者在中间产物基础上打出一个完整的、带bin目录的JDK镜像。JDK17的编译流程可以粗略分成几个阶段创建Boot JDK桩用Boot JDK的jar工具、javac工具来编译java.base等模块。编译java.base模块JDK模块化之后所有基础类都在这步打包成jmod文件。编译HotSpot虚拟机这一步是纯C编译耗时最重中间会调用g生成一堆.o文件然后链接成libjvm.so。生成各类可执行文件把jvm动态库、java launcher、javac等工具整合成jdk目录。打包镜像拷贝所有模块、工具、运行时资源到最终产物目录。如果你想观察到底编译到哪一步了直接加JOBS1降低并发便于查看日志虽然会慢很多但对新手理解流程很有帮助。4.2 编译耗时与并行控制JDK构建系统默认允许并行编译核心越多越快。语法很简单make images JOBS4这个JOBS参数控制并发任务数。个人建议新机器、不知道稳定性如何时先跑nproc看核心数然后设为核心数减一。我见过有人把JOBS直接设成核心数的两倍结果机器内存爆掉编译到一半进程被内核杀掉前功尽弃。对于内存判断一个粗略估算每个并发编译任务大概需要1GB内存左右。8核机器开6个任务内存16GB绰绰有余4核8GB机器开4个任务就会有点紧。如果编译报“virtual memory exhausted”要么降JOBS要么关调试信息重建。另外如果编译中途失败重跑make images它会自动增量继续不需要从零开始。这个机制极大方便了排错流程——修掉报错再跑一次make就行只有改动到的部分会重编。4.3 构建产物怎么找build目录的学问编译完成后输出路径类似于build/linux-x86_64-server-release/images/jdk这个目录就是你亲手编译出来的JDK17里面有bin、lib、conf、legal等标准子目录。用起来毫无区别export PATH/path/to/build/linux-x86_64-server-release/images/jdk/bin:$PATH java -version输出的版本信息末尾会带上你编译时的构建标识比如8之类的版本后缀另外还能从java --version里看到构建源码仓库的信息——这些在官方发行版里也能看到类似格式不过现在带的是你自己的机器号。我把最终产物目录单独打包一份放到/opt/jdk-custom/然后配置好JAVA_HOME日常开发就能用自己的“手搓JDK”了。4.4 编译参数调优让第二次编译更快如果你打算基于同一个源码目录反复修改、反复编译做JDK定制的人基本都这样有两个优化强烈推荐第一个是ccache。在configure阶段加上--enable-ccache后续C文件重复编译时直接命中缓存HotSpot模块那堆大文件能省下非常多时间。实测中开ccache和不打开之间增量编译速度能差到两倍以上。第二个是不要频繁clean。JDK构建系统支持增量编译不要一看到报错就想着make clean全部重来。绝大多数情况下的报错都只涉及少数文件直接改掉再make即可。只有当你改了configure级别的参数比如切换编译级别、换Boot JDK才需要make clean或者干脆删掉build目录重建。还有个小技巧在多台机器之间同步源码后记得删掉build目录再配置因为build目录里存了本机的工具链路径和环境直接搬去别的机器跑make极大概率会因路径不匹配而诡异报错。5. 常见问题与排查技巧实录5.1 configure阶段的坑环境变量比参数更优先先讲一个我自己被坑过的用--with-boot-jdk明明指定了JDK17configure还是报Boot JDK太老。查了半天发现问题出在PATH里有个JDK8configure执行时调用的java命令来自PATH而不是--with-boot-jdk传的路径。JDK构建系统优先寻找java、javac这些命令参数指定路径只是第二选项。正确做法是先把PATH和JAVA_HOME统一设好export JAVA_HOME/opt/jdk-17.0.2 export PATH$JAVA_HOME/bin:$PATH然后再跑configure省心。排查思路失败时先看build/linux-x86_64-server-release/config.log这是configure完整日志搜关键字“error”、“not found”大多数依赖缺失都能在里头直接定位。5.2 编译中的OOM与内存策略中段报错不一定都会显示“OutOfMemory”也可能是某个C编译进程被内核杀掉了表现为Killed或Exit code 137。这时候不要盲目重跑make要先看内存free -h如果内存确实只剩几百MB了说明并行度太高。刚才说的JOBS参数在这时就用上了。另外一个大头是GCC本身占内存如果在配置阶段加过--disable-debugging编译优化级别降低内存占用会小很多。还有一种情况是磁盘满了用df -h检查如果根分区耗尽清点一下大文件——大部分情况下删掉build目录重来反而是最快的但千万别删源码本体。5.3 动态库链接失败缺的是头文件还是运行库有一次我编译到HotSpot链接阶段爆出一堆undefined reference to XRender...之类的链接错误。这其实是典型的“有运行库但缺开发头文件”问题。运行库是运行时用的编译需要的是带头文件的devel包。所以排查时要区分# 查运行库是否存在 ldconfig -p | grep XRender # 查开发头文件是否存在Ubuntu示例 dpkg -l | grep libxrender-dev如果运行库都在但缺头文件直接装libxrender-dev这类-devel包即可不需要重新configuremake会自动检测到新增的头文件。5.4 验证编译产物的正确性很多人编译完就高高兴兴配PATH忘了验证。我的习惯是三步验证# 第一步版本与构建信息 java -version # 第二步写一个完整跑一遍的Demo java -Xlog:gc* -jar my-demo.jar # 第三步检查模块系统加载 java --list-modules | grep java.base第二步尤其重要因为我遇到过编译成功但JVM启动直接crash的情况运行一下最基础的程序能快速暴露类库或native层的问题。另外如果要跑AWT图形相关的程序记得确保系统里桌面环境和X11相关的运行库齐全我曾在无显示器服务器上编译成功但图形接口全不可用——好在编译本身照样能过只是运行时受环境限制。5.5 最快的错误排查方式善用日志与注释源码构建失败时别花太多时间在网络上搜索日志片段。OpenJDK构建系统的报错信息通常相当明确。我的流程是先看最后50行输出起码知道是哪个目录、哪个目标失败了。再翻build/.../build.log搜索error关键字。如果是C编译报错错误信息里一般有代码文件路径和行号可以打开源码看一眼很多是版本冲突或宏定义问题。如果是链接错误用nm查看对应的二进制里有没有那个符号确认哪个函数没有编出来。这三板斧够快。老实说真正难的问题很少绝大多数都是环境和命令的问题。6. 进阶扩展编译一个带定制参数的JDK176.1 修改JVM默认参数后重新编译一旦你跑通了基础编译流程就可以尝试修改源码。举个例子我想把JDK默认的初始堆大小改成512M默认是物理内存的1/64找到HotSpot源码中的参数定义文件src/hotspot/share/runtime/globals.hpp搜索InitialHeapSize相关定义修改默认值后重新配置编译bash configure --with-boot-jdk/opt/jdk-17 --enable-ccache make images JOBS8增量编译通常几分钟就能出结果这样你就得到了一个“初始堆默认512M”的独家JDK。这个操作让很多同学第一次感觉到JDK原来也是可以被改的。6.2 自定义模块与裁剪JDKJDK17的模块系统在运行时其实很灵活。如果你判断当前场景不需要CORBA、不需要java.desktop图形模块可以在生成jmod时做个精简镜像。虽然这不算严格意义的“编译定制”但用自己编译出来的JDK做jlink精简jlink --module-path $JAVA_HOME/jmods \ --add-modules java.base,java.logging,java.naming \ --output /opt/jdk-trimmed这种做法写出来和直接编译一个完整JDK结合起来理解是理解Java运行时体系一条很好的路径。6.3 交叉编译与环境限制下的特殊场景最后一个扩展点如果是为嵌入式ARM设备或者特殊平台交叉编译configure里会用到--with-target-bits、--with-sysroot这类参数。说实话这类需求相对少见但要留个印象大多数发行版没有针对每个CPU都准备好预编译包时交叉编译工作站就会成为团队内部的“JDK工厂”。真想玩这块建议先从qemu的ARM环境尝试比一上来就跨真实硬件省事。最后分享一点个人体会编译JDK17就是个熟练工活第一次跑通之后第二次就轻车熟路了。真正值得多花时间的是把configure的输出看懂理解每一种参数对产物行为的影响。我见过不少人在这一步直接跳到make编译出了问题再回头翻配置反而绕了远路。建议第一次就养成“配置信息逐行看”的习惯后面改来改去的时候会少走很多弯路。如果你手头正好有台闲置的Linux机器不妨现在就拉一份源码按文章里的步骤跑一遍相信那种亲手造出一个JDK的感觉比下载一个安装包有意思多了。
阅读完成 · 觉得有帮助?