CentOS 8 安装第二个 JDK:多版本共存与隔离切换指南 1. 为什么一台机器上非要装第二个 JDK先说场景。很多团队的业务机器上原本跑着一套老服务JDK 8 稳稳当当跑了好几年谁也不想动它。某天来了个新需求编译一个只支持高版本的新组件或者要临时验证一个在新 JDK 上才能跑起来的构建流程。这时候最省事的想法是直接升级现有 JDK但只要动手过的人都清楚老服务依赖的启动脚本、环境变量、第三方库的字节码兼容性任何一处对不上第二天凌晨出问题的就是你。所以真正稳妥的做法是让两个 JDK 并存各管各的谁也不污染谁。装第二个 JDK 不难难的是隔离两个字——很多人装完之后发现java -version依然是老版本或者老服务重启后莫名报错根子就在环境变量的作用域和优先级没理清。这篇内容解决的就是这件事在一台已经装着旧版 JDK 的 CentOS 8 机器上把第二个 JDK 完整装好并且做到切换干净、互不干扰。适合两类人看一是有一定 Linux 基础、能看懂PATH和软链接的新手运维二是被装完不生效折腾过、想搞清楚原理的开发。文中的路径和版本号你可以替换成自己的但思路和顺序建议照抄因为顺序错了排查成本会翻好几倍。需要提前明确一点CentOS 8 的软件源状态比较特殊官方镜像仓库已经迁移直接用yum install java这类命令往往不顺利。我下面走的路线是离线压缩包 手动配置好处是完全不依赖网络源坏处是每一步都得自己来。这条路线在任何发行版上都通用学会了换机器也不慌。2. 动手之前先摸清家底现有 JDK 到底装在哪2.1 三条命令定位现有环境装第二套之前必须先把第一套的位置、来源、配置方式全部搞清楚。很多隔离失败的案例本质是机器上存在两处以上的 Java 配置改了一处漏了另一处。依次执行下面几条命令把结果记下来which java readlink -f $(which java) java -version echo $JAVA_HOMEwhich java告诉你当前 shell 调用的是哪个可执行文件readlink -f顺着软链接一路追到真实路径这一步最关键因为/usr/bin/java几乎肯定是个软链接java -version确认版本echo $JAVA_HOME看环境变量是否已设置输出为空说明没有全局配置。接着查系统级配置文件和包管理器记录rpm -qa | grep -i jdk ls -l /usr/lib/jvm/ cat /etc/profile ls /etc/profile.d/rpm -qa能看出旧 JDK 是不是当年用包管理器装的。如果是alternatives机制可能已经接管了java命令的指向这一点后面第 4 章会专门处理。/usr/lib/jvm/是多数发行版存放 JDK 的惯例目录如果里面有东西说明至少有一套是通过包管理器或手动放在这里的。2.2 判断旧 JDK 的配置来源这一步决定了你后续改哪里。常见的配置位置有四种我按优先级从高到低排配置位置生效范围优先级说明当前 shell 手动 export仅当前会话最高关掉终端即失效最容易造成明明配了却不生效的假象~/.bashrc/~/.bash_profile单个用户高只影响当前用户切换用户就变/etc/profile.d/*.sh全部用户中系统级推荐把新配置放这里/etc/profile全部用户中老配置常在这里改动需谨慎用grep -r JAVA_HOME /etc/profile /etc/profile.d/ ~/.bashrc ~/.bash_profile 2/dev/null一条命令把上面所有位置扫一遍输出里出现的每一行都要逐一确认内容。我见过最坑的情况是/etc/profile里写了一份旧路径/etc/profile.d/里又写了一份新的两份互相覆盖最终生效的取决于文件名的加载顺序按字母序。这种配置下你再叠加第三份结果基本靠猜。提示在做任何修改前先备份。cp /etc/profile /etc/profile.bak.$(date %F)这条命令花不了两秒钟但能救你一次远程登录失败。2.3 确认磁盘空间与位数JDK 解压后通常占 300MB 到 500MB加上临时文件和后续可能的多版本共存建议/opt或你选定的安装目录所在分区至少有 2GB 空闲。用df -h /opt看一眼再用uname -m确认架构是x86_64还是aarch64下载的包必须和架构一致下错了解压能成功但执行会报 cannot execute binary file这个错很容易被误判成权限问题。3. 拿到安装包下载、校验、解压的门道3.1 选哪个版本怎么选第二个 JDK 的版本选择要看你的真实目的。如果是为了跑新构建选当前主流的长期支持版本即可如果是为了复现某个特定的构建环境那就严格对齐版本号差一个小版本都可能出问题。我建议优先选压缩包tar.gz形式而不是 rpm 或 deb 包。原因很直接压缩包解压即用不往系统目录里写东西不注册服务不修改alternatives删除时一个rm -rf就干净了。而包管理器安装会自动接管/usr/bin/java反而给你增加隔离难度——这也是为什么我不推荐在已有环境的机器上用包管理器装第二个 JDK。版本号里还有个细节1.8.0_xxx这种命名是 JDK 8 时代的写法JDK 9 之后统一叫9、11、17、21。脚本里做版本判断时千万别用字符串前缀匹配1.8之外的值容易翻车。另外注意区分 JDK 和 JRE我们要装的是 JDK含编译器JRE 里没有javac装完你会发现编译命令找不到。3.2 传输与校验从内网文件服务器或本地下载好后用scp传到目标机器scp jdk-xx_linux-x64_bin.tar.gz userhost:/tmp/传完先算校验和和官方提供的值比对sha256sum /tmp/jdk-xx_linux-x64_bin.tar.gz这一步不能省。大文件传输中断、磁盘写入错误都会导致压缩包损坏而损坏的包有时能解压出大部分文件只在某个lib下的库文件上出问题跑起来报的错莫名其妙。花三十秒校验能省掉两小时的怀疑人生。3.3 解压到独立目录安装位置建议单独开一个目录不要塞进/usr/lib/jvm也不要和旧 JDK 同级混放。我自己习惯用/opt/jdk/版本号这种结构mkdir -p /opt/jdk tar -zxvf /tmp/jdk-xx_linux-x64_bin.tar.gz -C /opt/jdk/解压后确认目录结构和可执行文件ls /opt/jdk/jdk-xx/bin/ | head -20 /opt/jdk/jdk-xx/bin/java -version注意这里是用绝对路径直接调用不依赖任何环境变量。如果这一步能正常打印版本号说明包本身没问题后面出问题一定是配置问题。这个先验证二进制、再验证环境变量的排查顺序是我处理这类问题时的固定套路能把问题范围一刀切成两半。4. 隔离的核心PATH、JAVA_HOME 与 alternatives 三者的关系4.1 环境变量到底谁说了算要理解隔离先得理解 shell 找命令的规则。你敲java的时候shell 会按PATH里从左到右的顺序依次查找找到第一个就停后面的不再看。所以哪个 JDK 生效完全取决于谁的bin目录在PATH里排得更靠前。JAVA_HOME则是给应用程序看的很多启动脚本、构建工具如 Maven、Gradle 的 wrapper会读这个变量去定位 JDK。它和PATH是两个独立的东西只改一个不改另一个就会出现命令行 java 是新版、但程序跑起来还是老版的经典错位。正确的配置必须让两者指向同一个目录export JAVA_HOME/opt/jdk/jdk-xx export PATH$JAVA_HOME/bin:$PATH关键在$JAVA_HOME/bin拼在$PATH前面这样新 JDK 的bin优先级最高。如果写成PATH$PATH:$JAVA_HOME/bin新路径排在最后几乎肯定被前面的老路径挡住于是你改了配置却看不出任何变化——这是最高频的踩坑点没有之一。4.2 alternatives 机制要不要碰如果旧 JDK 是通过包管理器装的系统里很可能启用了alternatives它在/usr/bin/java和真实的 JDK 之间插了一层软链接管理。这种情况下即使你配好了PATH某些不读PATH、直接调用/usr/bin/java的脚本仍然会用老版本。先查看现状alternatives --display java如果输出里能看到多个候选项说明机制已启用。我的建议是新建的第二个 JDK 不要注册进 alternatives。理由是这样能保持边界清晰——alternatives 管的是系统默认而我们的目标是给特定用户或特定项目用新版注册进去反而把两套环境又搅在一起了。反过来如果旧 JDK 确实是通过 alternatives 管理的而你想让新 JDK 成为全局默认那才需要注册alternatives --install /usr/bin/java java /opt/jdk/jdk-xx/bin/java 200 alternatives --config java后面的数字是优先级越大越优先。但请注意这只改了java命令javac得单独再注册一次否则编译时依然走老版本。这一对不一致是无数编译失败但版本号看着没错问题的源头。4.3 三种隔离粒度按需选一种隔离不是只有一种做法粒度不同适用场景也不同隔离粒度配置位置适用场景优点缺点用户级~/.bashrc只有某个账号需要新版影响面最小最安全换账号就失效会话级手动 export 或写成脚本 source临时验证、一次性构建完全不留痕迹关终端即失效系统级/etc/profile.d/jdk-xx.sh全机器统一用新版所有用户一致影响旧服务需评估绝大多数装第二个 JDK的真实需求用用户级就够了。老服务通常由专用账号启动那个账号继续读老配置互不干扰新需求在你的账号下跑配置只挂在你自己身上。这是风险最低的方案也是我实际项目中默认采用的。5. 落地配置把新 JDK 挂到指定用户身上5.1 写一份独立的 profile 脚本不要在~/.bashrc里直接堆 export 语句而是单独建一个文件需要时 source不想用就删管理起来清爽。在用户家目录下建jdk-env.shcat ~/jdk-env.sh EOF # 新 JDK 隔离环境 export JAVA_HOME/opt/jdk/jdk-xx export PATH$JAVA_HOME/bin:$PATH export CLASSPATH.:$JAVA_HOME/lib EOF然后让登录时自动加载在~/.bashrc末尾追加一行echo source ~/jdk-env.sh ~/.bashrc这里用单引号EOF包裹 heredoc 很重要它能阻止变量在写入时被提前展开。如果写成不带引号的EOF$JAVA_HOME和$PATH会在创建文件的那一刻就被替换成当前值等你以后换版本再改脚本时就完全不生效了而文件内容看起来完全正常排查起来极其难受。5.2 让配置立即生效并验证source ~/jdk-env.sh which java java -version echo $JAVA_HOME三个输出必须互相印证which java指向/opt/jdk/jdk-xx/bin/javajava -version显示新版本JAVA_HOME指向/opt/jdk/jdk-xx。任何一项对不上就回到第 4.1 节检查PATH的拼接顺序。再验证编译器javac -version很多时候java已经切过去了javac还在老位置因为某些旧配置里单独写死了javac的路径。javac版本和java版本必须一致否则会出现编译通过但运行报 UnsupportedClassVersionError的情况。5.3 用脚本做隔离切换而不是靠记忆如果你经常在两个环境间来回切手敲 export 迟早出错。写两个小脚本cat ~/use-jdk-new.sh EOF #!/bin/bash export JAVA_HOME/opt/jdk/jdk-xx export PATH$JAVA_HOME/bin:$PATH echo 已切换到新 JDK: $(java -version 21 | head -1) EOF cat ~/use-jdk-old.sh EOF #!/bin/bash export JAVA_HOME/usr/lib/jvm/old-jdk export PATH$JAVA_HOME/bin:$PATH echo 已切换到旧 JDK: $(java -version 21 | head -1) EOF chmod x ~/use-jdk-new.sh ~/use-jdk-old.sh注意这两个脚本必须用source执行而不是直接运行。直接执行会在子 shell 里改变量脚本一结束变量就没了回到父 shell 依然是老环境。用.或source才是在当前 shell 里生效source ~/use-jdk-new.sh这个小细节坑过太多人。判断方法很简单执行完立刻echo $JAVA_HOME如果值没变八成就是用了直接执行的方式。6. 装完之后最容易踩的五个坑6.1 坑一改了配置但新开的终端不生效现象是当前终端切过去了关掉重开又变回老版本。原因通常是改了~/.bashrc但登录时加载的其实是~/.bash_profile或者~/.profile后者不会自动读前者。解决办法是在~/.bash_profile里加一行引入if [ -f ~/.bashrc ]; then source ~/.bashrc fi很多发行版的默认~/.bash_profile里本来就有这段但被前人改过之后可能就丢了。这是配置生效范围问题不是配置内容问题所以只看配置文件内容永远找不到原因。6.2 坑二sudo之后 JDK 变了sudo默认会重置环境变量切到 root 的配置。所以你在自己账号下是新的sudo java -version却显示老的这不代表配置错了。如果确实需要 sudo 下也用新 JDK用sudo -E保留环境或者直接在 root 的配置里也加上。我的做法是尽量不给 sudo 下的命令配 JDK需要编译就用普通账号权限问题和环境问题分开处理免得两件事缠一起。6.3 坑三cron 任务里 JDK 还是旧的cron 执行时环境极其精简几乎只有系统默认的PATH不读~/.bashrc。所以你在终端里测得好好的脚本丢进 cron 就跑挂报的还是找不到 java或版本不符。解决办法有两个我推荐第二个在 crontab 顶部显式声明JAVA_HOME/opt/jdk/jdk-xx、PATH/opt/jdk/jdk-xx/bin:/usr/bin:/bin更稳的是在脚本内部用绝对路径调用 java并在脚本开头自己 export 一遍让脚本自带环境不依赖调用它的人。第二种做法的好处是脚本可移植换机器、换调度方式都不用改配置。6.4 坑四临时文件和缓存串味java.io.tmpdir默认是/tmp多版本并存时一般不会冲突但构建缓存会。Maven 的~/.m2/repository、Gradle 的~/.gradle/caches是共享的用不同 JDK 编译同一个项目时缓存里的字节码版本可能不匹配表现是编译莫名其妙失败或者增量构建产出错误结果。遇到这种结论诡异的构建问题时先mvn clean或清理缓存目录再试能排除一大半玄学。如果长期要两个版本并行构建同一项目建议给每个版本指定独立的缓存目录export MAVEN_OPTS-Dmaven.repo.local/opt/m2repo/jdk-xx6.5 坑五删掉旧 JDK 之后才发现依赖它有人图省事装完新版就把旧版rm -rf了结果老服务起不来。请注意系统里可能还有其他东西依赖着那个路径尤其是在/usr/lib/jvm下被包管理器登记过的。真要卸载走包管理器命令让系统自己处理依赖关系或者至少先lsof/grep一圈确认没有残留引用。装第二个 JDK 的初衷就是共存不是替换把这句话记在脑子里能避免很多返工。7. 验证隔离是否真的干净四个连续检查配置写完之后不要急着交付按下面顺序过一遍全部通过才算真正隔离成功。第一步在目标用户下开一个全新的登录会话不是su而是完整重新登录或用bash -l模拟执行java -version和which java确认新版本生效且路径正确。第二步用启动老服务的那个账号登录同样执行一遍确认它仍然是老版本。这一步是整个隔离的验收核心——只验证新环境能用是不够的还必须验证老环境没被影响。第三步检查env | grep -i java的输出看有没有意料之外的变量残留比如某些应用自带的_JAVA_OPTIONS、JAVA_TOOL_OPTIONS这些变量的优先级高于命令行参数会悄悄改变 JVM 行为。第四步实际跑一次编译加运行的最小闭环echo public class T { public static void main(String[] a){ System.out.println(System.getProperty(java.version)); } } T.java javac T.java java T rm -f T.java T.class输出应该和javac -version一致。这一步能同时验证编译器、运行时和类路径三者是否指向同一套 JDK比单纯敲-version更有说服力。8. 我的几点实操体会装第二套 JDK 这件事本身技术含量不高真正花时间的永远是隔离得干不干净。我这些年的经验可以浓缩成几句实话。先查后装永远别跳过摸底。机器是别人装的、配置是前人写的你以为的系统默认可能早就被改过三层了。花五分钟把PATH、JAVA_HOME、alternatives、profile 文件全部看清楚能省掉后面几小时的困惑。能用用户级就别用系统级。影响面越小出问题的概率越低责任边界也越清楚。系统级配置看着一次配好全机器通用很诱人但只要机器上跑着老业务这个诱人的方案就是在给自己埋雷。路径一律写绝对路径。启动脚本、cron 任务、构建配置里凡涉及 JDK 路径的地方都用绝对路径不要指望当时的环境变量。环境变量是给交互式操作准备的自动化流程应该自带完整上下文。改配置之前先备份改完先验证再交付。备份命令上面给过了验证就按第 7 章那四步走。这套流程我执行了很多次虽然每次多花十分钟但从来没因为环境问题在半夜被叫起来过——这笔账怎么算都划算。最后补一个小技巧把版本信息和服务清单整理成一张表贴在运维记录里写清楚哪个服务用哪个 JDK、由哪个账号启动、配置文件在哪。这台机器一年后可能是别人接手那张表就是救命的东西。