简介:面向Linux Arm64平台的JDK 17完整发行包,适合在ARM服务器、云原生节点及嵌入式设备上编写、编译与运行Java应用的开发者。该版本包含密封类、记录类、instanceof模式匹配、增强型Switch表达式等新特性,并针对低暂停垃圾收集器做了优化,可在ARM生态中直接使用。压缩包共384个文件,包含71个license、71个jmod模块描述文件、37个so动态链接库,以及java、javac、jlink、jpackage等核心命令行工具,整体约171.64MB,结构完整可直接解压配置。已有1388人学习下载。解压后通过JAVA_HOME与PATH环境变量配置即可使用,适合需要为Arm64设备构建Java服务或自行定制JDK模块的中高级开发者,也便于在此基础上开展JVM调优与模块化应用打包。
1. 为什么单独折腾 JDK17-Linux-Arm64:不只是换一个安装包
在 ARM64 机器上跑 Java 应用,第一脚就踩在 JDK 上。x86 的安装包塞进 aarch64 系统,轻则报 cannot execute binary,重则跑起来后 JIT 直接罢工。JDK17-Linux-Arm64 这套资源解决的就是这个错配问题:把编译好的 ARM64 版 JDK 17 和配套的配置脚本整理到一起,解压就能用。适合三类人:买了 ARM 云实例或开发板准备部署微服务的开发者、做国产化迁移踩到架构坑的技术人,以及刚拿到 arm64 服务器想少走弯路的新手。我接下来按实际部署顺序,把安装、调参、避坑和服务化一条条说清楚。
2. 安装与目录规范:用 tar.gz 包做到随拿随用
2.1 三种发行包怎么选:tar.gz、rpm 与纯二进制镜像
JDK 17 在 Linux ARM64 上的分发方式主要三种:tar.gz 压缩包、rpm 包和官方提供的纯二进制镜像。rpm 包适合 rpm 系发行版,但依赖系统的包管理策略,装完被塞进 /usr/lib/jvm,卸载时容易留下半截环境变量。纯二进制镜像偏实验性质,适合临时跑一次命令,不适合做常驻服务的基础运行环境。
我一般选 tar.gz。理由有三个:解压路径可控,方便把 JDK 和业务软件分开管理;多版本 JDK 可并存,互不覆盖;在 ARM64 平台上,官方 aarch64 构建大多以 tar.gz 形式分发,下载链路最短。JDK17-Linux-Arm64 资源包采用的也是这个形态:解压后得到一个完整的 jdk-17 目录,内含 bin 下的 java、javac、jcmd 等工具。有一点要提醒:拿到包后先看目录名,通常包含 aarch64 或 arm64 字样,如果没有,第一件事先运行 file 命令确认 ELF 平台类型。
file jdk-17/bin/java # 期望输出:ELF 64-bit LSB executable, ARM aarch64 ...如果输出里出现 x86-64,这个包就不是给当前机器用的,继续装只会浪费时间。我保留这个判断习惯到现在:任何跨架构的发行包,先 file 再决定要不要解压。
选型之外,依赖检查也同样重要。解压后的 java 是动态链接的,依赖系统中已有的 libc、libz 等运行库。在完整发行版上一般没问题,但在裁剪版容器镜像里,libfreetype.so 和 fontconfig 经常缺失,这会影响后面跑图表导出或图形验证。判断方法一条命令:
ldd /opt/jvm/jdk/bin/java | grep "not found"只要输出里有 not found,先补基础运行库。这一步做在配置环境变量之前,能省掉后面一堆奇怪的 UnsatisfiedLinkError。
2.2 解压到一个不后悔的路径:目录规划与软链
ARM64 服务器上的 Java 目录规划,我习惯按 /opt/jvm 做统一编排,不放 /usr/local,也不放用户 home。原因很简单:/opt 适合第三方软件,一个应用一个子目录,备份、迁移、给其他用户授权都清晰。具体结构如下:
mkdir -p /opt/jvm tar -xzf jdk-17-linux-aarch64.tar.gz -C /opt/jvm ln -s /opt/jvm/jdk-17 /opt/jvm/jdk # 符号链接,方便版本升级 ls -l /opt/jvm/jdk/bin/java这里的关键是那个符号链接。业务脚本里全部引用 /opt/jvm/jdk 而不是 /opt/jvm/jdk-17,后续换小版本时只需要解压新包、重指链接,所有服务和 PATH 不用逐个改。很多开发者在 ARM 机器上翻车,就是直接把 PATH 写死成 /opt/jvm/jdk-17/bin,结果升级一次拆一次东墙。
解压后建议顺手做一个有效性探测,而不是急着 export。运行下面的命令,把输出和系统架构做一次对照:
/opt/jvm/jdk/bin/java -version /opt/jvm/jdk/bin/javac -versionjavac 也输出版本号才算完整,只验证 java 的话,后面编译阶段才暴露问题。实际碰过的场景里,某次在模拟项目X中解压了不完整的包,java -version 正常,可是 javac 报 ClassNotFoundException,排查了半天,最后发现是包体积不对。所以这两个命令建议每次都跑。
2.3 环境变量与首轮验证:java -version 不等于能用
环境变量配置推荐写到 /etc/profile.d/java.sh,这样所有用户、所有登录方式都能读到。不要写进单个用户的 ~/.bashrc,因为 systemd 服务默认不加载 .bashrc,服务启动时找不到 JAVA_HOME,这个问题常年出现在生产环境。
cat > /etc/profile.d/java.sh <<'EOF' export JAVA_HOME=/opt/jvm/jdk export PATH=$JAVA_HOME/bin:$PATH EOF source /etc/profile.d/java.sh java -versionsource 后如果能正常打印版本号,这一步才算走到位。但真正要确认的是环境变量是否进入了非交互 shell。用下面的命令验证:
bash -c 'echo $JAVA_HOME'如果输出空行,说明配置没被读取,常见原因是文件权限不对,或者系统没有自动 source /etc/profile。把文件权限改成可读,重新登录终端验证。这里顺便讲个细节:echo $JAVA_HOME 输出为空,不代表 JDK 没装,可能是 shell 缓存了旧 PATH,执行 hash -r 清理一下,大多数“明明配了却找不到”的假象就能消失。
第一轮验证建议多跑一条命令:
/opt/jvm/jdk/bin/jshell <<< "System.out.println(System.getProperty('os.arch'));"输出 aarch64 代表当前 JVM 跑在 ARM64 架构上。这条验证在我的习惯里是必做的,因为它能发现 java -version 不会暴露的错误:比如在 x86 的 JVM 上硬塞 ARM64 的 libawt.so,版本命令正常,但一调用图形库就崩。在 JDK17-Linux-Arm64 的安装场景下,这步过了再谈参数调优。
环境变量这里还有一条我习惯固定加上的配置,写在 /etc/profile.d/java.sh 里:
export JAVA_TOOL_OPTIONS="-Dfile.encoding=UTF-8"这一条解决 ARM 服务器上常见的中文文件名和日志乱码问题。JAVA_TOOL_OPTIONS 对所有通过此 shell 启动的 Java 进程自动生效,适合全局默认,但如果某个应用要覆盖,记得用 JDK_JAVA_OPTIONS 或启动脚本里的显式传参,后者优先级更高。
3. 在 ARM64 上把 JVM 参数调对:从默认值到容器感知
3.1 ARM64 上默认堆与容器上限的错位
JDK 17 默认开启 UseContainerSupport,JVM 能识别容器 cgroup 的内存限制。但默认堆大小按物理内存的 1/4 计算,这是针对开发机的保守设置。在 2C4G 的 ARM64 实例上,如果部署一个微服务,默认堆 1G,堆外再加 metaspace、线程栈、JIT 缓存,内存很容易逼近上限。模拟项目X里就出现过一次:服务起来才 15 分钟,OOM Killer 把进程杀了,查监控发现堆根本没满,是 JVM 整体占用加容器内其他进程一起把内存顶爆。
ARM64 上还有个容易被忽略的点:普通对象引用压缩(UseCompressedOops)开启与否取决于堆内存边界。小于 32G 堆通常可压缩,超过则关闭。这影响内存占用和指针运算速度,调优时如果改了堆大小,建议用 jcmd 确认压缩指针状态。
容器场景下,cgroup v2 已经普及。JDK 17 对 cgroup v2 的支持已经成熟,但如果你还在用老的 cgroup v1 内核,又手动指定了 -Xmx,JVM 可能向上读到宿主机的物理内存,而不是容器限制。这时候 MaxRAMPercentage 的计算基准会出错,表现为容器内报 OOM 但宿主机内存还很宽裕。所以参数设置前,先确认系统挂在哪个 cgroup 版本:
stat -fc %T /sys/fs/cgroup/ # cgroup2fs 代表 v2,tmpfs 代表 v1这个信息直接决定后面用 -XX:MaxRAMPercentage 还是固定 -Xmx。
3.2 一组可复制的 JVM 参数模板与含义拆解
给 ARM64 上的 Java 应用准备的参数模板,我默认这样写:
export JAVA_OPTS=" -XX:MaxRAMPercentage=80.0 -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:+ExitOnOutOfMemoryError -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/log/java -Djava.awt.headless=true -Dfile.encoding=UTF-8 "逐项解释。MaxRAMPercentage=80.0 让 JVM 自动按容器或宿主机内存计算堆上限,这比写死 -Xmx 更适应实例规格的变化。如果同一个服务包要部署到 2G、4G、8G 三档规格的 ARM 机器上,用百分比比手动改 -Xmx 省力得多。G1 是 JDK 17 的默认收集器,MaxGCPauseMillis=200 只是目标值,不保证一定达到。
ExitOnOutOfMemoryError 可以让 JVM 在内存溢出时直接退出,配合 systemd 自动重启,比僵在那里继续接收请求干净。HeapDumpOnOutOfMemoryError 必须配 HeapDumpPath,否则堆转储文件写到当前工作目录,服务在根目录下运行时报 permission denied 的情况很常见。headless=true 和 file.encoding 在 ARM 服务器上是稳定性的最后一道防线,不少图表生成场景里,没有 headless 会直接报 java.awt.HeadlessException。
如果服务里确实需要固定堆大小,可以改回这种写法:
export JAVA_OPTS="-Xms512m -Xmx2g -XX:+UseG1GC"但要注意:-Xmx 与 MaxRAMPercentage 同时出现时,百分比会被忽略。不要配了两者还奇怪为什么另一个没生效。我一般二选一,容器化部署用百分比,物理机裸部署用固定堆。
3.3 用 jcmd 和 jinfo 验证参数是否真的生效
配好参数后,很多开发者的下一步是直接启动,我建议先用 jcmd 看一遍运行时参数。启动 Java 进程时把 JAVA_OPTS 原样传入,然后:
jcmd $PID VM.flags | grep -E 'MaxHeapSize|UseG1GC|MaxRAMPercentage' jcmd $PID VM.system_properties | grep os.archVM.flags 输出的是 JVM 最终采纳的参数值,这个输出才能真正告诉你有没有配错。比如你写了 MaxRAMPercentage 但忘了去掉 -Xmx,VM.flags 里没有 MaxRAMPercentage,只有 MaxHeapSize,说明参数被忽略了。这种情况我至少踩过两次,玄学调参的根源就是没验证最终值。
如果不想找进程号,也可以用 jps 先列出当前用户下的 JVM 实例:
jps -ljps 输出 pid 和主类名,拿 pid 再喂给 jcmd。这套组合熟练后,调参不再是“配完赌一把”,每一轮改动都能立刻看到生效结果。对 ARM64 平台尤其重要,因为官方包里附带完整的 JDK 工具链,活用 jcmd 能省掉反复重启应用的折腾。
还想确认某个单独参数时,jinfo 比 jcmd 更直接:
jinfo -flag MaxRAMPercentage $PID jinfo -flag UseCompressedOops $PID前一条可能输出 -XX:MaxRAMPercentage=80,后一条输出 -XX:+UseCompressedOops。如果你想确认堆的最终上限,直接读 MaxHeapSize 的值,这个值才是 JVM 真正用来分配堆的边界,别的都是参考。
4. 避坑与排查:ARM64 上常见的五个翻车点
4.1 架构误判:aarch64 和 arm64 的视觉陷阱
现象:解压完运行 java -version,报 cannot execute binary file: Exec format error,或者直接段错误退出。
原因:系统 uname -m 输出 aarch64,但下载的包是 x86_64 版,或者反过来在 x86 机器上装了 ARM 包。资源命名也容易混淆:有的写 arm64,有的写 aarch64,目标其实是同一个 ELF 平台,但下载时看错单词就会翻车。
解决:执行 uname -m,输出 aarch64 就找包含 aarch64 字样的包。Debian 系文档常说 arm64,GNU 工具链叫 aarch64,二者都指 64 位 ARM 架构。JDK17-Linux-Arm64 这个资源包对应的正是 aarch64 平台,装上后可以用回第 2 章的那条 jshell 验证命令再次确认。
4.2 GLIBC 版本不匹配与裁剪镜像缺字体
现象:运行 java 时报 ./lib/libjli.so: /lib/aarch64-linux-gnu/libc.so.6: version GLIBC_2.32 not found。
原因:较新版本的 JDK 构建一般链接较新的 glibc,旧发行版如基于 CentOS 7 或 Ubuntu 18.04 的系统,glibc 版本不够。ARM64 的国产系统里更常见,因为基础镜像长期停留在旧版本。
解决:优先换用为该发行版编译的 JDK 版本,或者直接升级系统到 glibc 2.31+。临时方案是安装兼容库,但生产环境不建议这种叠补丁的做法,兼容库本身也是二进制,出问题更难看。
现象:AWT/Swing 程序启动时报 java.lang.UnsatisfiedLinkError 找不到 libfreetype.so。
原因:精简版系统镜像没有安装字体渲染相关库。
解决:安装 fontconfig 和 freetype。Debian 系执行 apt-get install fontconfig libfreetype6,rpm 系执行 yum install fontconfig freetype。加 -Djava.awt.headless=true 能避开大多数窗口环境依赖,但图表导出类功能仍需要字体文件,系统里至少保留一套基础字体。
4.3 环境变量只在交互 shell 生效;SELinux 拦截新目录
现象:手动终端里 java -version 正常,systemd 服务启动却报 java 命令找不到。
原因:服务环境不读 /etc/profile,也不读 ~/.bashrc。很多开发者只配置了交互 shell 的环境变量,服务进程起来自然找不到。
解决:在 systemd 单元文件的 [Service] 段显式设置 Environment=JAVA_HOME=/opt/jvm/jdk 和 Environment=PATH=/opt/jvm/jdk/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin。我调试过多次这个经典问题,先查服务文件,再查脚本里有没有依赖 login shell 的 export。
现象:JDK 安装到 /opt 后,运行时报 error while loading shared libraries,但 ldd 查不出缺失库。
原因:SELinux 上下文没同步到新解压目录,访问被强制访问控制拦截。
解决:root 下执行 restorecon -R /opt/jvm,或者把 JDK 放到默认的 /usr/lib/jvm 路径再用 alternatives 登记。这条在 ARM64 服务器上出现概率不低,尤其常见于固件默认开启强管控的场景。
5. 把 JDK 17 跑成系统服务:systemd 单元与日志轮转
5.1 为什么生产上不用 nohup 直接甩
开发机上 nohup java -jar app.jar > app.log 2>&1 & 很方便,但生产环境这么做等于放弃三样东西:开机自启动、崩溃拉起、统一日志。nohup 进程挂在当前终端会话之下,终端退出后虽然能用,但系统重启后没人帮你拉起来。更麻烦的是,子进程的退出状态没人收集,Java 进程因为 OOM 被 Kill 掉,你只能在日志里看到进程消失了,没有恢复机制。
systemd 在绝大多数 Linux ARM64 发行版上都是默认初始化系统,把 Java 应用做成单元服务,由 systemd 来监管生命周期,内存溢出退出后自动拉起,启动失败也会记录到 journald。这一节我把完整配置贴出来,照着改就能用。
5.2 通用 systemd 单元文件写法与启动时序
假设应用是一个 Spring Boot 风格的 jar,放在 /opt/app/app.jar,JAVA_HOME 指向 /opt/jvm/jdk。推荐的单元文件如下:
[Unit] Description=Java App Service After=network-online.target Wants=network-online.target [Service] Type=simple User=appuser Group=appuser Environment=JAVA_HOME=/opt/jvm/jdk Environment=PATH=/opt/jvm/jdk/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin Environment=JAVA_OPTS=-XX:MaxRAMPercentage=80.0 -XX:+UseG1GC -XX:+ExitOnOutOfMemoryError ExecStart=/opt/jvm/jdk/bin/java $JAVA_OPTS -jar /opt/app/app.jar Restart=on-failure RestartSec=10 TimeoutStartSec=300 [Install] WantedBy=multi-user.target参数说明。Environment 写 PATH 是为了让 java 命令能被子进程找到,JAVA_OPTS 里的内容会被 ExecStart 展开。注意 ExecStart 必须写绝对路径 /opt/jvm/jdk/bin/java,不要直接写 java,否则 systemd 在某些版本上会忽略 PATH 环境变量。Restart=on-failure 表示非正常退出时自动拉起,RestartSec=10 间隔 10 秒,TimeoutStartSec 给启动比较慢的应用留足时间。
配置完成后执行:
systemctl daemon-reload systemctl enable app.service systemctl start app.service systemctl status app.servicestatus 输出里能看到 Active: active (running) 才是成功。如果启动失败,第一步看 journald:
journalctl -u app.service -n 50 --no-pager这里多数时候能直接看到报错。最常见的是 JAVA_OPTS 里带换行符导致 ExecStart 解析失败,检查单元文件里是否有多余回车。另一种是 User=appuser 对 /opt/jvm/jdk 目录没有读权限,用 chmod -R 755 /opt/jvm/jdk 解决。
5.3 日志轮转与崩溃自动拉起
systemd 会把服务的标准输出和标准错误收集到 journald,但日志量大时 journald 自身也会膨胀。建议应用内日志按天滚动,同时由系统日志轮转管理。如果你用 logrotate,配置一个独立文件:
cat > /etc/logrotate.d/java-app <<'EOF' /opt/app/logs/*.log { daily rotate 7 compress delaycompress missingok copytruncate } EOFcopytruncate 适用于应用内主动打开日志文件的场景,先复制当前文件再截断,避免向应用发送信号。这个参数在 Java 服务里特别重要,很多框架自己管理日志文件描述符,标准 logrotate 的 rename 方案会导致应用持续往旧文件里写,截断后旧文件不释放磁盘。
崩溃自动拉起的验证,可以直接杀进程看 systemd 反应:
kill -9 $(pgrep -f app.jar) sleep 12 systemctl status app.service如果状态变成 active (running),说明 Restart=on-failure 生效。注意 RestartSec=10 意味着杀死后要等至少 10 秒再查。这套机制在 ARM64 服务器上非常依赖 ExecStart 里的绝对路径和环境变量,前面第 4 章说的环境变量不生效问题,往往在这里才暴露。
还有一种隐蔽场景:Java 进程捕捉了 SIGTERM,执行完清理逻辑后自己 exit 0,systemd 认为这是正常退出,不会拉起。如果希望这种行为也能触发重启,用 Restart=always。但 always 会把手动 stop 后的服务也拉起来,使用前想清楚你是否每次都需要手动 systemctl stop 后再 disable。
6. 收尾技巧:用 update-alternatives 管理 JDK 版本切换与回滚
多版本 JDK 并存时,我习惯用 update-alternatives 统一管。JDK17-Linux-Arm64 这类资源包解压到 /opt/jvm 后,不急着改 PATH,先登记到 alternatives:
update-alternatives --install /usr/bin/java java /opt/jvm/jdk-17/bin/java 1700 update-alternatives --install /usr/bin/javac javac /opt/jvm/jdk-17/bin/javac 1700 update-alternatives --config java--install 三个参数依次是链接路径、名称和实际可执行文件,最后的 1700 是优先级,数字越大越优先。登记完,所有依赖 /usr/bin/java 的脚本都能自动命中新的 JDK 版本,省去了逐个检查 PATH 的麻烦。
切换版本时执行 update-alternatives --config java,会列出已登记的所有 JDK,输入序号即可切换。回滚到上一个版本也在这里选,不需要改任何环境变量。但那之后我给自己立了一条规矩:每次切换完必须执行一遍确认命令,不然下次排障容易记错当前指向:
update-alternatives --list java update-alternatives --list javac readlink -f /usr/bin/javareadlink -f 会输出最终解析到的真实路径,看到 /opt/jvm/jdk-17/bin/java 才算数。如果输出变成 jdk-21 之类的路径,说明当时切换时选了别的版本。从那以后,我每次装完 JDK 都强制走一遍 update-alternatives 确认指向,并把回滚命令直接写进部署笔记开头,防止自己在忙乱时凭记忆切换版本。希望帮到你。
本文还有配套的精品资源,点击获取