资讯动态

CentOS Stream 9 安装 JDK 17:二进制归档包与环境变量配置

发布时间:2026/10/27 7:20:27 来源:尧图企业网站定制
1. 为什么在 CentOS Stream 9 上用源码方式装 JDK 17先把结论说在前面在 Linux 服务器上装 Java JDK 17所谓“源码方式安装”绝大多数场景指的并不是拿 OpenJDK 的 C 源码去跑 configure/make 编译出十几个 G 的中间产物而是把发行方打包好的二进制归档包tar.gz解压到指定目录再手工配置JAVA_HOME和PATH。这个叫法在运维圈子里已经约定俗成和“yum 装”“dnf 装”相对。我在 CentOS Stream 9 上给几十台机器做过这套流程也踩过不少只有实际动手才会遇到的坑下面把完整过程和我理解的“为什么”一次讲清楚。这套流程适合的人很明确需要在同一台机器上保留多个 JDK 版本、需要精确控制 JDK 的安装位置、机器访问不了软件源或者内网源里的 JDK 版本太旧、以及给某些中间件比如需要指定 JDK 路径才能启动的服务做环境隔离的场景。如果你只是想在一台个人机器上写个 Hello World那么源里直接装反而更快。但只要你后面要在生产环境跑服务迟早会遇到“系统自带 java 和服务要求的 java 不是同一个”的问题这时候手工安装带来的确定性就很值钱。CentOS Stream 9 这个发行版有个特点需要提前说明它是滚动更新的上游版本软件仓库里的包版本比传统的稳定版更新得快但同时也意味着你今天dnf install java-17-openjdk装上的小版本号过几个月在另一台机器上重装可能就变了。对于要复现环境、要写部署文档、要给多台机器保持一致性的场景这种不确定性是需要主动消除的。手工下载固定版本的归档包把版本号写死在目录名里是最直接的办法。1.1 包管理器安装和手工归档包安装的取舍把两种方式的差异摆到台面上选择就很清楚了。对比维度dnf 安装java-17-openjdk归档包手工安装安装位置固定在/usr/lib/jvm/下路径由包决定完全自定义/usr/local、/opt都行版本锁定跟随仓库可能被更新覆盖目录名带版本号不动就不变多版本共存靠alternatives切换能用但被动手动指定路径切换粒度更细升级方式dnf update一把梭解压新目录 改软链接可回滚依赖处理自动处理需要自己确认 glibc、字体等卸载干净程度包管理器负责要记得删目录、删配置文件、删软链接内网/离线环境需要搭本地源传一个 tar.gz 就完事我自己的判断标准比较朴素这台机器上只跑一个 Java 应用而且版本要求不苛刻用 dnf这台机器要跑多个 Java 应用、版本要求各不相同或者要给外部交付一个可复现的环境用手工包。后一种情况在实际工作中占比不低尤其是那些需要在同一台机器上并存 JDK 8 和 JDK 17 的老系统。还有一个容易被忽略的点dnf 装的 OpenJDK 会附带java-17-openjdk-headless这样的拆分包如果误装了带图形依赖的完整包会在没有桌面环境的服务器上额外拉进一批 X11 相关的库。手工装归档包时JDK 是完整的一坨压缩包里该有的都有不会牵扯系统包依赖这在精简过的服务器镜像上是个实打实的好处。1.2 真正的“源码编译”是什么样为什么我不推荐为了把概念说清楚还是简单交代一下真源码编译的路径。OpenJDK 的源码构建需要先准备一个“引导 JDK”bootstrap JDK通常要求比目标版本低一到两个大版本然后配置 autoconf、编译工具链、各种开发库libX11-devel、libFontconfig-devel、alsa-lib-devel、cups-devel等等跑bash configure再make images。整个过程在一台 4 核 8G 的机器上大致要 30 分钟到 2 小时不等中间任何一步缺依赖都会中断而且报错信息往往指向性不强。编译完得到的产物体积比官方包还大因为没做 strip。这条路适合什么人适合要给 JDK 打补丁、要交叉编译到特殊架构、或者要做 JDK 层面性能调优比如改 JVM 内部的某些常量的团队。绝大多数业务开发者和运维不需要走这条路。官方发布的 tar.gz 已经是编译、测试、签名完成的成品直接解压即用没有任何理由再自己编一遍。所以后面所有实操内容都围绕“二进制归档包 手工配置”这条主线展开。这一点先说透避免有人在网上看到“源码安装 JDK”的标题结果一头扎进编译流程里出不来。1.3 安装前的心理预期哪些东西是会变的在 CentOS Stream 9 上做这套安装有几件事必须提前接受否则后面会反复怀疑自己。第一系统里可能已经有一个 java 了。CentOS Stream 9 的最小化安装通常不带 JDK但如果你装了某些依赖 Java 的软件包或者镜像本身是定制过的/usr/bin/java可能已经存在指向一个你完全不知道的版本。安装前先which -a java看一眼心里有数。第二环境变量的生效范围是有边界的。写在/etc/profile.d/里的脚本只对“登录 shell”生效。你用 SSH 登录敲命令是登录 shell所以看着一切正常但 systemd 服务、cron 任务、ssh 主机 命令这种非交互执行都可能读不到你的配置。这个坑我在实际交付中遇到过不止一次后面单独讲。第三版本号要写死在目录里。别图省事解压成/usr/local/java否则将来想同时放两个版本时就尴尬了。目录名带完整版本号再做一个软链接指向它升级时只改软链接配置文件和启动脚本都不用动这是最省心的做法。2. 系统环境确认与目录规划动手之前花五分钟做环境确认能省掉后面半小时的排查。这段内容看起来啰嗦但每一条检查我都见过因为跳过而出问题的案例。2.1 三行命令确认系统底子第一件事是确认发行版和架构。注意 CentOS Stream 9 的版本信息文件和传统 CentOS 不完全一样。cat /etc/centos-release # CentOS Stream release 9 uname -m # x86_64 或 aarch64 getconf GNU_LIBC_VERSION # glibc 2.34uname -m这一条尤其关键。现在 ARM 架构的服务器越来越多如果你的机器输出是aarch64却下了一个x64的包解压后执行java -version会得到cannot execute binary file: Exec format error这种一眼看不懂的报错。JDK 17 对 glibc 的最低要求是 2.17CentOS Stream 9 自带的 2.34 完全满足这一条基本不用操心但如果你的机器是从更老的系统原地升级上来的还是确认一下比较稳妥。再顺手看一眼磁盘空间。一个完整 JDK 17 解压后大约 300MB 左右加上下载的压缩包/usr/local所在分区至少要有 1GB 余量。用df -h /usr/local看一眼如果/usr/local和/在同一个分区且使用率已经 80% 以上先清理再动手。df -h /usr/local free -h内存这条不是给安装用的是给后面验证用的。JDK 17 的 JVM 启动时会根据物理内存计算默认堆大小在内存极小的容器里可能出现启动参数相关的告警。提前知道机器的内存量验证阶段看到告警就不会慌。2.2 版本选择17 的哪个小版本哪个发行版JDK 17 是长期支持版本小版本会持续更新。选哪个小版本我的建议是选当前最新的 17.x 的补丁版本但不要盲目追最新的主版本。补丁版本包含安全修复和 bug 修复升级风险很低而跨大版本升级比如 17 升到 21涉及行为变化需要单独评估。至于发行版的选择这块我给的是判断方法而不是具体推荐。市面上的 JDK 17 构建大致分几类完全开源的社区构建、带商业支持条款的商业发行版、以及云厂商针对自家环境优化过的版本。选择时看三个点就够了许可条款确认你在生产环境的使用方式是否被允许特别是要打包进容器镜像分发的场景。更新节奏安全补丁跟进得快不快有没有公开的更新记录可查。体积和组成是否包含 JavaFX、是否做了 jlink 精简、是否只提供 headless 版本。对于内部系统、不对外分发的场景绝大多数开源构建都能满足。我在实际项目中倾向于固定使用同一个构建来源把它写进团队的部署规范里避免不同人装出不同东西。这一步的价值在于消除变量而不是追求某个“最优解”。提示下载页面上的文件通常有tar.gz和zip两种。Linux 上用tar.gz别下成zip再回头找解压工具。另外注意区分jdk-17_linux-x64_bin.tar.gz和jdk-17_linux-x64_bin.rpm后者是给 rpm 系包管理器用的手工安装用不上。2.3 目录规划为什么我不用 /opt安装目录的选择社区里常年有两派/usr/local派和/opt派。我的习惯是/usr/local/java/理由有三条。第一/usr/local在文件系统层级标准里的定位就是“本机自行安装的软件”语义上最贴合。第二绝大多数默认配置里的PATH已经包含/usr/local/bin把软链接放进去天然可用不需要额外改路径。第三团队里其他人接手时看到/usr/local就能猜到这是手工装的减少沟通成本。规划好的目录结构长这样/usr/local/src/ 下载的压缩包临时存放 /usr/local/java/jdk-17.0.11/ 实际解压出来的 JDK 目录 /usr/local/java/jdk17 - jdk-17.0.11 软链接配置里只引用这个 /usr/local/bin/java - /usr/local/java/jdk17/bin/java /etc/profile.d/jdk17.sh 环境变量脚本这个结构里最重要的设计是中间那层软链接。所有环境变量、启动脚本、systemd unit 文件一律引用/usr/local/java/jdk17永远不直接写带小版本号的路径。将来升级到 17.0.12只需要解压新目录、把软链接重新指一下、验证通过后删掉旧目录业务侧完全无感。如果直接把JAVA_HOME写成/usr/local/java/jdk-17.0.11升级时就要满机器去找哪些地方写了这个路径风险大得多。注意不要把 JDK 解压到/tmp下再想长期使用。很多加固过的系统会给/tmp挂noexec选项/tmp下的二进制文件根本执行不了你会看到Permission denied而且这个报错和文件权限位看起来毫无关系非常容易误判。同理/home下也不建议放某些 daemon 用户的 home 目录有限制。3. 下载、校验与解压的正规流程这段是纯操作但每一步都有它存在的理由我会把理由一起讲。3.1 下载与网络受限时的替代路径先在机器上建好目录然后下载。为了命令可复制这里用变量占位表示下载地址实际执行时替换成你从发行方拿到的真实链接。mkdir -p /usr/local/src cd /usr/local/src # 占位示意把 URL 换成实际的下载地址 curl -fLO https://下载地址/jdk-17.0.11_linux-x64_bin.tar.gz ls -lh jdk-17.0.11_linux-x64_bin.tar.gzcurl的-f参数很关键它让 HTTP 状态码非 2xx 时直接报错退出而不是把一段 HTML 错误页面保存成.tar.gz文件。不带-f的话你下载失败后得到的是一个几十 KB 的“压缩包”解压时才报gzip: stdin: not in gzip format排查方向会被带偏。如果服务器不能直连外网有三条路可走。一是从一台能上网的机器下载好后用scp或内部文件服务传过去二是走内网的制品仓库很多团队会把常用中间件包统一托管三是如果目标机器上有可用的包管理器源直接dnf download把 rpm 拉下来再提取也可以但那样就不算手工安装了这里不展开。无论走哪条路都要保证传输过程不失真传完用第 3.2 节的校验和核对一遍。3.2 校验和验证别省这三十秒这一步在很多教程里被略过了但我坚持每次都做。原因很实在传输中断、代理缓存返回旧版本、下载源本身有问题这三种情况都会导致你拿到一个“能解压、能跑但内容有问题”的包这类问题排查起来极其痛苦。# 发行方一般会提供对应的 sha256 值 sha256sum jdk-17.0.11_linux-x64_bin.tar.gz拿到输出的哈希值和下载页面上公布的值逐位比对。如果发行方提供的是.sha256文件更省事的做法是curl -fLO https://下载地址/jdk-17.0.11_linux-x64_bin.tar.gz.sha256 sha256sum -c jdk-17.0.11_linux-x64_bin.tar.gz.sha256 # 输出 OK 就说明包是完整的sha256sum -c的返回值可以直接用于脚本判断$?为 0 即校验通过。如果你在写自动化部署脚本这两行就是标准的防呆环节。3.3 解压、归属与权限校验通过后解压。注意 JDK 归档包解压出来自带顶层目录jdk-17.0.11所以解压目标选/usr/local/java就行不要再手动套一层。mkdir -p /usr/local/java tar -xzf jdk-17.0.11_linux-x64_bin.tar.gz -C /usr/local/java/ cd /usr/local/java ln -sfn jdk-17.0.11 jdk17 ls -l /usr/local/java/ln -sfn里的-n是给“软链接已存在且指向目录”这种情况用的。如果jdk17已经是一个指向目录的软链接不带-n的ln -sf会在目标目录内部再创建一个链接形成/usr/local/java/jdk17/jdk17这种奇怪的嵌套。这个坑我在升级脚本里踩过一次后来所有软链接操作都固定加-n。权限方面JDK 目录保持root:root所有、普通用户只读即可不需要chmod 777。有些人为了让应用用户能读直接一把chmod -R 777这在安全审计里是明确的问题项。实际上755的目录权限任何用户都能读和执行够用了。chown -R root:root /usr/local/java chmod -R 755 /usr/local/java再补充一个实际会遇到的细节有些 Java 应用需要字体渲染比如生成图片验证码、导出带文字的 PDF。JDK 本身不带字体依赖系统的 fontconfig。如果服务器是最小化安装可能一个字体都没有运行时抛出Fontconfig head is null之类的错误。提前装一下基础字体能省掉后面一顿排查。dnf install -y fontconfig dejavu-sans-fonts fc-cache -fv如果你的应用完全不碰图形和文字渲染这一步可以跳过。判断方法是看依赖里有没有java.awt、javax.imageio相关的调用。4. 环境变量配置细节都在边界条件里环境变量这块写对很容易写“对得没有漏洞”不容易。很多人配完发现当前会话能用换个用户、换个连接方式就不行了问题基本都出在配置文件的加载时机上。4.1 配置文件放在哪三个位置的取舍Linux 上能放环境变量的地方有好几个作用范围完全不同。先把这个搞清楚再决定写哪。文件位置生效范围生效时机适合场景/etc/profile.d/*.sh所有用户登录 shell 启动时系统级统一 JDK推荐/etc/profile所有用户登录 shell 启动时不推荐改容易被系统更新覆盖~/.bashrc当前用户每次交互式 bash 启动个人开发机、需要单独版本~/.bash_profile当前用户登录 shell个人机器systemd unit 的Environment单个服务服务启动时生产服务最稳我的标准做法是系统级配置写/etc/profile.d/jdk17.sh生产服务再在 unit 文件里显式声明一次。双保险看着冗余但能避免“手动执行没问题、systemd 起服务就找不到 java”这类经典故障。/etc/profile.d/这个目录有个加载规则需要注意/etc/profile里是用for i in /etc/profile.d/*.sh的方式遍历加载的所以文件名必须以.sh结尾否则不会被读取。我见过有人把文件命名成jdk17.conf然后对着echo $JAVA_HOME的空输出怀疑人生。另外文件权限要是644如果误设成600其他用户登录时读不到也会静默失效。4.2 变量逐个拆解每个都有它的作用新建配置文件cat /etc/profile.d/jdk17.sh EOF export JAVA_HOME/usr/local/java/jdk17 export JRE_HOME${JAVA_HOME}/jre export CLASSPATH.:${JAVA_HOME}/lib:${JRE_HOME}/lib export PATH${JAVA_HOME}/bin:${PATH} EOF chmod 644 /etc/profile.d/jdk17.sh这里逐条说明为什么要这么写。JAVA_HOME指向软链接而不是实际目录前面已经说过是为了升级时不用改配置。几乎所有 Java 生态的中间件都读这个变量Tomcat 的启动脚本、Maven 的mvn脚本、Gradle 的 wrapper第一件事都是找JAVA_HOME。如果这个变量没设它们会退而求其次去PATH里找java找到哪个用哪个行为不可控。PATH的拼接顺序有讲究。${JAVA_HOME}/bin放在${PATH}前面才能保证新装的 JDK 优先级高于系统里已有的/usr/bin/java。如果写成PATH${PATH}:${JAVA_HOME}/bin那么系统自带的旧版本会一直赢你会看到java -version输出一个莫名其妙的版本号。CLASSPATH这一条说实话在 JDK 17 的时代基本是历史遗留。现代构建工具Maven、Gradle和启动脚本都会自己管理 classpath显式设置CLASSPATH反而可能带来干扰。但我还是保留了原因是有些老旧的脚本或者手写的java -cp命令会依赖它保留一个包含当前目录.的最小配置兼容性更好。如果你确定环境里没有这类东西删掉这一行也没问题。JRE_HOME同理。JDK 9 之后 JRE 就不再单独分发了${JAVA_HOME}/jre这个目录在 JDK 17 里根本不存在。保留它是为了兼容那些还在读JRE_HOME的老脚本。这不是错误是有意为之的兼容层但你要知道它指向的目录不存在别在排查问题时被它误导。4.3 立即生效与分层验证配置写完后当前会话不会自动感知必须重新加载。source /etc/profile.d/jdk17.sh echo $JAVA_HOME # /usr/local/java/jdk17 which java # /usr/local/java/jdk17/bin/java java -versionjava -version的输出有个细节它是打到标准错误流stderr的不是标准输出。所以在脚本里判断版本时直接java -version | grep 17会拿到空结果正确写法是java -version 21 | grep 17。这个坑非常普遍我第一次写部署脚本时就栽在这里查了半小时以为是 JDK 没装好。验证要分层做一层层排除问题# 第一层变量对不对 env | grep -E JAVA_HOME|JRE_HOME # 第二层命令找到的是不是你想的那个 which -a java ls -l $(which java) # 第三层版本对不对 java -version 21 # 第四层编译器在不在只装 JRE 的话这里会失败 javac -version # 第五层运行时自报的家目录最权威 java -XshowSettings:properties -version 21 | grep -E java.home|java.version第五层的java.home是最可信的判断依据因为它直接反映 JVM 自己认为的安装位置不受PATH和软链接的干扰。如果JAVA_HOME和java.home不一致说明你的PATH里混进了别的东西回去查which -a java的输出。最后跑个真实的编译执行确保整条链路是通的mkdir -p /tmp/javatest cd /tmp/javatest cat Hello.java EOF public class Hello { public static void main(String[] args) { System.out.println(JDK version: System.getProperty(java.version)); System.out.println(Vendor: System.getProperty(java.vendor)); System.out.println(Heap max: Runtime.getRuntime().maxMemory() / 1024 / 1024 MB); } } EOF javac Hello.java java Hello能看到版本号和堆大小输出就说明编译、运行、JVM 参数解析全部正常。这一步看着多余但它同时验证了javac可用、java可用、临时目录可执行、内存识别正常一次性排掉四类问题。5. 多版本共存alternatives 的正确用法一台机器上并存 JDK 8 和 JDK 17 是很常见的需求尤其是老系统迁移期间。这时候光靠PATH顺序切换会很别扭alternatives机制就是为这个场景设计的。5.1 alternatives 到底在做什么它的原理不复杂/usr/bin/java本身不是真实的二进制文件而是一个指向/etc/alternatives/java的软链接而/etc/alternatives/java又是一个软链接指向你注册的某个实际路径。切换版本时改的是中间那一层链接所有指向/usr/bin/java的地方自动跟着变。/usr/bin/java - /etc/alternatives/java - /usr/local/java/jdk17/bin/java理解了这条链很多现象就好解释了。比如你发现/usr/bin/java指向的版本和你echo $JAVA_HOME不一致那是因为PATH里${JAVA_HOME}/bin排在/usr/bin前面直接命中了真实路径绕过了 alternatives 这一层。两种机制共存时会互相干扰需要明确以哪个为准。我的建议是系统级统一用/etc/profile.d/配置alternatives 只用来管理/usr/bin下的命令两者不要指望自动同步。换句话说alternatives 是给“不读环境变量的场景”兜底的比如某个脚本硬编码了/usr/bin/java或者某个服务以nobody用户运行、读不到你的 profile 配置。5.2 注册与切换的实操命令注册的时候最后那个数字是优先级数字越大优先级越高alternatives --auto会自动选优先级最高的。# 注册 java alternatives --install /usr/bin/java java /usr/local/java/jdk17/bin/java 1700 # 一并注册其他常用命令不然会出现 java 是 17、javac 是 8 的诡异情况 alternatives --install /usr/bin/javac javac /usr/local/java/jdk17/bin/javac 1700 alternatives --install /usr/bin/jar jar /usr/local/java/jdk17/bin/jar 1700 alternatives --install /usr/bin/keytool keytool /usr/local/java/jdk17/bin/keytool 1700 alternatives --install /usr/bin/jshell jshell /usr/local/java/jdk17/bin/jshell 1700切换用alternatives --config java它会列出所有已注册的候选输入编号回车即可。切完再java -version确认一下。如果机器上还装着系统源的 JDK也要一并注册进去这样才能在列表里看到两个候选# 假设系统源装的是 JDK 8 alternatives --install /usr/bin/java java /usr/lib/jvm/java-1.8.0-openjdk/bin/java 800 alternatives --install /usr/bin/javac javac /usr/lib/jvm/java-1.8.0-openjdk/bin/javac 800注意alternatives只改/usr/bin下的命令指向不会改JAVA_HOME。如果你的应用脚本依赖JAVA_HOME切换 alternatives 之后它拿到的还是原来的值。这就是前面说的“两套机制不同步”的问题务必在部署文档里写清楚你们团队以哪一套为准。还有一个容易忽略的java和javac必须指向同一个 JDK 版本。曾经遇到过java是 17 而javac还是 11 的机器编译能过、运行时抛UnsupportedClassVersionError看着像代码问题实际是环境问题。注册时把所有相关命令一起注册就是防这个。6. 验证之后的排查实录这部分是我认为整篇文章最有价值的部分。前面所有步骤照着做基本都能成功真正消耗时间的是“明明都做了但就是不生效”的情况。6.1 检查生效范围的边界条件配置写好了source之后当前窗口能用但换个场景就失效。这几个场景我都遇到过逐个说明。场景一sudo执行时找不到 java。原因是sudo默认会重置环境变量env_reset并且使用secure_path里定义的路径你的/etc/profile.d/配置被丢掉了。表现是java -version正常sudo java -version报command not found。解决办法有三个用绝对路径sudo /usr/local/java/jdk17/bin/java -version用sudo -E保留环境需要 sudoers 里允许或者直接在/etc/sudoers的secure_path里加上 JDK 的 bin 目录。我一般推荐第一个最简单也不用改系统配置。场景二systemd 服务里找不到 java。这是最经典的一个。systemd 启动的服务不经过 shell完全不读/etc/profile.d/。必须在 unit 文件里显式声明[Service] EnvironmentJAVA_HOME/usr/local/java/jdk17 EnvironmentPATH/usr/local/java/jdk17/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin ExecStart/usr/local/java/jdk17/bin/java -jar /opt/app/app.jarPATH也要显式写因为 systemd 的默认 PATH 里没有你的 JDK 目录。改完记得systemctl daemon-reload再重启服务否则新配置不加载。场景三cron 任务里找不到 java。cron 的环境极其精简PATH通常只有/usr/bin:/bin。在 crontab 里直接写java大概率失败。两个办法在 crontab 顶部加JAVA_HOME...和PATH...的定义或者在脚本里用绝对路径。我倾向后者脚本自带环境依赖不依赖外部配置。场景四ssh 主机 java -version找不到。非交互式执行的命令不加载/etc/profile只加载部分配置。这就是前面提到的“看着一切正常”的陷阱因为手动登录测试永远测不出来。用绝对路径或者把变量也写进~/.bashrcbash 在非交互模式下会读BASH_ENV指定的文件路径匹配的话~/.bashrc也会被读但行为因配置而异不如用绝对路径稳。6.2 常见问题速查表下面这张表是我这些年攒下来的基本覆盖了安装 JDK 17 会踩到的九成问题。现象大概率原因处理方式java: command not found配置文件没加载或文件名不是.sh检查/etc/profile.d/jdk17.sh后缀和权限重新登录cannot execute binary file下载的架构不对x64 装到 aarch64uname -m确认后重新下载对应架构包Permission denied执行 java所在目录挂载了noexecmountjava -version版本不对PATH顺序问题或有旧版本抢占which -a java调整 PATH 拼接顺序javac和java版本不一致alternatives 只注册了其中一个把所有相关命令一起注册gzip: stdin: not in gzip format下载失败得到的是 HTML 页面用curl -f重新下载校验 sha256Fontconfig head is null系统缺字体dnf install fontconfig dejavu-sans-fontsInaccessibleObjectExceptionJDK 17 强封装内部 API老框架反射被拦加--add-opens java.base/java.langALL-UNNAMED等参数UnsupportedClassVersionError编译用的 JDK 比运行的高统一编译和运行环境或降-source/-target中文输出乱码locale 没设或file.encoding不对设LANGzh_CN.UTF-8或加-Dfile.encodingUTF-8服务启动后立即退出systemd 里没有JAVA_HOMEunit 文件里补Environment内存告警/堆大小异常容器里 JVM 读的是宿主机内存显式设-Xmx或启用容器感知参数6.3 几个只有实际用过才知道的坑JDK 17 的强封装是真的会拦住东西。从 JDK 16 开始--illegal-accesspermit这个兼容开关被移除了JDK 17 里默认就是强封装。一些依赖深度反射的老框架各种老版本的序列化库、某些 ORM、某些工具类会直接抛InaccessibleObjectException。解决方式是在启动参数里加--add-opensjava --add-opens java.base/java.langALL-UNNAMED \ --add-opens java.base/java.utilALL-UNNAMED \ --add-opens java.base/java.nioALL-UNNAMED \ -jar app.jar需要开哪个包看报错信息里module X does not opens Y那一段照着加就行。别图省事一次开十几个--add-opens会影响 JIT 优化开得越多性能损失越大。容器里的内存识别问题。如果你的 JDK 跑在容器里早期版本的 JVM 会读到宿主机的总内存然后按比例算出一个巨大的默认堆容器一启动就被 OOM Killer 干掉。JDK 17 已经支持容器感知UseContainerSupport默认开启但如果你在 Kubernetes 里限了limits.memory又没设-Xmx还是建议显式指定堆大小把控制权拿在自己手里。经验值是把-Xmx设成容器内存上限的 60% 到 70%剩下的留给元空间、线程栈和堆外内存。file.encoding的默认值陷阱。JDK 18 之前file.encoding默认跟随系统 locale。如果你的服务器 locale 是POSIX或者空的默认字符集可能是ANSI_X3.4-1968读文件时中文直接变小问号。CentOS Stream 9 最小化安装默认 locale 往往是C.UTF-8一般没事但一旦是从更老的系统迁过来的环境就要小心。稳妥做法是启动参数里显式加-Dfile.encodingUTF-8别依赖环境。软链接的绝对路径问题。用ln -s创建软链接时如果用相对路径链接的有效性依赖于当前工作目录一旦目录移动就断了。所有软链接统一用绝对路径能避免这类隐蔽故障。# 对的 ln -sfn /usr/local/java/jdk-17.0.11 /usr/local/java/jdk17 # 有风险的 cd /usr/local/java ln -sfn jdk-17.0.11 jdk17这两条命令创建出来的链接ls -l看着都正常但行为不同。前者换个目录访问照样有效后者在别的地方访问可能就指向不存在的目标。7. 升级与干净卸载的处理方式装完之后日常运维还有两件事将来怎么升小版本以及万一装错了怎么卸干净。7.1 小版本升级的三步走假设要从 17.0.11 升到 17.0.12流程是这样的。# 第一步新版本解压到新目录不动老的 cd /usr/local/src # 下载并校验 17.0.12 tar -xzf jdk-17.0.12_linux-x64_bin.tar.gz -C /usr/local/java/ # 第二步先把软链接切到新目录重启一个非核心服务验证 ln -sfn /usr/local/java/jdk-17.0.12 /usr/local/java/jdk17 java -version 21 # 第三步确认业务正常后删掉旧目录回收空间 rm -rf /usr/local/java/jdk-17.0.11这里的关键是软链接切换本身是可逆的。切过去之后如果发现问题一条命令就能切回来因为旧目录还在。别一上来就删旧版本那样出问题只能重装。另外如果你的机器上配了 alternatives切换软链接之后还要重新指一下 alternatives因为它记录的是绝对路径软链接切换对它没有影响。这一点经常被忽略alternatives --install /usr/bin/java java /usr/local/java/jdk-17.0.12/bin/java 1712 alternatives --config java至于大版本升级比如 17 升 21就不只是换个目录的事了。要评估废弃 API 的移除、GC 默认策略的变化、第三方库的兼容性还要看应用启动参数里有没有被标记为废弃的选项。建议先在测试环境完整跑一轮重点看启动日志里的WARNING和Unrecognized VM option这类提示。7.2 卸干净的标准动作手工装的东西卸载也要手工来别指望包管理器。按这个顺序做基本不会有残留。# 1. 清理 alternatives 注册 alternatives --remove java /usr/local/java/jdk17/bin/java alternatives --remove javac /usr/local/java/jdk17/bin/javac # 其他注册过的命令同理 # 2. 删除 /usr/local/bin 下的软链接如果建过 rm -f /usr/local/bin/java /usr/local/bin/javac # 3. 删除环境变量配置 rm -f /etc/profile.d/jdk17.sh # 4. 删除安装目录 rm -rf /usr/local/java # 5. 清理下载的压缩包 rm -f /usr/local/src/jdk-17*.tar.gz # 6. 重新登录或 source 一下确认变量已消失 env | grep -i java第 6 步很重要。如果env | grep -i java还有输出说明还有别的地方在设这些变量可能是~/.bashrc、可能是/etc/environment也可能是某个应用自己的配置。不查清楚就装新版本很容易出现两个版本配置打架的情况。再补充一条经验卸载前先确认没有正在运行的进程依赖这个 JDK。用jps -l或者ps -ef | grep java看一眼如果有业务进程在跑先停掉再删目录否则进程虽然还能继续跑因为文件句柄还在但重启时就起不来了而且报错信息会让你以为 JDK 装错了。我个人在实际操作中的体会是这套“归档包 带版本号的目录 稳定软链接 profile.d 配置 必要时 alternatives 兜底”的组合只要一次搭好后面几年的升级几乎不用动脑子。真正容易翻车的从来不是解压和改配置而是那些环境变量的生效边界——登录 shell 和非登录 shell 的差别、sudo重置环境、systemd 不读 profile这三件事吃透之后一台机器上跑三四个不同版本的 Java 应用就是很平常的事。最后分享一个小技巧把整个流程写成一个幂等的 shell 脚本用if [ -x /usr/local/java/jdk17/bin/java ]判断是否已经装过重复执行也不会出错交付新机器的时候一条命令跑完比手工敲十几遍可靠得多。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑