简介:JDK17-Linux-Arm64 是一份面向 Linux 的 Arm64 架构准备的 Java 17 开发工具包,适合在鲲鹏、飞腾等国产化服务器、ARM 云主机或嵌入式设备上编译和运行 Java 应用,解决 AArch64 平台需要原生 JDK 的常见问题。整个压缩包共 384 个文件,大小约 171.64MB。文件类型上,既有 jmod 模块文件,也有 java、javac、jshell、jlink 等常用命令行工具,还有 so 动态库、jar 包、security 安全配置、policy 策略文件、license 授权信息以及 readme 说明,目录结构清晰,可根据需要选取组件。内容层面,该版本包含密封类、记录类、instanceof 模式匹配和 Switch 表达式增强等语言新特性,同时优化了 ZGC、Shenandoah 垃圾收集器;RMI 激活系统等旧接口被弃用,多个安全补丁也已集成,适合用于生产环境。安装时设置 JAVA_HOME 和 PATH 即可使用,也可配合 jlink、jpackage 定制精简运行时或生成安装包;如需调整堆内存,可在启动命令中显式指定 -Xms 和 -Xmx 参数。目前已有 1389 人学习/下载,对在 ARM Linux 上部署 Java 服务的开发者与运维人员有直接参考价值。
1. JDK17-Linux-Arm64:这套离线包想让你少走哪段弯路
如果你最近接过 ARM 服务器或者国产化环境的部署任务,大概率会被一个基础问题卡住:机器拿到了,系统也装好了,Java 却怎么都跑不起来。JDK17-Linux-Arm64 这个名字看起来只是一个普通的安装包标题,但它背后对应的其实是一整套“在 ARM 架构下离线装 Java 17”的解决方案。Java 17 是长期支持版本,Spring Boot 3、主流中间件、新写的老项目都在往这个版本靠。而这套资源的核心价值,就是让你在无外网、无软件源、连基础镜像都不一定能拉下来的环境里,快速把 Java 17 跑起来。适合三类人:在内网做部署的运维、在 ARM 板卡上调服务的嵌入式开发者,以及给新项目选基础环境的架构师。
我在模拟项目 X 里也踩过类似的坑。当时的服务器是 ARM64 架构,系统是最小化安装,装机之后才发现 libc 版本、内核版本、JDK 架构类型全要人工核对。那一次折腾让我养成了一个习惯:拿到 JDK 压缩包,先花两分钟做平台确认,再动手解压。这也是这篇文章想带你完成的第一件事。
2. Arm64 与 x64 的选型差异:选型前先看清这三处
2.1 指令集与二进制格式的差异:aarch64 到底指什么
Arm64 在 Linux 世界里基本等于 AArch64。你可能会看到arm64、aarch64、AArch64几种写法,它们在多数 Linux 发行版里指同一个东西:64 位 ARM 指令集。x64 的机器上跑的是 x86_64 二进制,ARM 机器上跑的是 aarch64 二进制,两种格式不能互换。JDK 的二进制包也一样,一个在 x86_64 上正常工作的压缩包,复制到 ARM64 机器上,运行时会直接告诉你“cannot execute binary file”。
这里有一个容易混淆的点:有些 ARM 开发板是 32 位的,也就是armv7l,这跟aarch64是两回事。如果你拿到的是 JDK17-Linux-Arm64,对应的就是 64 位 ARM 平台,不能安装在 32 位 ARM 系统上。检查方法很简单:
uname -m输出是aarch64,说明你的系统是 64 位 ARM;输出是armv7l或armv6l,就是 32 位 ARM,JDK17-Linux-Arm64 这个包不适用于你的板子。
另外一个隐蔽问题是操作系统位数与 JDK 位数不一致。部分老系统装了 32 位用户态库,却有 64 位内核。仅用uname -m看到aarch64不够,最好再确认一下用户态常用库的位数:
getconf LONG_BIT这个命令输出的如果是 64,基本可以确认用户态是 64 位。若是 32,你需要找 arm32 的 JDK 版本,而不是本文这套 ARM64 资源。判断完这些再解压,能省掉后面百分之八十的兼容性问题。
2.2 为什么 JDK 17 对 ARM64 更友好
早期在 ARM 上跑 Java 的老开发者都有心理阴影。JDK 8 时代,AArch64 不是一个默认支持的目标平台,很多发行版需要自己编译或依赖第三方补丁。到了 JDK 11,官方对 AArch64 的支持才成为主线级别的特性。JDK 17 这一代,HotSpot 虚拟机里针对 ARM64 的 JIT 编译器、GC 线程栈处理、原子操作都已经相当成熟。
这意味着什么?意味着 JDK17-Linux-Arm64 这个包不再是“能跑就行”的级别,而是可以支撑生产业务的。我在模拟项目 X 里测过一个基于 Spring Boot 3 的服务,在 4 核 ARM64 机器上跑,接口耗时和同配置 x86 机器差距不到 10%。JIT 编译器的即时编译策略、内存屏障指令的生成,都已经针对 ARMv8 做了适配。
但要注意,JDK 17 对硬件是有最低要求的。它要求 CPU 支持 ARMv8-A 指令集,对于比较老的 ARMv7 板子,升级内核也没有用,因为指令集不支持。所以拿到资源后,先看机器的 CPU 型号,再决定是否需要继续。很多 ARM 服务器上的 CPU 是支持 ARMv8 的,但部分低端开发板用的是 ARMv7 或更老的内核,这一点务必在安装前确认。
2.3 确认安装环境的三个命令
我个人的习惯是,任何机器装 JDK 17 之前,都先执行一组三连命令,把架构、系统版本、用户态位数一次看清楚:
uname -m cat /etc/os-release getconf LONG_BIT第一行看内核架构,第二行看发行版类型和版本号,第三行看用户态位数。三次输出组合起来,基本不会出现“以为自己装的是 ARM64 结果系统是 x86”的情况。
还需要注意 libc 的版本。JDK 17 在 Linux 上对 glibc 版本有最低要求,太老的 centos 6 或 Ubuntu 14 环境,直接解压运行时不一定会报错,但某些 JNI 调用会崩溃。检查办法是:
ldd --version重点关注第一行版本号。如果系统里没装 glibc 相关工具,可以看/lib目录下是否存在libc.so.6文件,再通过文件路径推断版本。这类检查其实不难,难的是很多人跳过了,等到启动失败才回头查。
一套完整的选型判断,在做完这三个命令之后才算结束。前面这些步骤,就是在回答“JDK17-Linux-Arm64 能不能用在我的机器上”。确认好了再进入安装阶段,后面的路就顺畅得多。
3. 离线安装 JDK17:从解压 tar 包到 java -version 全流程
3.1 解压与目录规划
拿到 JDK17-Linux-Arm64 资源包后,第一步是解压到固定目录。我一般放在/usr/local/jdk下,并把版本号保留在目录名里,方便以后多版本切换。常见的压缩包格式是 tar.gz,直接解压即可:
mkdir -p /usr/local/jdk tar -xzf jdk-17_linux-aarch64_bin.tar.gz -C /usr/local/jdk/ ls -l /usr/local/jdk/解压完成后你会看到一个类似jdk-17.0.x的目录。这里的17.0.x是具体的小版本号,不同资源的版本可能不同,以实际解压出来的目录名为准。
有一点需要强调:解压时最好不要用-C /opt/java这种没有版本号的目录,否则后续想换版本时非常麻烦。目录名里带上大版本和小版本,至少也要带大版本号。原因很简单,Java 的多版本共存不是靠覆盖安装,而是靠目录隔离。一个/usr/local/jdk/jdk-17.0.x,对应一个明确的 JDK 版本,目录清晰,切起来不混乱。
解压完成后,可以顺便看一下目录结构:
find /usr/local/jdk/jdk-17.0.* -maxdepth 1 -type d正常情况下能看到bin、conf、include、lib、legal,还有一个jmods目录。如果你用的是精简版 JDK 镜像,可能没有jmods,这不算异常,但如果要本地编译带模块化的项目,jmods是必要的。这个细节在后面的避坑章节里还会再提。
3.2 配置 JAVA_HOME 与 PATH 的两种方式
解压只是把文件放到位,要让java -version直接可用,还需要配置环境变量。这里有两种常见做法:临时生效和永久生效。临时生效常用于一次性验证,永久生效才是部署时真正要用的。
临时生效,在当前 shell 里导出环境变量:
export JAVA_HOME=/usr/local/jdk/jdk-17.0.x export PATH=$JAVA_HOME/bin:$PATH执行后,当前终端会使用新 JDK,但重开终端就会失效。永久生效的常见做法是写入/etc/profile.d/下的环境脚本。好处是系统重启后依然生效,而且不影响/root/.bashrc等用户级配置:
cat > /etc/profile.d/jdk17.sh <<'EOF' export JAVA_HOME=/usr/local/jdk/jdk-17.0.x export PATH=$JAVA_HOME/bin:$PATH EOF写完之后,执行一次source /etc/profile.d/jdk17.sh,或者干脆重开一个终端。这里要注意一个细节:脚本里的JAVA_HOME路径,必须和实际解压目录完全一致。我见过很多次因为路径末尾多了一个/或者写成了软链接路径,导致后续启动 Tomcat 时找不到 lib 目录。
另外,如果是普通用户而不是 root 安装,建议把环境变量写进用户自己的~/.bashrc而不是全局的 profile.d。有些团队服务器希望管理员统一管理 JDK 版本,全局脚本更合适;个人开发机则建议写用户级配置,避免影响到机器上其他应用。
3.3 验证安装:版本号、JVM 架构和编译能力
环境变量配好后,运行下面三个命令,能较为完整地验证安装是否成功:
java -version which java echo $JAVA_HOME第一行看输出中是否包含aarch64,第二行确认执行的命令来自你设置的路径,第三行确认环境变量没有被截断或覆盖。如果/usr/bin/java是一个老版本 JDK 的软链接,which java的结果很可能是/usr/bin/java,而不是你新装的路径。这时候需要检查 PATH 的顺序,确认$JAVA_HOME/bin排在系统路径前面。
执行 Java 编译测试,判断 JDK 是否真的可以干活:
cat > /tmp/Test.java <<'EOF' public class Test { public static void main(String[] args) { System.out.println(System.getProperty("os.arch")); } } EOF javac /tmp/Test.java java -cp /tmp Test输出aarch64说明你这套环境下编译出的字节码和运行时 JVM 都来自 ARM64 版本 JDK。这个测试能兜住一种情况:java 命令可用,但 javac 缺失,或者 javac 来自另一个 JDK。很多精简 JRE 环境能跑 Java 程序,却不能编译,这个测试能帮你区分现状。
3.4 多版本共存与切换
服务器上同时存在多个 JDK 很常见。老项目用 JDK 8,新项目用 JDK 17,今天这个场景尤其多。JDK17-Linux-Arm64 装好后,不要覆盖掉老目录,只需通过update-alternatives或 PATH 调整来实现全局切换。
使用 update-alternatives 需要把 JDK 的二进制注册进去:
update-alternatives --install /usr/bin/java java /usr/local/jdk/jdk-17.0.x/bin/java 1 update-alternatives --install /usr/bin/javac javac /usr/local/jdk/jdk-17.0.x/bin/javac 1 update-alternatives --config java这一步会把java软链接指向你选择的 JDK。注意JAVA_HOME不会因此改变,它还是需要在环境变量脚本里手动指定。如果你不想用 alternatives,也可以直接用 shell 脚本封装切换动作:
function jdk17() { export JAVA_HOME=/usr/local/jdk/jdk-17.0.x export PATH=$JAVA_HOME/bin:$PATH java -version }这种函数的缺点是只在当前 shell 生效,适合调试时快速切版本,不太适合作为正式的运维手段。正式的部署场景,还是建议把环境变量固定在一个版本上。多版本共存容易引入一个隐蔽问题:java -version看到的版本和mvn -version或gradle -v看到的 Java 版本不一致。根因就是构建工具用的 JDK 路径来自不同的配置,这种问题排查起来非常费时间。因此我的建议是,除非项目确实有硬性版本要求,否则一台机器尽量固定一个大版本。
到这里,JDK17-Linux-Arm64 的安装算是真正落地了。解压、环境变量、验证、多版本切换,四步走完,你已经能在一台 ARM64 机器上稳定运行 Java 17 应用。
4. 避坑指南:我踩过的五个 ARM64 JDK 坑
4.1 现象:运行 java -version 报 cannot execute binary file
这是最常见的启动失败。对着 ARM64 服务器运行刚解压出的java -version,系统直接提示cannot execute binary file: Exec format error。
原因有两类。第一类是压缩包本身是 x86_64 版本,被误以为是 ARM64。第二类是系统内核架构没有识别清楚,比如在 armv7l 的系统上尝试运行 aarch64 二进制。前者通常发生在下载资源时选错包,后者发生在嵌入式设备上较多。
解决方式就是回归到 2.3 节里的三连命令,先看uname -m和getconf LONG_BIT。确认系统是 aarch64 之后,再看压缩包解压出的bin/java文件类型:
file /usr/local/jdk/jdk-17.0.x/bin/java输出里应该有ELF 64-bit LSB shared object, ARM aarch64字样。如果不是,说明资源包架构不匹配,需要重新下载对应架构版本。
4.2 现象:重启后 java 命令找不到了
刚装完时java -version正常,重启服务器后执行命令提示command not found。
原因大概率是环境变量配置在了当前 shell 的临时变量里,没有写入持久化配置。还有一个常见原因:/etc/profile.d/jdk17.sh文件虽然存在,但执行权限或属主不对,导致系统登录时没有加载。
解决方式是把配置脚本全部重写一遍,并手动确认加载:
chmod 644 /etc/profile.d/jdk17.sh source /etc/profile.d/jdk17.sh java -version如果重启后又失效,用cat /etc/profile.d/jdk17.sh检查内容,重点看有没有不正常的路径拼接或字符转义。这里就体现出当初不在路径里加多余空格的必要性。
4.3 现象:编译正常,运行后提示找不到主类或版本不匹配
这个坑的隐蔽性很高。用javac编译出的 class 文件,放到目标机器上用java运行,提示UnsupportedClassVersionError,但两边的java -version看起来都是 17。
原因实际上指向了编译环境和运行环境的 JDK 小版本差异。JDK 17 的 class 文件默认 major version 是 61,但如果编译时用的是 17.0.10,而运行环境是 17.0.2,这种情况一般不报错。报错往往是因为编译时用了更高的大版本,例如 JDK 21,class 文件版本成了 65,再用 JDK 17 运行自然直接拒绝。
解决方式是在编译时加上--release 17参数,强制编译器的字节码版本按 JDK 17 输出:
javac --release 17 /tmp/Test.java java -cp /tmp Test这个参数比单纯设置-source 1.17更稳妥,它能同时约束 source、target 和 bootclasspath。我现在的习惯是,所有 Maven 或 Gradle 项目里统一把<maven.compiler.release>设为 17,避免团队里有人用高版本 JDK 编译导致线上运行失败。
4.4 现象:容器启动后时区异常或字体相关报错
用官方 OpenJDK 的 ARM64 镜像跑应用,容器起来后日志里出现java.lang.NullPointerException,追下去发现是图片验证码生成功能触发了字体库缺失。还有一些容器时区显示 UTC,导致定时任务到点不执行。
原因很简单,很多最小化基础镜像里没有安装 fontconfig、libfreetype 等库,JDK 本身只是把这些库作为可选依赖,缺失时某些功能会在运行时才崩溃。时区问题则是/etc/localtime在镜像里没有与时区数据同步。
解决方式,在构建容器时显式安装基础依赖。这里给一段我常用的 Dockerfile 片段:
FROM ubuntu:22.04 RUN apt-get update && apt-get install -y \ fontconfig \ libfreetype6 \ tzdata \ ca-certificates \ && rm -rf /var/lib/apt/lists/* ENV TZ=Asia/Shanghai RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone COPY jdk-17.0.x /usr/local/jdk ENV JAVA_HOME=/usr/local/jdk ENV PATH=$JAVA_HOME/bin:$PATH注意,这里的ubuntu:22.04名称只是示意,实际你应按团队基线选择基础镜像。加入fontconfig能解决图形验证码、PDF 导出等场景的字体问题,tzdata解决定时任务时区不一致问题。这两个组件在纯计算型服务里看起来无用,但真踩到的时候,日志往往毫无头绪。
4.5 现象:内存配置生效但程序依旧 OOM
ARM64 机器上给 JVM 分配了足够的内存参数,-Xmx也设置正确,但程序运行一段时间后仍然 OutOfMemoryError。
原因并不一定是堆内存不足,而可能是 ARM 机器上内存压缩、指针压缩默认行为不同,或者容器读取的内存限额没有真正传给 JVM。在容器场景里,JVM 默认根据容器限制计算堆大小,前提是 JVM 能读取到 cgroup 信息。旧版本 JDK 在有些容器环境下会忽略 cgroup 限制,导致堆大小按宿主机物理内存计算。
解决方式是显式指定内存参数,不要依赖 JVM 的默认探测:
java -Xms512m -Xmx1024m -XX:MaxRAMPercentage=50.0 -XX:MaxMetaspaceSize=256m -jar app.jar-XX:MaxRAMPercentage=50.0表示容器可用内存的一半用于堆。-Xmx是硬上限,两者可以同时存在。如果部署环境资源紧张,更推荐直接固定-Xmx,这样最不会出意外。
这套坑的共性是:问题表面在 JDK 或程序,根子却在基础设施没配对。我在第一轮部署时也总把错误归到 JDK 包上,后来才意识到,多数情况下是环境细节遗漏。JDK17-Linux-Arm64 这类资源本身不带魔法,它只是把正确版本的二进制给你,剩下的是环境匹配和参数管理的工作。
5. 进阶用法:把 JDK17 打进基础镜像并自检
很多团队用完 JDK17-Linux-Arm64 就停在单机部署层面,其实这套资源在容器镜像构建时更有价值。内网环境不能访问外网镜像仓库时,把官方离线 JDK 打进内部基础镜像,再推送到私有仓库,后续所有 Java 服务都能从这个基础镜像出发,既省流量又统一版本。
我构建基础镜像的习惯是先做一个干净层,再单独加 JDK。尽量不在镜像里留编译工具链,除非这个镜像就是要当构建镜像用。比如运行镜像里可以只放java和必要的动态库,不装javac。这样镜像体积更小,攻击面也更小。
下面是一个自检脚本,每次构建完镜像后先在本地跑一遍,确认架构和 JVM 行为正常,再推送到仓库:
#!/bin/bash # 自检 JDK 镜像的架构、版本和运行能力 image_name="registry.local/java17-arm64:base" docker run --rm --platform linux/arm64 "$image_name" java -version docker run --rm --platform linux/arm64 "$image_name" javac --release 17 -d /tmp /dev/stdin <<'EOF' public class Check { public static void main(String[] a) { System.out.println("ok"); } } EOF第一条命令验证 JVM 能启动且架构是 aarch64,第二条验证javac能在线编译。若基础镜像里确实不装javac,那就执行一条运行测试:
docker run --rm --platform linux/arm64 "$image_name" sh -c 'echo "public class Check{public static void main(String[] a){System.out.println("ok");}}" > /tmp/Check.java && javac /tmp/Check.java && java -cp /tmp Check'这条命令等价于编译后再执行,输出ok才算通过。把这段逻辑固化成一个check_base_image.sh放进 CI,后续任何人改了基础镜像,都会强制走一遍检查流程。
我还会在镜像里额外输出 JVM 的关键参数,方便排障时快速确认运行时环境:
docker run --rm --platform linux/arm64 "$image_name" java -XX:+PrintFlagsFinal -version | grep -E "UseCompressedOops|UseCompressedClassPointers"这段命令能确认 JVM 是否启用了指针压缩。ARM64 平台上默认策略和 x86 不完全一样,看到输出结果是 true 不需要惊讶,关键是不能出现 undefined 或异常覆盖。
从那以后,我每次交付 JDK17 基础镜像,都强制自己走一遍这个流程:先uname -m确认架构,再java -version确认版本,最后用临时 class 文件做一次编译运行自检。这套习惯也许看起来繁琐,但确实帮我挡住了至少三次“镜像推上去才发现装错架构”的翻车事故。希望帮到你。
本文还有配套的精品资源,点击获取