☰
Windows 11下编译OpenJDK 21:从工具链到make images的完整指南
2026/10/20 8:56:14 网站建设 项目流程

说来有点打脸,我在Windows 11上编译OpenJDK 21这件事,前后折腾了一整个周末。最初想象得很简单——下载源码、跑两条命令、完事。结果configure阶段就让我体会到什么叫“一行报错信息背后藏着三个准备不周”。如果你也想在Windows 11下面自己编译一份OpenJDK 21,想搞明白JVM内部实现、修改某些默认行为,或者纯粹好奇Java平台的构建系统是怎么工作的,这篇文章大概能让你少走两天弯路。我会按自己的实际路径,把从工具链选型到最终跑通make images的每一个关键环节都摊开来讲,包括配置参数的含义、报错的排查套路、以及几处文档里没有细说的坑。

1. 为什么放着现成的JDK不用,非要自己编译

这个问题的答案,决定了你有没有必要往下看。老实说,99%的应用开发者确实不需要自己编译JDK,直接下载官方二进制包就好了。但剩下那1%的人,迟早会碰到官方发行版解决不了的场景。

1.1 官方发行版解决不了的刚需

最常见的刚需是调试JVM的native代码。Java层面的问题用IDE调试就行,但一旦问题深入JIT编译器、GC实现、类加载器的native层,你就必须要有一份带调试符号、且源码路径跟你本机对应的JDK。官方发行版虽然也会发布调试符号,但符号与源码版本严格一一对应,你想在某个HotSpot源码文件里加两行日志再跑一次,官方包根本做不到。

另一个刚需是定制JVM行为。比如你想修改默认GC参数、调整某个内存管理的默认阈值、或者自己实验一个JIT优化选项,这些都得改源码重新编译。我认识一个做性能分析的朋友,为了观察某个GC行为,在HotSpot源码里加了一段统计代码,重新make images之后跑基准测试,整个流程大概十几分钟就能完成一轮实验。这种节奏是二进制发行版给不了的。

还有一类需求是验证新特性或学习底层机制。OpenJDK 21刚发布时,不少人想试试虚拟线程和分代ZGC的实际运行效果,同时希望能在本地代码里看到这些特性在JVM内部是怎么流转的。自己编译一份,配合调试器单步跟踪,理解深度完全不一样。

1.2 编译一次能收获什么

抛开功能诉求,单说技术收获就很值。JDK的构建系统是GNU Make加autoconf的经典组合,configure生成配置、make执行构建,整个体系在开源界非常典型。你把这一套跑通,对大型C/C++项目的构建管理会有更实在的理解,而不是只在终端里敲过./configure && make而已。

另外,编译过程中你会被迫搞清楚不少Windows平台工具链的细节:MSVC和MinGW的ABI差异、系统库的依赖、第三方库的编译接入方式、以及如何在Windows上模拟一个Unix-like的构建环境。这些东西单独学很枯燥,但在“为了把JDK编译出来”的驱动下,你会记得特别牢。

1.3 什么人适合动手,什么人建议放弃

我建议满足以下任意一条的人再动手:要改JVM源码并长期维护自定义分支,想深入调试native层问题,或者对构建系统有强烈好奇。反之,如果只是想要一个“更新版本”的JDK,那直接去下官方二进制包,十分钟搞定,不要在自己机器上浪费一整天。

还有一点要提前说:OpenJDK官方只把Windows上的MSVC作为受支持工具链,MinGW虽然有尝试支持但不在官方保证范围内。你在网上会看到很多用MinGW编译成功的帖子,但如果你想少踩坑,请老老实实装Visual Studio的C++工作负载。

2. 构建前的三件套:Boot JDK、MSYS2、Visual Studio

整个准备阶段的核心就是这三样东西,任何一样出了问题,后面都走不通。我会把每一件的关键细节都讲清楚,顺序也建议按我下面的来。

2.1 Boot JDK版本的选择逻辑

编译JDK需要一个“引导JDK”(Boot JDK),用来在构建过程中执行Java代码,比如编译Java标准库源码、生成类文件等。官方要求构建JDK 21必须使用JDK 20或更新的版本。注意这里的边界逻辑:不能用太老的,否则构建工具无法运行;也不建议用超出太多的版本,尽管大部分情况下兼容,但万一遇到某些代码生成逻辑对版本敏感,排查起来很耗时。

我的选择是下载一个JDK 20的官方二进制包(比如20.0.2),解压到一个短路径下,比如C:\dev\bootjdk。这里有一个很实际的经验:Windows的路径如果有空格,在MSYS2的bash里会非常烦人,每次传参都要加引号,稍不留神就把路径拦腰截断。所以尽量把Boot JDK、源码目录、第三方库全部放到无空格的短路径下面。

你可能会问,能不能用已经发布的JDK 21二进制做Boot JDK?可以,构建系统通常不会拒绝,但官方文档的建议是从上一个版本开始引导,所以我在实际操作中一直用JDK 20作为Boot JDK,稳。

2.2 MSYS2:出问题的重灾区

OpenJDK的构建脚本体系是在Unix环境里设计的,Windows下必须模拟一个基本的Unix shell环境,MSYS2是目前最主流的选择。安装本身很简单:从官网下载安装包,一路下一步。但装完之后有几个关键坑。

第一,启动Shell时有多个入口,常见的是“MSYS2 MSYS”和“MSYS2 MINGW64”。编译OpenJDK一定要用MSYS2 MSYS,不要用MINGW64。因为MINGW64环境会自动把MinGW的bin目录加入PATH,一旦configure检测到MinGW工具链,可能会误判工具链类型,甚至给你报一个“MinGW is not supported”的错。别问我怎么知道的。

第二,进入MSYS2 shell后第一件事是更新系统:

pacman -Syu

更新完如果提示关闭终端重开,就重开一下,可能需要执行两轮才能完全更新干净。

第三,安装构建需要的工具包:

pacman -S autoconf zip unzip make file diffutils findutils

这些工具看着不起眼,缺一个都会导致configure中途报错。尤其是zip和unzip,OpenJDK在打包镜像时要靠它们来压缩、解压各类资源。file工具用来识别二进制文件类型,也会被configure检查。

2.3 Visual Studio的正确安装姿势

Windows下唯一受官方支持的C++编译器是MSVC,也就是Visual Studio里的那套工具链。我用的版本是Visual Studio 2022 Community,免费版完全够用。

安装时的工作负载选择很关键,必须勾选“使用C++的桌面开发”(Desktop development with C++),它会把MSVC编译器、Windows SDK、CMake等一堆组件一起装进来。另外建议在右侧“安装详细信息”里确认Windows 11 SDK已经被选中,如果没有,手动补上。

装完之后,理论上configure可以通过vswhere自动检测到Visual Studio的位置,不需要手动配置。但如果你的机器上装了多个版本的VS,或者安装路径比较特殊,configure偶尔会找不到。这种情况下可以设置环境变量VSINSTALLDIR,把它指到VS的安装根目录。比如:

export VSINSTALLDIR="C:/Program Files/Microsoft Visual Studio/2022/Community"

注意在MSYS2的bash里,Windows风格路径用正斜杠。

顺带提醒,不要为了图省事去装MinGW-w64替代MSVC。OpenJDK官方不支持此路线,你会在编译链接阶段遇到一堆莫名其妙的ABI问题。

3. configure:参数和坑的第一次集中爆发

准备阶段做完,进入真正的“第一次集中爆发点”——configure。这一步负责检查系统、探测工具链、生成构建配置。绝大多数失败都发生在这里,但也正因为失败得早,问题通常不难修。

3.1 我的configure命令模板

先拉取源码。进入MSYS2 MSYS shell之后,切换到你准备的代码目录:

git clone --depth 1 --branch jdk-21+35 https://git.openjdk.org/jdk cd jdk

jdk-21+35是OpenJDK 21的GA版本标签。如果官方站点访问慢,也可以找代码托管镜像下载release的tar.gz包,解压后效果一样。

我的configure命令长这样:

export MSYS2_ARG_CONV_EXCL="*" bash configure \ --with-boot-jdk=/c/dev/bootjdk \ --with-freetype-src=/c/dev/freetype-src \ --with-jobs=8

逐个解释。

MSYS2_ARG_CONV_EXCL="*"是我踩过一次大坑之后养成的习惯。MSYS2默认会把某些看起来像路径的参数自动转换成Windows格式或Unix格式,这种“贴心”在复杂的configure里反而会引入混乱。设置这个环境变量等于告诉MSYS2不要自动转换任何参数,让configure脚本自己处理路径。

--with-boot-jdk指向Boot JDK的解压目录,必须是你准备阶段放的那个短路径。

--with-freetype-src指向freetype的源码目录。OpenJDK的java.desktop模块渲染字体时依赖freetype,Windows下用MSVC工具链时不能直接用MSYS2里为MinGW编译的freetype库,因为ABI不兼容。最简单可靠的方式就是下载freetype官方源码包,解压后指定源码路径,构建系统会在构建过程中自己编译一个可用的版本。这一步在缺乏图形界面依赖的开发服务器上也很实用。

--with-jobs=8是编译时的并行线程数。如果你的CPU是16核,可以设成16,但前提是内存跟得上,我后来会详细说。

3.2 每次configure都该检查的五件事

跑完configure之后,它会输出一个摘要报告,列出各项检测结果。我强烈建议你每次都在这个报告里确认五件事,缺一不可。

第一,确认“Boot JDK”正确识别为JDK 20,并且路径没有解析出额外的反斜杠或空格污染。第二,确认“Toolchain”显示为microsoft或带msvc字样,而不是gcc。第三,确认freetype状态是ok,不是not found或者failed。第四,确认build configuration里平台是windows-x86_64-server-release这样的正常格式。第五,确认没有大面积warning级别的失败项,尤其是external library dependencies部分。

这五件事里只要有一件不对,先修好再往下走,不要抱着“先make试试看”的心态。configure阶段的问题在make阶段通常会被放大三倍,到时候定位起来痛苦得多。

另外,configure的报错信息大部分会给出明确的提示,比如缺某个工具时说“Cannot find zip”,缺少编译器时说“No supported C compiler found”。如果你看到这类信息,先对照我前面准备的工具清单补装,不要盲目改configure参数绕过检测。绕过检测一时爽,make的时候往往更惨。

4. make images:正式编译与提速经验

configure成功之后,一切才刚开始。真正耗时的是make images,它会在build目录下生成一个完整的JDK镜像,包括bin目录、lib目录、conf目录,以及最关键的JVM本体。

4.1 第一轮编译的正确姿势

第一次编译建议不要加太多花哨参数,直接用:

make images

如果你的configure阶段指定了--with-jobs=8,make默认就会用8个线程。如果没有指定,可以在make命令里临时加:

make images -j8

整个编译过程分几个阶段:先是编译hotspot的C++代码,然后编译Java模块,再链接native库,最后打包JDK镜像。hotspot编译是最耗时的阶段,几百个C++文件一个接一个地编译,期间CPU会长时间跑满。

在8核16GB内存的机器上,第一轮全量构建一般需要30到60分钟。如果你的机器是4核8GB,建议把jobs设在2到4,宁可慢一点,也别中途被内存耗尽拖垮。我见过有人直接把jobs设成16,结果编译到hotspot中段时内存耗尽,编译器进程被杀,整轮构建报废,前面的时间全白费。

首次构建成功后,build目录下会出现:

build/windows-x86_64-server-release/images/jdk

这个目录就是成品JDK。用里面的bin\java.exe运行-version,你会看到类似这样的输出:

openjdk version "21" 2023-09-19 OpenJDK Runtime Environment (build 21+35-mybuild) OpenJDK 64-Bit Server VM (build 21+35-mybuild, mixed mode, sharing)

注意那个mybuild,这是因为我当时编译时在configure里加了--with-version-opt=mybuild。如果你不加,这里就显示官方风格的空版本后缀。

4.2 如何显著缩短编译时间

第一次全量构建成功之后,后续的增量构建会快很多,几分钟到十几分钟就能完成。但如果你要反复做全量构建,或者机器配置一般,有四个提速手段非常有效。

第一个手段是调整并行度。--with-jobs或者make的-j参数是立竿见影的,但要关注内存。hotspot里有些单个C++文件编译时能吃到1GB以上的内存,jobs数量乘以1.5GB做个粗略估算,别超过物理内存。

第二个手段是关闭Windows Defender对构建目录的实时扫描。这一步对速度影响之大,超出很多人的预期。Windows Defender会在每个新生成的exe、dll被写入时进行实时扫描,而JDK构建会生成成千上万个文件,扫描开销积少成多,能把构建时间拉长一倍。解决办法是在Windows安全中心的“病毒和威胁防护”里,把源码目录和build目录都加入排除列表。

第三个手段是减少不必要的JVM特性。configure支持通过--with-jvm-features控制编译进的组件,比如不需要分代ZGC或Shenandoah时可以显式剔除:

bash configure --with-jvm-features=-zgc,-shenandoahgc

这样能减少一部分C++代码的编译量。不过我要提醒,如果要拿这份JDK做性能对比实验,建议保留所有GC,否则结果不完整。

第四个手段是使用SSD。机械硬盘上全量构建OpenJDK是折磨,大量随机小文件读写会显著拖慢速度。如果条件允许,一定在SSD上构建。

5. 编译失败排查:我的问题清单和解决路径

编译OpenJDK不遇到报错几乎不可能,关键在于快速定位根因。下面是我实际碰到过并且有明确解决路径的四类问题,你可以直接对照排查。

5.1 cl.exe找不到或VS检测失败

报错形态:configure阶段出现“No supported C compiler found”,或者显式提示无法定位Visual Studio。这个问题的原因通常是VS没有安装C++工具链,或者configure没找到VS安装位置。

解决路径分三步。第一,确认Visual Studio Installer里确实勾选了“使用C++的桌面开发”。第二,确认Windows 11 SDK已安装,如果没有,去VS Installer里修改组件。第三,如果前两步都对,在MSYS2 shell里手动导出VSINSTALLDIR环境变量,指到VS的安装根目录,然后删除旧configure缓存重新执行:

rm -rf build bash configure ...

这里有一个常见的低级错误:修改环境变量后忘记删除build目录,旧配置里残留的检测结果会让configure误以为工具链仍然不可用。删掉build目录从零开始最稳。

5.2 freetype的常见报错

报错形态:configure输出类似“Could not find freetype library”,或者“The freetype test result is not usable”。根因基本只有一个:没有提供符合MSVC ABI的freetype。

网上很多教程说装MinGW的freetype包,这是不行的。因为MSVC和MinGW的ABI在结构体对齐、符号命名上都有差异,你用MinGW编译的freetype库去链接MSVC编译的JVM,static库和动态库都会出幺蛾子。

我推荐的解决方案是configure时用--with-freetype-src指向freetype源码目录。你只需要从官方下载对应版本的源码包,解压到某个短路径下,构建系统会自动把它编译成适用当前工具链的库。

如果你确实不需要图形界面,比如纯粹做一个服务端JVM实验,也可以加--enable-headless-only跳过freetype检测。但我不建议常规操作这么做,因为你后面跑AWT/Swing相关代码时会发现缺了字体渲染能力,还得回过头来补。

5.3 内存不足、编译器堆空间用尽

报错形态:make阶段出现fatal error C1060: compiler is out of heap space,或者进程被系统直接kill。根因是并行编译太多,MSVC的cl.exe进程同时吃满了物理内存。

解决路径很简单:降低jobs数量。我有个习惯,内存16GB时jobs不超过8,8GB时jobs不超过4。如果已经在编译中途爆掉,清理build目录重来,或者至少把导致失败的中间目录删掉,然后使用更保守的并行度。

另外一个容易被忽视的点:Windows的虚拟内存(页面文件)如果太小,也会加剧这个问题。在“系统属性-高级-性能设置-高级-虚拟内存”里,让系统自动管理页面文件大小通常就够了,但如果你严格设置了固定大小,建议改成4GB以上。

5.4 杀毒软件导致的构建缓慢或偶发失败

这个问题表面上不像错误,更像是玄学:编译进度一动不动,或同一个错误反复出现,换一台机器又完全正常。如果你用的是Windows自带的Defender,大概率是实时扫描在捣乱。

排查方法是打开任务管理器,选择“性能-打开资源监视器”,看磁盘活动里MsMpEng.exe进程是否长期占用很高。如果是,把源码目录、build目录加入Defender排除列表。这一步不仅解决卡顿,还能让构建时间显著缩短。

另外,如果你用了第三方安全软件,构建时暂时退出监控,等构建完成再开启。安全策略上请自己权衡,不过就我体验来看,把构建目录加入白名单是相对稳妥的做法。

6. 验证产物并跑通自定义JVM

构建成功不等于一切结束。拿到一份自己编译的JDK之后,怎么验证它可用、怎么进行后续的定制迭代,才是决定这项工作有没有价值的关键。

6.1 验证JDK是否可用

最基本的验证是把build目录下的JDK镜像加入PATH,然后运行java -version和javac -version。但光这样不够,我建议再做两件事。

第一,用一个稍微复杂的Java程序验证运行时稳定性,比如启动虚拟线程、跑一段垃圾回收密集的任务。OpenJDK 21的新功能里,虚拟线程和分代ZGC是两个重点,随便写个并发任务压一下,能暗示JVM内存管理和调度模块有没有问题。

第二,用javap -verbose检查某个编译出来的类文件版本。例如:

./bin/javap -verbose java.lang.String | head -30

确认class file major version是65,对应Java 21。这一步能帮你确认模块编译链路的完整性。

如果你后续要调试JVM native代码,建议在configure时把构建级别设为slowdebug。操作方式是在configure时加--with-debug-level=slowdebug,这样生成的JDK包含完整的调试符号,但性能和镜像大小都不适合日常使用。我通常的做法是维护两套构建:一台机器或一个目录专门做release构建用于日常实验,另一个目录做slowdebug构建用于调试。

6.2 修改一处JVM源码后的重新编译流程

当你能编译JDK并且能跑起来之后,真正的“玩具时刻”开始了。比如你想在hotspot源码里加一段日志输出,观察某个GC事件触发的时机,流程是这样的:

先定位源码文件,修改后用增量构建。绝大多数情况下,只需要再次执行:

make images

构建系统会根据文件时间戳判断哪些文件需要重新编译,hotspot的C++文件被改动后,它会把受影响的目标文件重编一遍,再重新链接JVM动态库,整个过程通常只要几分钟到十几分钟,远快于全量构建。

这里有两个经验。第一,修改C++代码后,如果新增了非静态全局变量,要注意符号导出问题;最好保持改动尽量局部化,避免牵扯到跨平台的接口定义。第二,如果你改了Java标准库代码,构建系统会重新编译对应模块,生成新的class文件,验证时务必用build/windows-x86_64-server-release/images/jdk里的完整镜像,而不是只替换某个jmod文件,否则会踩到运行时模块校验的坑。

我一开始做自定义修改的时候,有个坏习惯:源码改了十处,觉得没必要每次重新构建,攒到晚上再一起编译。结果一系列小改动彼此影响,排错排到怀疑人生。后来学乖了,每次只改一个点,验证通过再做下一次改动。增量构建这么便宜,没必要攒。

还有一点,修改源码时最好在源码仓库里创建自己的分支,并给tag留好记录。虽然个人实验不需要复杂管理,但至少每次改动前用git diff看一眼自己到底改了哪些文件。我之前有次改乱了hotspot的某个文件,想恢复却记不清原始内容,最后只能重新拉源码,白白浪费了重新配置和编译的时间。

Windows 11下编译OpenJDK 21,说难其实不难,说简单也确实劝退了不少人。整个流程最大的障碍不是某个技术点有多深,而是它横跨了Windows环境下的多个子系统:MSYS2的Unix模拟、MSVC的Windows原生工具链、autoconf的脚本逻辑、庞大的C++源码树。任何一个环节的文档都算充分,但链接成一条完成链路时,细节和坑才是真正的门槛。

我个人在实际操作中的体会是:别急着追求全自动构建,第一次就老老实实按手动步骤来,搞清楚每一行configure参数在做什么。等你完整走通一遍之后,再考虑写自动化脚本。脚本能帮你节省时间,却没法帮你理解问题;而对编译OpenJDK这件事来说,理解问题本身才是最有价值的部分。最后再分享一个小技巧:编译成功后,把整个build目录压缩保存一份。之后无论是做增量实验还是对比调试,你都有个干净且确定可用的基线,这个习惯后来帮我省了不止一个周末。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询