简介:OpenJDK 21 Windows x64压缩包面向需要在64位Windows环境中体验最新Java特性的开发者,属于非LTS版本,包含完整JDK运行时与编译、文档生成等核心工具,适合Java工程师、学生或研究者测试JEP新功能或进行日常开发。压缩包共包含484个文件,其中dll、jmod、exe等组件分别对应动态链接库、模块镜像和命令行工具,license授权文件70个、policy策略文件7个等授权与安全类内容齐备,整体压缩包大小约189.2MB;解压后遵循标准JDK目录结构,bin、lib、conf等模块划分明确,便于快速定位所需内容。目前已有571人学习下载,资源开箱即用,无需额外配置即可运行javac、java、javadoc等常用命令。借助该包可提前体验虚拟线程、记录模式、作用域值等JEP新特性,并基于GPLv2协议规避商业授权风险;对于关注Java演进趋势的开发者而言,既可满足日常开发之需,也是测试新功能的实用入口。
1. 从 Windows x64 的 OpenJDK 21 安装包聊起:这资源到底解决什么事
如果你的 Windows 服务器上还跑着 Java 8 时代的系统,而某天你需要用 JDK 21 的虚拟线程、ZGC 或者只想在交付前先评估新特性,这时候你手头大概率缺一个解压就能用的 OpenJDK 21 Windows x64 构建包。这个资源就是拿来填这个缺口的。它不是教程,而是一个可以直接落地的构建包,自带 java、javac、jlink、jmods 全套工具,省掉你自己从源码编译的功夫。它解决的问题很直接:不用在几个发行版之间反复比对,不用临时去翻各种下载页,拿到手配好环境变量就能跑。适合两类人:一类是要在多台 Windows 机器上统一交付运行时的人,另一类是只想在旧系统里尝鲜 JDK 21 又不想折腾开发环境的人。
2. 安装方式选型:zip 解压版与安装版到底差在哪
2.1 安装包目录结构:一张表理解六个目录的职责
很多人下载完 OpenJDK 21 压缩包,解压后看到一堆目录就开始慌。其实每个目录的职责非常固定,我拆这份资源时一般先按下面这张表核对一遍。
| 目录 | 放了什么 | 你什么时候会碰它 |
|---|---|---|
| bin | java.exe、javac.exe、jlink.exe、jcmd.exe、jdeps.exe 等可执行文件 | 配 PATH 时把 bin 加进去 |
| conf | 全局默认的 security、logging、management 配置 | 改 JVM 默认 TLS、日志级别时用 |
| include | 写 JNI 需要的 C/C++ 头文件 | 做本地方法接口时才会碰 |
| legal | 各模块的许可证与版权信息 | 交付审计时一般要保留 |
| lib | 模块化运行时镜像、静态库、JSP 编译辅助文件 | 日常不用手动动 |
| jmods | JMOD 格式的模块包 | 做 jlink 自定义运行时、打包时用 |
对照完这张表,你基本就能判断手上的包是不是完整。常见做法是检查 bin 和 jmods 两个目录,因为 jlink 裁剪运行时完全依赖 jmods,少一个都会在生成镜像时报错。注意,从 JDK 9 开始,JDK 目录里不再默认出现独立 jre 子目录,旧时代那些把路径指到 jdk\jre\bin 的脚本,在这份资源里会直接失效。
2.2 选 zip 还是选安装版:从交付场景看
同一份 OpenJDK 21 构建,通常会以 zip 和 msi 两种形态提供。zip 版没有注册表、没有卸载程序、没有安装向导,解压后就是一个自包含的目录;msi 版会自动写入环境变量、注册 .jar 文件关联、提供卸载入口。
做交付时我更倾向 zip 版。原因很简单:可控。你想把 JDK 放哪个盘、目录叫什么名字都由你定,出问题时整个目录删掉就是后悔药;而 msi 版会在系统里留下注册表痕迹,多版本共存时卸载一个可能连带影响另一个。个人开发机上用 msi 会省事一些,双击安装完就能用,但代价是你不太清楚它到底往环境变量里写了什么。我给某跨平台系统做环境标准化时,统一规定所有机器必须用 zip 版放在固定根目录,就是为了让每台机器行为完全一致。
2.3 动手:zip 解压版的安装步骤
zip 版安装本质上就是解压,但解压目录的选择有讲究。我一般会把它放在一个独立的应用目录下,比如 C:\apps\jdk,而不是直接放 C:\Program Files,因为 Program Files 路径带空格,某些老脚本拼接路径时容易翻车。
# 假设安装包已经下载到当前用户的 Downloads 目录 $dest = 'C:\apps\jdk' Expand-Archive -Path "$env:USERPROFILE\Downloads\openjdk-21-win-x64.zip" -DestinationPath $dest -Force Get-ChildItem $destExpand-Archive 的 -DestinationPath 指定解压目标根目录,-Force 表示目标目录已存在时强制覆盖。解压后要留意:压缩包内部通常带有一层目录,比如 jdk-21.0.1,所以实际 JDK 根路径是 C:\apps\jdk\jdk-21.0.1,而不是 C:\apps\jdk。如果直接拿 C:\apps\jdk 当 JAVA_HOME,后面配置环境变量时大概率找不到 bin\java.exe。
2.4 动手:安装版(msi)的静默安装与卸载
如果你拿到的是 msi 形态,可以用命令行静默安装,避免安装向导里那些默认勾选影响后续判断。
# 管理员权限的 PowerShell 中执行 msiexec /i openjdk-21-win-x64.msi /quiet ADDLOCAL=FeatureMain,FeatureEnvironment,FeatureJarFileRunAs,FeatureJavaHomeADDLOCAL 参数里的 FeatureMain 是安装核心文件,FeatureEnvironment 表示写入 PATH,FeatureJavaHome 表示写入 JAVA_HOME,FeatureJarFileRunAs 控制是否把 .jar 文件关联到 java.exe。卸载时用同一份 msi 文件即可。
msiexec /x openjdk-21-win-x64.msi /quiet我一般不建议在服务器上用 msi 的默认双击安装方式,因为很多安装包把 JAVA_HOME 写到默认路径,和你自定义的安装目录对不上,后面排查时相当头疼。
2.5 目录结构验证:用命令确认安装是否完整
解压完不要急着配环境变量,先验证一下关键文件是否存在。
$jdkHome = 'C:\apps\jdk\jdk-21.0.1' @('bin\java.exe', 'bin\javac.exe', 'bin\jlink.exe', 'jmods\java.base.jmod') | ForEach-Object { [PSCustomObject]@{ File = $_; Exists = Test-Path (Join-Path $jdkHome $_) } }这段命令会输出四个关键文件的检测结果。java.exe 是运行入口,javac.exe 是编译器入口,jlink.exe 后面做精简运行时要用,java.base.jmod 是模块系统的基础包。只要这四个文件齐全,这个构建基本是完整的。如果 java.exe 缺失且 Test-Path 返回 False,先别急着重新下载,后面避坑章节里提到的杀毒软件隔离就是常见元凶。
3. 环境变量才是关键:JAVA_HOME 与 PATH 的实操细节
3.1 JAVA_HOME 为什么不能指向 bin 子目录
配置环境变量时,最常见的错误就是把 JAVA_HOME 写成 C:\apps\jdk\jdk-21.0.1\bin。从表面看,似乎 jdk 目录里能执行的就是 bin,指向它没毛病,但很多构建工具并不会直接读取 java.exe 的绝对路径,而是通过 JAVA_HOME 去拼接 JAVA_HOME\bin\java.exe。一旦 JAVA_HOME 已经指向 bin,拼接结果就变成 bin\bin\java.exe,必然找不到。
正确写法是 JAVA_HOME 指向 JDK 根目录,即包含 bin、conf、jmods 那一层,然后 PATH 里追加 %JAVA_HOME%\bin。这个规则从 Java 8 时期就是这样,到了 JDK 21 依然适用。我见过某开发者在配置脚本里把 JAVA_HOME 写错成 bin 之后,javac -version 正常而 java -version 报错,查了半天才发现是两套路径拼出来的结果。
3.2 PATH 顺序与多版本共存
Windows 的 PATH 是从左到右按顺序查找的,先命中的先用。如果机器上同时装了 JDK 17 和 JDK 21,而旧版本的 bin 路径排在前面,你执行 java -version 看到的永远是 17,哪怕你刚把 JAVA_HOME 改成了 21。这不是配置没生效,只是 PATH 顺序在从中作梗。
排查这类问题时,我一般先执行 where.exe java 来看当前命中的路径到底是什么。这个命令会把 PATH 里所有匹配 java.exe 的路径按顺序列出来,第一行就是最终生效的那个。如果第一行是某个老 JDK 的 bin,那就把新的 %JAVA_HOME%\bin 追加到 PATH 最前面,而不是末尾。
3.3 设置环境变量的 PowerShell 代码示例
设置环境变量最常见的方式是图形界面,但脚本化交付时我更推荐直接用 PowerShell 操作。下面这段代码把 JDK 21 写入系统级环境变量。
$jdk = 'C:\apps\jdk\jdk-21.0.1' [Environment]::SetEnvironmentVariable('JAVA_HOME', $jdk, 'Machine') $regPath = 'HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager\Environment' $oldPath = (Get-ItemProperty -Path $regPath).Path if ($oldPath -notlike "*$jdk*bin*") { Set-ItemProperty -Path $regPath -Name Path -Value "$jdk\bin;$oldPath" }这里有几个细节值得说明。用 [Environment]::SetEnvironmentVariable 设置 JAVA_HOME 时,第三个参数 'Machine' 表示系统级变量,对所有用户生效。设置 PATH 时我刻意用了 Get-ItemProperty 去读注册表原始值,而不是 [Environment]::GetEnvironmentVariable,因为系统级 Path 通常是 REG_EXPAND_SZ 类型,里面存着 %SystemRoot%\System32 这类未展开的引用。用 .NET 的读取方式会把 %SystemRoot% 展开成实际路径,写回去时可能破坏原有结构;PowerShell 的 Get-ItemProperty 读注册表原始值则不会展开,相对安全。
另外,我不推荐用 setx 命令去写 PATH。setx 有 1024 字符截断的毛病,系统 PATH 稍长一点就可能被拦腰斩断,到时候重启机器各种命令找不到,非常头疼。
3.4 终端与系统环境变量的刷新机制
很多人在环境变量设置完成后,立刻在当前终端里执行 java -version,发现还是旧版本,就以为配置失败。其实 Windows 的环境变量广播只通知新启动的进程,已经打开的终端进程不会收到更新。每个进程在启动时会把环境变量拷贝成自己的环境块,之后系统层面的改动对它无效。
想验证配置是否真正生效,重开一个终端最直接。如果不想重开,可以在当前 PowerShell 会话里手动同步机器级 PATH,让当前进程立即看到新值。
$machinePath = [Environment]::GetEnvironmentVariable('Path', 'Machine') $env:Path = "$machinePath;$env:Path"这段只影响当前 PowerShell 进程,不影响系统配置。同步后执行 java -version 就能立即看到 21 的版本输出。注意,这里用 .NET 读取只是为了让当前进程看到展开后的完整值,不涉及写回,所以没有安全问题。
3.5 验证:java -version / javac -version / where java
环境变量配完,我会固定执行三组命令验证。
java -version javac -version where.exe javajava -version 的输出分为三行,第一行是版本字符串,第二行是运行时构建标识,第三行是 HotSpot 虚拟机信息。重点看第一行是不是 21.0.x 开头,而不是 1.8.x 或 17.0.x。javac -version 直接输出编译器版本,如果 java 是 21 而 javac 是 17,说明 PATH 里同时存在两个 JDK 的 bin 目录,且旧版的 javac 排在了前面。where.exe java 列出的第一个路径,才是 java 命令最终解析到的位置。如果这个路径带有旧版 Java 自动写入的公共目录影子,记得从 PATH 里清掉。
4. 拿到安装包后最容易翻车的五个点:避坑手记
4.1 javac 找不到,java 却正常
现象:java -version 输出正常,但执行 javac 时报“不是内部或外部命令”。
原因:很多旧脚本在配置 PATH 时,把路径指向了 JDK 目录下的 jre\bin,或者指向了某个没有 javac.exe 的公共运行时目录。JDK 21 的构建包里不再有独立的 jre 子目录,编译工具只存在于 JDK 根目录的 bin 下面,运行时目录里自然找不到 javac。
解决:把 PATH 里的 %JAVA_HOME%\bin 调整为第一个命中的路径。调整后执行 where.exe javac 确认 javac.exe 位于 JAVA_HOME 下的 bin 目录,再执行 javac -version 验证。
4.2 杀毒软件隔离了 java.exe
现象:解压后 Test-Path 检查文件不存在,或者运行 java.exe 时没有任何响应,服务启动直接失败。
原因:某些基于 OpenJDK 的 Windows 构建包,二进制特征可能触发安全软件误报,java.exe 在解压瞬间就被隔离到隔离区,目录里留下一个空位。
解决:先把 JDK 根目录加入安全软件的排除列表,再重新解压。解压完成后,用官方源提供的 SHA-256 值核对一次文件完整性。
Get-FileHash -Path .\openjdk-21-win-x64.zip -Algorithm SHA256复制输出的哈希值,和下载页面标注的哈希做比对。只要一致,基本可以排除下载损坏,剩下的就是安全软件误报。遇到这种情况,把隔离区里的 java.exe 恢复并加入白名单即可。
4.3 设置环境变量后新终端仍是旧版本
现象:在图形界面改了 JAVA_HOME 和 PATH,也重开了终端,java -version 依然显示 17。
原因:Windows 在合并 PATH 时,用户变量里的 PATH 会排在系统变量之前。如果用户变量里残留了一个旧 JDK 的 bin 路径,系统变量里新加的内容再正确也会被它压在后面。另一个叠加因素是环境变量广播机制没有覆盖到新终端继承的父进程环境块。
解决:打开环境变量编辑器,分别检查用户变量和系统变量里是否各有一份 JAVA_HOME 和 Path。把用户变量里的旧 JDK 路径删掉,只保留系统变量里的新条目。修改完成后不要从旧终端直接开新窗口,而是从任务管理器完全结束 explorer 或重新登录,确保新进程拿到的是完整的新环境块。
4.4 MSI 安装后 JAVA_HOME 指向的路径不一定是你的安装路径
现象:使用 msi 安装时自定义了安装目录,安装完成后检查 JAVA_HOME,发现它指向默认安装路径,或者根本没有写入。
原因:msi 安装时通过 ADDLOCAL=FeatureJavaHome 触发环境变量写入,但部分构建包写入 JAVA_HOME 时依赖安装器记录的 InstallDir 属性。当用户通过向导界面修改安装路径后,部分记录可能没同步到环境变量写入逻辑里。
解决:不要依赖 msi 自动写入的 JAVA_HOME。安装完成后手动执行第三章里的 PowerShell 设置代码,把 JAVA_HOME 指向你的真实安装目录。卸载时,先用 msiexec 卸载,再手动清理注册表中残留的 JavaHome 键值,避免下次安装时把旧路径带出来。
4.5 解压报错或运行提示“不是有效的 Win32 应用程序”
现象:解压 ZIP 时提示文件损坏,或者运行 java -version 直接报“不是有效的 Win32 应用程序”。
原因:把 64 位的构建包放到了 32 位系统上运行;或者下载中途断流,ZIP 文件尾部没有完整写完。
解决:先确认系统架构是 x64,控制面板或 PowerShell 里都能看到。如果系统是 32 位,这份资源本身不适用,别花时间折腾。如果系统是 x64 但解压失败,重新下载后用 Get-FileHash 核对哈希值,确认文件完整后再解压一次。
提示:OpenJDK 21 的官方构建已经全面面向 x64 及更高位架构,32 位 Windows 不在支持范围内,遇到兼容性报错先查系统架构比什么都实在。
5. 把 JDK 21 用起来:虚拟线程、ZGC 与 jlink 裁剪的最小可运行场景
5.1 确认你手头的构建支持这些特性
JDK 21 是一个长期支持版本,虚拟线程默认开启,ZGC 已经不再是实验特性,但具体到你下载的这个构建包,最好还是先验证一下。
java --version java --help-extra--help-extra 会列出与 JVM 调优和诊断相关的全部参数,如果输出里能看到 -XX:+UseZGC,说明 ZGC 可用。虚拟线程不需要额外参数,它是 Java 语言层面的标准 API,只要 java -version 显示 21.0.x,就可以直接使用 Thread.ofVirtual()。
5.2 虚拟线程最小示例:从 Java 代码到运行
虚拟线程是 JDK 21 这次升级里最值得先体验的东西。它的最小示例不需要引入任何依赖,一个单文件就能跑通。
// VirtualThreadDemo.java public class VirtualThreadDemo { public static void main(String[] args) throws Exception { Thread task = Thread.ofVirtual() .name("demo-thread") .start(() -> { System.out.println("run in: " + Thread.currentThread()); }); task.join(); } }编译和运行分别用 javac 和 java。
javac VirtualThreadDemo.java java VirtualThreadDemo代码里 Thread.ofVirtual() 创建的是虚拟线程,name 方法给它指定一个便于日志识别的名字,start 方法启动任务,join 方法让主线程等待任务执行完成。运行时输出会看到类似 run in: VirtualThread[#21,demo-thread] 的字样,中间的 #21 是线程内部编号,这个编号和 JDK 21 没有直接关系,只是恰好数字相同。与平台线程相比,虚拟线程创建成本低得多,适合高并发 IO 密集场景,这也是我推荐先跑通它的原因。
5.3 如何打开 ZGC:参数说明与注意事项
ZGC 在 JDK 21 已经可以用于生产环境,但默认 GC 仍然是 G1,要用 ZGC 需要显式指定。
java -XX:+UseZGC -XX:MaxGCPauseMillis=50 -jar 某应用.jar-XX:+UseZGC 负责启用 ZGC 垃圾回收器,-XX:MaxGCPauseMillis=50 表示期望最大暂停时间在 50 毫秒以内。注意这个值是软目标,ZGC 会尽量向这个指标靠近,但极端情况下仍然可能出现更长的暂停。对于堆内存较大的服务,ZGC 的并发处理能力能显著降低停顿,这也是它在高容量场景下受欢迎的原因。
如果你的构建包支持并行分代模式,可以追加 -XX:+ZGenerational。有些构建需要加 -XX:+UnlockExperimentalVMOptions 才能识别这个参数,遇到 Unrecognized VM option 报错时先补这个开关。验证 ZGC 是否真正生效,可以用 jcmd 查运行中 JVM 的参数。
jcmd <pid> VM.flags | findstr "GC"输出里出现 UseZGC=true 就说明 GC 已经是 ZGC。
5.4 jlink 裁剪出一个精简运行时
完整 JDK 解压后通常有几百兆,交付时往往只需要运行环境。jlink 可以把模块化 JDK 裁剪成只包含必要模块的精简运行时,这是 JDK 21 自带的功能,也是我每次交付必做的一步。
$jdkHome = 'C:\apps\jdk\jdk-21.0.1' & "$jdkHome\bin\jlink.exe" ` --add-modules java.base,java.logging,java.naming ` --output 'C:\apps\jdk\mini-runtime' ` --strip-debug ` --no-header-files ` --no-man-pages ` --compress=zip-6--add-modules 指定保留的模块,java.base 是根模块,java.logging 提供基本日志能力,java.naming 供 JNDI 使用。--output 指定生成目录,注意这个目录必须不存在,jlink 不会覆盖已有目录。--strip-debug 去掉调试符号,--no-header-files 和 --no-man-pages 分别去掉头文件和帮助文档,--compress=zip-6 用压缩级别 6 压缩模块镜像。
裁剪完成后,直接运行精简目录里的 java.exe 验证。
C:\apps\jdk\mini-runtime\bin\java.exe -version如果应用还依赖其他模块,运行时启动报错后,把报错信息里提到的模块名补齐到 --add-modules 里重新生成即可。
5.5 启动失败排查:模块缺失与运行时损坏
精简运行时最常见的问题就是启动时报模块找不到。比如某个应用用到了 CORBA 相关接口,而你只加了 java.base,启动时可能直接提示找不到某个类。解决方法不是去下载完整 JDK,而是回到 jlink 那一步,把缺的模块加进 --add-modules 再生成一次。
还有一类报错是运行时试图访问 jrt-modules 镜像失败,通常是精简目录被移动过或者权限不够。保证 mini-runtime 目录路径中没有空格,而且所有文件都是可读状态。从 jlink 生成镜像到实际运行之间,不要手动删除任何子目录,模块镜像的完整性依赖整个目录结构。
6. 进阶技巧:一份安装包如何优雅地管理多个 JDK 21 实例
6.1 用目录链接实现版本级切换
同一个 JDK 21 可能会有多个 update 版本,继续沿用改系统 PATH 的方式,每次都要维护一串长路径,容易把系统目录环境搞乱。我后来习惯用一个固定链接指向当前生效的 JDK,让 JAVA_HOME 永远指向这个链接,换版本时只改链接指向。
mklink /D C:\apps\jdk\current C:\apps\jdk\jdk-21.0.1mklink 的 /D 参数表示创建目录符号链接。执行后 C:\apps\jdk\current 就会指向 jdk-21.0.1。注意创建符号链接需要管理员权限或在系统中开启开发者模式,否则会提示权限不足。
6.2 写一个 PowerShell 切换脚本
手动敲 mklink 不难,但多版本切换时每次都要重定义 JAVA_HOME,容易漏步骤。我写了一个简单的切换脚本,把整个过程收在一个命令里。
# Switch-Jdk.ps1 param([Parameter(Mandatory)]$Version) $base = 'C:\apps\jdk' $target = Join-Path $base "jdk-$Version" if (-not (Test-Path "$target\bin\java.exe")) { Write-Error "找不到 $target 下的 java.exe" exit 1 } $link = Join-Path $base 'current' if (Test-Path $link) { (Get-Item $link).Delete() } New-Item -ItemType SymbolicLink -Path $link -Target $target | Out-Null [Environment]::SetEnvironmentVariable('JAVA_HOME', $link, 'User') $userPath = [Environment]::GetEnvironmentVariable('Path', 'User') -split ';' $newPath = ($userPath | Where-Object { $_ -notlike "$base\*" }) -join ';' [Environment]::SetEnvironmentVariable('Path', "$link\bin;$newPath", 'User')脚本接收一个版本参数,比如 21.0.2,然后拼出目标目录 jdk-21.0.2。Test-Path 检查目标目录里的 java.exe 是否存在,不存在就直接报错退出,避免链接指向空目录。存在则删除旧链接,创建新链接指向目标版本。
环境变量部分只改用户级,不碰系统级,这样不需要管理员权限,也不会有改坏系统 PATH 的风险。更新 PATH 时,先把旧的 $base 开头的路径全过滤掉,再把新链接的 bin 放到最前面,保证版本切换彻底干净。
6.3 验证切换是否生效的完整动作
切换完成后,我会按固定顺序做三件事:确认 JAVA_HOME 指向链接、查看 java 版本、查看链接实际指向。
echo $env:JAVA_HOME java -version Get-Item C:\apps\jdk\current | Select-Object Target第一次输出应该显示 C:\apps\jdk\current,第二次显示 21.0.x 的版本号,第三次的 Target 属性指向真实的 jdk-21.0.x 目录。三件事都符合预期,这次切换才算完成。
以前我也喜欢直接改系统变量,有次在给某台机器配置时把系统 Path 里的原有引用弄丢了,重启后一堆命令找不到,那台机器上的服务全部起不来,光恢复就折腾了一个多小时。从那以后,我每次换 JDK 版本都强制走一遍链接加脚本这套流程,宁可多写两行脚本,也不再去直接碰系统 Path。希望这个习惯对你有帮助。
本文还有配套的精品资源,点击获取