简介:在ARM架构的Linux系统(如树莓派、飞腾、鲲鹏等aarch64设备)上运行或开发Java应用,需要配套的JDK。这份JDK 11.0.20正是面向此类场景的Linux aarch64版本压缩包,包含编译、运行、调试Java程序的完整工具链,适合需要在ARM设备上构建并部署项目的开发者。压缩包共392个文件,总大小约158.16MB,除核心的java、javac、jmod模块文件外,还包含类库、动态链接库(so)、安全策略配置及jcmd、jstat、jarsigner等命令行工具,覆盖从日常开发到性能调优的常见需求。该版本属于长期支持(LTS)版本,可享受持续的安全更新与维护,适合生产环境使用。资源目前已有697人学习下载,文件目录结构清晰,解压并配置环境变量后即可直接使用。
1. ARM机器上的Java 11不是玄学:先搞清楚aarch64包与x86包的区别
在ARM服务器或开发板上部署Java应用,最常见的翻车不是版本选错,而是随手拿了个Linux x86_64的JDK硬解压。执行java -version弹出cannot execute binary file时,多半是架构不匹配,不是包坏了。jdk-11.0.20-linux-aarch64-bin.tar就是针对ARM 64位Linux的JDK 11二进制包,它自带aarch64架构的JVM,能直接跑在各类ARM开发板和ARM服务器上,解决Java服务起不来、工具脚本不能用这类具体问题。这篇文章从选型、安装、排错到用jlink裁减运行时一次讲完,新手能照着敲,熟手也能看到边界参数和实际踩过的坑。
2. aarch64与x86_64的Java运行区别:为什么ARM机器必须用专用JDK包
2.1 架构不匹配的直接后果:cannot execute binary file的底层原因
Java应用编译出来的.class字节码确实是跨平台的,这一点没有争议。但真正把字节码跑起来的JVM本身,是一个用C/C++编译出来的原生可执行文件。它编译成哪类CPU的指令集,就只能在哪类CPU上执行。x86_64的JVM执行的是x86指令集,ARM64的CPU译码器不认识这些指令;操作系统加载这个ELF可执行文件时,会根据ELF头里的e_machine字段判断它能否在当前内核上运行,x86_64对应的值是62,aarch64对应的是183,内核一比对发现不匹配,就拒绝加载,shell于是抛出cannot execute binary file。
这也是为什么在ARM机器上装JDK,必须先看清包名里的架构标识。jdk-11.0.20-linux-aarch64-bin.tar里的aarch64表示ARM 64位指令集,它是和JVM二进制强绑定的。要注意的是,一部分ARM设备还运行着32位用户态,比如armv7l架构对应的是armhf包;如果设备是32位用户态,装aarch64包一样跑不起来。在动任何安装操作之前,先用下面两条命令确认硬件状态:
uname -m getconf LONG_BIT第一条输出aarch64说明内核是64位ARM架构;第二条输出64说明用户态也是64位。两个条件都满足,再解压JDK。uname -m在大多数ARM服务器上输出aarch64,这是最干净的标识;但有些开发板在32位用户态下输出armv8l,这时你需要的不是aarch64包,而是armhf包。这个坑我踩过不止一次,当时拿着aarch64的JDK往板子上传,解压后怎么执行都报错,最后用file命令检查二进制才反应过来。
2.2 读懂JDK版本命名:11.0.20、linux-aarch64-bin.tar的每个字段
先完整拆一下文件名。jdk-11.0.20-linux-aarch64-bin.tar从左到右是四段信息。第一段11.0.20,主版本11,这是Java SE的LTS长期支持版本,补丁更新按季度发布,包含安全修复和少量bug修复,11.0.20属于11生命周期内较新的季度更新,适合生产环境。第二段linux,目标操作系统是Linux。第三段aarch64,目标CPU架构是ARM 64位,对应ARMv8-A指令集,这一字段决定了JVM能不能被加载。第四段bin.tar,bin表示编译好的二进制产物,tar表示用tar归档格式打包;GNU tar的-xf参数可以自动识别gzip、xz等压缩格式,所以文件名里即使没有.gz或.xz后缀,解压命令也不受影响。
实际选型时还要区分开发包和运行包。JDK是完整开发工具集,包含javac、jlink、jdeps、jshell等工具;JRE是纯运行环境。Java 11之后官方不再单独发布JRE安装包,但这不代表你只能背着一个庞大的JDK到处部署。第5章会演示怎么用jlink从JDK里裁出一个只含必要模块的精简运行时,这在ARM嵌入式设备上尤其实用。
选JDK版本本身也是一个需要确认的决策。如果你的应用依赖较老的三方库,或者用了部分只在Java 8下正常的反射写法,直接从8跳到11可能会遇到模块化或IllegalAccessError这类问题。反过来,如果你是新项目,建议至少在11和17之间选一个LTS版本。11.0.20这个包的价值在于:它在Java 11的兼容性基础上,集成了截至该版本的季度安全修复,同时aarch64的JVM实现相对成熟,是很多ARM服务器和容器镜像里常用的基础运行环境。
2.3 下载源的信任边界:哈希校验与文件真实性
从网上下载JDK包,习惯上要先确认文件完整性和来源可靠性。最常见也最有效的验证手段是计算SHA-256哈希,和发布方给的值比对。这一步能同时发现两个问题:下载过程中文件损坏、以及文件被替换或篡改。命令非常简单:
sha256sum jdk-11.0.20-linux-aarch64-bin.tar输出是一个64位的十六进制字符串,只要它和发布方提供的官方哈希一致,基本可以确定文件完整且未被改动。严格一点的场景还会验GPG签名,用发布方的公钥验证.sig文件,但这需要额外维护密钥信任链,对多数内部部署来说成本偏高。我自己的习惯是:生产环境要求至少做一次SHA-256比对,测试环境就算跳过,也要保证压缩包是从可信来源直接下载的。
这里有一个容易踩的细节:同一个版本号、同一个架构,不同构建商的产物哈希值各不相同。你下载的是哪一家的构建,就要以那一家的哈希值为准。网上随手搜来的哈希只能作为参考,不能作为验证依据,否则可能拿一个错误或伪造的哈希去比对,反而放过了真实问题。
2.4 选型决策:通用OpenJDK构建、系统包管理器、厂商定制版怎么选
在ARM Linux上装Java 11,除了直接下载二进制tar包,还有另外两条常见路径,各自有明确的使用边界。
第一条是系统包管理器安装。部分Linux发行版通过apt或yum提供openjdk-11相关包,好处是自动解决依赖关系、更新也方便;坏处是版本滞后,发行版仓库里的JDK版本往往落后于官方季度更新,而且安装位置、目录结构被系统策略固化,不好自己控制。第二条是芯片厂商或整机厂商提供的定制JVM,这类版本针对特定ARM平台做过指令集优化,适合跑重型计算或对性能有极端要求的场景,但通常需要单独申请、授权链条也长,普通业务用不上。
放在一起对比,适用场景就很清楚了:
| 选型 | 适合场景 | 注意点 |
|---|---|---|
| 通用OpenJDK二进制包 | 生产环境、版本敏感的Java服务 | 自包含、版本精确,依赖自己管理 |
| 系统包管理器安装 | 测试环境、快速搭建 | 版本滞后、目录受控 |
| 厂商定制版 | 高性能计算、业务强绑定 | 授权与维护成本高 |
实际部署时我的选型规则很固定:生产环境一律用自包含的二进制包,安装到统一目录,不走系统包管理器;同一台机器有多版本JDK共存的需求,再用update-alternatives管理。这样可以保证版本可控、回滚方便,也避免系统包更新时悄悄改掉你的默认JDK。
3. 部署JDK 11:解压、环境变量与多版本共存的完整操作
3.1 安装前检查与解压:把JDK放进/opt并核对目录布局
进入安装环节,先把机器状态确认完。除了前面说的uname -m和getconf LONG_BIT,还要看磁盘空间和解压目录权限。JDK 11解压后占用空间不小,运行期还会产生缓存和日志,建议至少留出1GB以上的可用空间。我一般把JDK统一安装到/opt/java下,不使用发行版默认的/usr/lib/jvm目录,原因是避免和系统自带的JVM互相干扰,也方便后续做多版本管理。
sudo mkdir -p /opt/java sudo tar -xf jdk-11.0.20-linux-aarch64-bin.tar -C /opt/java ls -l /opt/javatar -xf拆开看:-x是解压,-f指定归档文件;-C /opt/java把解压目标目录指到/opt/java。不少打包文件内部会自带一层顶层目录,解压后生成/opt/java/jdk-11.0.20-linux-aarch64这样的完整路径。所以正确用法是先把目标目录建好、再在目标目录上一级执行tar -xf,避免把一堆文件和目录直接铺散到当前路径。解压完成后,ls -l应能看到bin、lib、jmods、conf这几个标准子目录。
这里补一个ARM设备上的使用习惯:解压大包时不要加-v参数。tar -xvf会把每个解压出的文件名刷到终端上,在ARM开发板这类I/O性能一般的设备上,几万个文件名的输出本身就足以让安装过程慢好几倍。需要确认归档内容时,用下面的方式更快:
sudo tar -tf jdk-11.0.20-linux-aarch64-bin.tar | head-t是列出归档内容的模式,不真正解压;配合head只看前几行,确认目录结构是否正常就够了。在磁盘空间紧张或I/O受限的环境里,这个习惯能省不少时间。
3.2 设置JAVA_HOME和PATH:当前会话与永久配置的差别
解压完成只是第一步,当前shell还不会自动找到java命令,需要配置环境变量。最精简的两行:
export JAVA_HOME=/opt/java/jdk-11.0.20-linux-aarch64 export PATH=$JAVA_HOME/bin:$PATH第一行定义JAVA_HOME,指向JDK的安装根目录,是很多脚本和应用读取Java路径的依据;第二行把$JAVA_HOME/bin插入到PATH的最前面,这样执行java时优先命中我们自己装的版本。直接敲这两行只对当前终端会话有效,一关终端就失效。要让配置永久生效,需要写进shell的启动脚本。多数Linux发行版默认shell是bash,配置写入~/.bashrc:
echo 'export JAVA_HOME=/opt/java/jdk-11.0.20-linux-aarch64' >> ~/.bashrc echo 'export PATH=$JAVA_HOME/bin:$PATH' >> ~/.bashrc source ~/.bashrc如果默认shell是zsh,写入~/.zshrc;如果这台机器要面向多个账号统一配置,则写到/etc/profile.d/java.sh,这个目录下的脚本会在用户登录时自动加载,比手动维护每个账号更省事:
sudo tee /etc/profile.d/java.sh <<'EOF' export JAVA_HOME=/opt/java/jdk-11.0.20-linux-aarch64 export PATH=$JAVA_HOME/bin:$PATH EOF注意两个细节。第一,JAVA_HOME里不要带尾斜杠,很多脚本拼接$JAVA_HOME/bin时对多一个斜杠的处理不一致,少写一个能少踩一类路径怪的坑。第二,/etc/profile.d/的配置对非交互式登录不一定生效,如果你通过某些远程执行工具跑命令,可能需要额外source或直接在服务脚本里写环境变量。
3.3 验证安装:java、javac、jshell三样检查清单
环境变量配置好并source当前shell之后,依次跑三条命令做基础验证:
java -version javac -version jshell --version正常输出里应该能看到openjdk version "11.0.20"和OpenJDK 64-Bit Server VM之类的描述,版本号明确写有11.0.20。重点确认三件事:版本号是不是11.0.20、JVM是不是64位、当前平台是不是aarch64。这里有一个容易误会的细节:java -version的输出是打到标准错误流stderr的,不是标准输出stdout。如果你在脚本里用$(java -version)去捕获输出,经常什么都拿不到,这是Java长期以来的行为,不是你的安装有问题。
再验证一下命令路径是否真的指向了我们安装的JDK,避免被发行版自带的旧Java抢先:
type -a java readlink -f $(which java)type -a java列出所有出现在PATH里的java可执行项;readlink -f解析软链接的最终路径,结果应以/opt/java/jdk-11.0.20-linux-aarch64/bin/java结尾。这一步很关键,因为有不少ARM开发板镜像预装了Java,你的PATH如果设置顺序不对,which java仍然指向旧版本,造成“配置了JAVA_HOME却不生效”的错觉。
3.4 多版本共存:update-alternatives的优先级与手动软链
生产机器上经常要同时保留JDK 8和JDK 11,用于跑不同的遗留应用。在多数使用dpkg机制的Linux发行版上,update-alternatives是管理命令版本冲突的内置工具,它通过维护一组符号链接来切换当前生效的JDK。注册JDK 11到系统中:
sudo update-alternatives --install /usr/bin/java java /opt/java/jdk-11.0.20-linux-aarch64/bin/java 1100 sudo update-alternatives --install /usr/bin/javac javac /opt/java/jdk-11.0.20-linux-aarch64/bin/javac 1100 sudo update-alternatives --config java--install后面跟着四段参数:第一个是符号链接要放的位置/opt/java/jdk-11.0.20-linux-aarch64/bin/java是实际程序路径1100是优先级数字,数值越大在自动模式下越优先被选中。java和javac是两个独立的命令,必须分别注册,这一步很多人会漏掉javac,结果java -version正常但javac编译时仍指向旧版。
执行--config java时会进入一个交互式选择界面,按对应的数字键切换当前生效的JDK。如果不想要交互,也可以直接用--set指定:
sudo update-alternatives --set java /opt/java/jdk-11.0.20-linux-aarch64/bin/java与手动创建软链接相比,update-alternatives的优势在于它把链接的变更记录写入系统配置文件,装新包或卸载包时系统能识别并正确处理,不会发生“自己改的软链被包管理器悄悄覆盖”的尴尬。运行RPM类发行版的环境则通常用alternatives命令或者直接在/usr/bin下面做软链,机制类似但命令名不同。如果没有多版本需求,只装一个JDK,那么前面配置好JAVA_HOME和PATH就够了,不需要引入alternatives。
4. aarch64 JDK 11运行排查:五个常见坑与修复方法
4.1 启动报错cannot execute binary file:本质是内核拒绝加载
现象:在ARM机器上解压完JDK后执行java -version,shell直接报cannot execute binary file: Exec format error。
原因:这是典型的架构不匹配。JVM二进制是Linux可执行文件,但它的ELF头里声明的目标架构和当前内核不匹配。另一个常见场景是在ARM机器上解压了一个x86_64的包,解压过程正常、目录结构完好,但执行时内核直接拒绝加载。
解决:先确认机器真实架构,再用file命令检查二进制实际格式:
uname -m file /opt/java/jdk-11.0.20-linux-aarch64/bin/java如果file输出里带ARM aarch64,而java仍不能执行,检查包是否解压完整或文件是否有执行权限;如果输出显示x86-64,说明下载错了包,需要换成aarch64版本、重新校验哈希后再次解压。这个坑看起来初级,实际出现的频率不低,不少情况下是拿以前下载的x86包应急存储、时间一久自己也记不清了。
4.2 提示error while loading shared libraries:LD_LIBRARY_PATH弄乱了
现象:java文件权限和架构都正常,但启动时报error while loading shared libraries: libjli.so: cannot open shared object file,或者提示缺libjvm.so。
原因:JDK的正常运行依赖安装目录下lib和lib/server里的多个本地库。这些库默认通过编译进二进制的rpath自动查找。但如果系统环境变量LD_LIBRARY_PATH里配置了指向其他目录的内容,动态加载器会优先搜索这个环境变量指定的路径,找不到时就报错。还有一种情况是你手动加了LD_LIBRARY_PATH但漏掉了lib/server这个子目录,而lib/server/libjvm.so正好是JVM本体所在。
解决:先看动态库依赖的实际解析情况:
ldd $JAVA_HOME/bin/java正常的输出里大部分so文件都应有对应路径,不会全部是not found。如果libjli.so或libjvm.so指向了不存在的路径,最稳妥的做法是把自定义的LD_LIBRARY_PATH清理掉,让JDK自带的rpath生效。如果确实要为其他软件保留这个环境变量,就把它改为既包含$JAVA_HOME/lib、也包含$JAVA_HOME/lib/server的完整配置,注意路径顺序、不要覆盖JDK自身的查找。排查这个问题时,strace -e openat可以看到动态加载器实际打开了哪些路径,但多数情况下先查环境变量就能定位。
4.3 启动Java进程后直接被Killed:堆内存超了ARM设备限额
现象:java -jar app.jar跑几秒后进程消失,没有任何异常堆栈。查看系统日志,显示Out of memory或者进程被内核OOM killer杀掉。
原因:ARM开发板和部分入门级ARM服务器的内存通常只有1GB到4GB。JVM在没有显式配置时,默认最大堆按物理内存的四分之一计算,在1GB的机器上默认堆就有256MB;再算上Metaspace、线程栈、JIT编译缓存,进程峰值很容易就冲破设备物理内存上限。更麻烦的是,很多ARM设备还启用了cgroup限制,JVM直接探测宿主内存,实际可用配额远小于默认值,启动后立刻被杀。
解决:显式限制堆和Metaspace的大小:
java -Xmx256m -XX:MaxMetaspaceSize=128m -jar app.jar-Xmx指定最大堆内存,-XX:MaxMetaspaceSize限制JVM元数据区域的上限。如果256MB还不够用,需要确认应用本身的内存占用基线再逐档调整。一个简单的经验法则:物理内存1GB时,JVM的总开销不要超过600MB,剩下的要留给操作系统和进程自身。可以用free -m先确认当前可用内存,再决定-Xmx和-XX:MaxMetaspaceSize的具体值。这里切忌盲目调大,ARM板子上的内存一旦吃满,系统可能连SSH都无响应。
4.4 容器内Java看到的CPU数不对:ActiveProcessorCount与UseContainerSupport
现象:Java应用部署在Docker容器里,限制--cpus=2、内存512MB,但应用里Runtime.getRuntime().availableProcessors()返回宿主机的16核,JVM默认堆也按照宿主物理内存计算,直接造成容器频繁被杀。
原因:JDK 11默认启用了容器支持特性(-XX:+UseContainerSupport),理论上会自动读取cgroup配额。但在内核版本较老、cgroup v1切换cgroup v2的早期阶段,或者使用了特殊的pause容器时,JVM对容器配额探测经常不准,于是回退到读取宿主机的CPU和内存数据。
解决:在容器启动时显式指定CPU数量和内存比例:
java -XX:ActiveProcessorCount=2 -XX:MaxRAMPercentage=50.0 -jar app.jar-XX:ActiveProcessorCount直接覆盖JVM探测到的CPU数量,它影响的不仅是availableProcessors()的返回值,还包括GC线程数、ForkJoinPool并行度等一系列内部参数。-XX:MaxRAMPercentage=50.0让最大堆按容器可用内存的50%计算,比手工指定-Xmx更适应动态配额。如果容器平台对cgroup读取一直正常,可以不额外设置;但如果发现Java在容器里识别的资源明显偏大,这两个参数就是最直接的修正手段。
4.5 涉及AWT/Swing的代码报字体缺失:headless模式的选择
现象:应用里用到java.awt相关代码,比如生成验证码图片或绘制图表,部署到ARM设备后启动抛HeadlessException,或者报FontConfiguration错误,找不到字体文件。
原因:无头服务器通常没有安装X11、字体库或fontconfig。JDK的图形相关模块初始化时要读取本地字体信息,在精简的ARM发行版上,这些组件要么没装,要么被裁剪掉了。
解决:如果应用本身不依赖真实显示器,最干净的做法是强制开启headless模式:
java -Djava.awt.headless=true -jar app.jar这个参数告诉JVM以无头模式运行,即使系统没有显卡和显示器,也能使用java.awt里的纯逻辑功能。如果应用确实需要生成含文字的图片,光靠headless还不够,必须安装系统字体和字体配置。以多数Linux发行版为例,用系统的包管理器安装fontconfig和fonts-dejavu-core后重启应用即可。我的处理习惯是:部署到ARM服务器上的图形相关代码,先确认业务能在headless模式下运行,再考虑补字体,避免为了一两个图片功能把整个图形环境都装进去。
5. 进阶用法:用jlink裁出几十MB的ARM精简JDK
5.1 用jdeps分析应用依赖模块
jlink适合两类场景:一是按应用实际用到的模块裁剪JDK体积,二是减少ARM设备上不必要的组件、降低攻击面。先用JDK自带的jdeps扫描应用依赖:
jdeps --print-module-deps --ignore-missing-deps app.jar输出会是一段逗号分隔的模块名称列表,例如java.base,java.logging,java.sql。--ignore-missing-deps让非模块化依赖不影响输出结果,对常规的Java服务特别友好,避免报一堆找不到的模块名干扰判断。
5.2 生成精简运行时
拿到依赖清单后,用jlink生成精简运行时:
jlink --module-path $JAVA_HOME/jmods \ --add-modules java.base,java.logging,java.sql,java.naming \ --strip-debug --no-header-files --no-man-pages \ --output /opt/jre11-min--add-modules必须包含java.base,这是所有模块的基础;--strip-debug去掉调试符号,体积更小;--no-header-files和--no-man-pages分别去掉C语言头文件和手册页;--output指定产物目录,要求这个目录事先不存在。生成完成后,用/opt/jre11-min/bin/java -version验证。
5.3 验证与替换
把应用的启动脚本指向新的精简运行时,实测一遍核心功能,特别是涉及JDBC连接、日志输出这些依赖了对应模块的路径。确认无误后,可以将JAVA_HOME改为/opt/jre11-min,完成运行时替换。注意jlink产物目录结构同样包含bin、lib、conf,和完整JDK的布局一致,多版本管理时也可以直接接入update-alternatives。
从那以后,我每次在ARM设备上装JDK,都强制走一遍uname -m确认架构、sha256sum校验包、java -version验证版本这三步,再决定要不要做jlink精简。这套流程帮我避免过不少半夜排查的尴尬。希望帮到你。
本文还有配套的精品资源,点击获取