1. 为什么需要在Linux上管理多个JDK版本
1.1 多版本并存的典型场景
先说一个我自己的经历。接手一个老项目时,它还在用JDK 8跑着Spring Boot 2.x,代码里一堆javax.*包,换到JDK 11就各种报错。而旁边的新项目已经上了JDK 17,用了不少Spring Boot 3的特性。装两个JDK?那是必然的。更头疼的是,还需要在两个项目之间来回切换开发,每次切项目都要重新配置环境变量,搞了两次就烦了。
这其实是很多开发者的真实处境。基于常见的开发环境经验,多JDK并存的需求主要来自这几种场景:
- 不同历史时期的项目并存,老项目锁死JDK 8/11,新项目用JDK 17/21。
- 测试某个依赖或框架在不同JDK版本下的兼容性,需要快速切换对比。
- 学习或复现别人的代码,对方的运行环境和你本机不一致。
- 使用一些特定工具时,工具本身对JDK版本有硬性要求。
也就是说,多JDK并存不是“要不要”的问题,而是“怎么管理”的问题。如果处理不好,轻则java -version对不上,重则项目起不来,甚至系统自带的工具都被你改崩了。
1.2 切换版本的核心思路
这里先讲清楚一个关键概念:Linux系统本身并不知道也不关心你安装了几个JDK,它只知道你给它指定的那个。你通过环境变量JAVA_HOME告诉系统JDK在哪里,通过PATH告诉系统去哪找java可执行文件。
所谓的“切换JDK版本”,本质上是同一件事的三种操作方式:
- 修改
JAVA_HOME指向不同的JDK目录。 - 修改
PATH中$JAVA_HOME/bin的顺序或指向。 - 让
java、javac、jps等命令通过符号链接指向当前需要的版本。
只要这三件事做对了,版本就切换成功了。很多初学者搞不清楚的是:为什么环境变量改了,java -version还是老的?因为终端会话启动时已经读取了环境变量,你改完文件后没有执行source,或者当前shell的PATH缓存还没刷新。
明白了这个底层逻辑,后面的所有方案就都顺理成章了。本质上就是在“改环境变量”和“改符号链接”这两条路上做文章。
2. 安装JDK的几种方式与版本选择
2.1 选择哪个JDK发行版
说到JDK,很多人脑子里蹦出来的第一个词是“Oracle JDK”。但说实话,日常开发和测试我更推荐OpenJDK或Eclipse Temurin(Adoptium项目)。理由很实际:免费、开源、持续更新、和主流构建工具兼容性好。Oracle JDK在17之前需要商业许可,而且从17开始虽然许可放宽了,但很多团队还是规避风险选了OpenJDK。
如果你在用的是国内云服务器,还可以考虑几个方便下载的镜像源。比如清华TUNA、阿里云镜像、华为云镜像,下载速度快且稳定。对于生产环境,建议紧跟LTS版本,像JDK 8、11、17、21都是当前主流的LTS版本。
具体到发行版选择,给大家一个参考:
| 发行版 | 提供商 | 适用场景 | 备注 |
|---|---|---|---|
| OpenJDK | Oracle/社区 | 日常开发、生产 | 最普遍,兼容性好 |
| Temurin | Adoptium | 生产、容器环境 | 提供完整的JRE/JDK构建包 |
| Zulu | Azul | 嵌入式、特殊平台 | 支持ARM、多架构 |
| Oracle JDK | Oracle | 商业需求、官方认证 | 注意许可条款 |
| Dragonwell | 阿里 | 超大规模生产 | 有针对性优化 |
我平时用Temurin最多,因为它在Docker镜像里基本是标配,而且Adoptium官网下载页的tar.gz包都是免安装的,解压就能用,很适合本文的场景。
2.2 手动解压安装(tar.gz)的方式
这种方式最干净,也最好控制,推荐大家优先使用。步骤很简单:
# 1. 创建统一的目录,把JDK集中放在一起 sudo mkdir -p /usr/local/java cd /usr/local/java # 2. 下载JDK(以Temurin 17为例,假设已经下载了tar.gz包) ls jdk-17*.tar.gz # 3. 解压 sudo tar -xzf jdk-17*.tar.gz # 4. 重命名,方便区分版本(关键步骤,别偷懒) sudo mv jdk-17.0.12 jdk-17 # 5. 在另一个目录解压JDK 8 sudo tar -xzf jdk-8*.tar.gz sudo mv jdk-8u412 jdk-8之所以放在/usr/local/java下,是因为/usr/local是Linux标准的第三方软件安装目录,权限和目录结构的约定都比较清晰。当然,如果你没有sudo权限,放在自己的home目录也可以,比如~/java/jdk-17,后续配置方式完全一样。
重命名这个步骤看起来简单,但特别关键。如果不重命名,你会面对/usr/local/java/jdk-17.0.12和/usr/local/java/jdk-1.8.0_412这种结构,后续写脚本、改环境变量时容易手误。统一命名成jdk-17、jdk-8这种简洁的名称,管理成本低得多。
2.3 通过包管理器安装(apt/yum)的限制
很多初学者会直接用包管理器安装:
sudo apt update sudo apt install openjdk-17-jdk这样确实省事,JDK会被自动安装到/usr/lib/jvm/目录下,并且包管理器会自动配置好默认的java命令。但问题在于,当你装完JDK 8再装JDK 17,系统只会默认把其中一个设为默认版本,切换需要靠update-alternatives来操作。
包管理器安装的好处是自动处理依赖、环境变量配置比较规范;坏处是你对JDK目录的控制力弱了一些,尤其是如果你想在同一个目录下管理多个自解压的JDK,包管理器的方式就显得有些笨重。
我的建议是:如果只是装一个JDK,随便哪种方式都行。如果明确有多版本需求,直接手动解压,后续切换的自由度最大。当然,包管理器安装的JDK也可以通过update-alternatives --config java切换,这个我们后面会详细讲。
3. 配置环境变量的核心原理
3.1 JAVA_HOME和PATH到底是怎么工作的
先打一个比方。把系统比作一栋大楼的物业管理中心,你在楼里上班(运行Java程序)。管理中心怎么知道你在哪个办公室?它需要一张通讯录,上面写着“java程序的办公室在3楼”。JAVA_HOME就是这张通讯录里的条目,指向的是你的“办公室”(JDK安装目录)。
而PATH是另一张更有权力的通讯录,它告诉系统:当你输入java命令时,要去哪些目录依次寻找这个命令。如果JAVA_HOME指向JDK 17,但PATH里/usr/local/java/jdk-8/bin排在前面,系统找到的还是JDK 8的java。
所以,配置环境变量的本质是确保两件事同时正确:
JAVA_HOME指向你当前想用的JDK目录。PATH里$JAVA_HOME/bin排在所有其他JDK目录之前。
到这里大家应该明白了,切换版本并不一定非要改JAVA_HOME本身,你只要把PATH里的查找顺序调整好,效果是一样的。
3.2 ~/.bashrc、~/.profile、/etc/profile到底该改哪个
这个问题几乎每个Linux新手都会踩坑。解释一下几个文件的区别:
/etc/profile:系统级的,对所有用户生效,登录时加载。需要sudo权限才能改。~/.profile:用户级的,只对当前用户生效,登录时加载(但如果你用的是bash非登录shell,它可能不加载)。~/.bashrc:用户级的,只对当前用户生效,每次打开一个新的bash终端都会加载。这是平时改环境变量最常用的文件。
如果只是给自己用,改~/.bashrc就够了。改了~/.bashrc后,运行source ~/.bashrc立即生效,或者直接重开一个终端。注意不要同时把环境变量写到/etc/profile和~/.bashrc里,两个地方都定义时,优先级容易出现混淆。
我自己习惯的做法是:只改~/.bashrc,其他文件不碰。因为我并不希望所有用户(包括root)都强制使用某个特定的JDK版本。如果你确实需要系统级默认版本,再考虑/etc/profile或/etc/environment。
3.3 一份干净的环境变量配置模板
下面是我常用的配置模板,放在~/.bashrc底部:
# JDK切换配置 export JAVA_HOME=/usr/local/java/jdk-17 export PATH=$JAVA_HOME/bin:$PATH需要注意一个细节:PATH=$JAVA_HOME/bin:$PATH把JDK的bin目录放在最前面,然后接原有的PATH。这样就保证了你输入java时,系统优先找到这个路径下的版本,而不是其他目录里的旧版本。
配置完后需要验证:
source ~/.bashrc echo $JAVA_HOME java -version which java如果输出符合预期,说明JDK已经生效。如果which java显示的是/usr/bin/java而不是/usr/local/java/jdk-17/bin/java,说明有其他配置干扰,需要用type -a java查看所有找到的路径。
4. 多版本切换的几种方案与实操
4.1 方案一:手动修改环境变量(最简单但最笨)
这个方案最容易理解,每次切换就是改~/.bashrc中的JAVA_HOME,然后source一下。
sed -i 's|export JAVA_HOME=.*|export JAVA_HOME=/usr/local/java/jdk-8|' ~/.bashrc source ~/.bashrc java -version切换回17也是同理。但这个方法有两个明显的缺点:一是每次要用sed或手动打开文件编辑,操作繁琐;二是容易改错,比如某个字符输错了,整个环境变量就崩了。
所以,这个方法只建议在”我只需要偶尔切一次“或者“临时测试某个版本”时用。日常频繁切换的话,你会被自己烦死的。
4.2 方案二:自定义shell函数(推荐日常切换)
既然手动改文件太麻烦,那就写个函数,把切换动作固化下来。在~/.bashrc底部加上:
# JDK切换函数 function switchjdk() { case "$1" in 8) export JAVA_HOME=/usr/local/java/jdk-8 ;; 11) export JAVA_HOME=/usr/local/java/jdk-11 ;; 17) export JAVA_HOME=/usr/local/java/jdk-17 ;; *) echo "Usage: switchjdk {8|11|17}" return 1 ;; esac export PATH=$JAVA_HOME/bin:$PATH echo "Switched to JDK $1" java -version }注意,这里export PATH=$JAVA_HOME/bin:$PATH有一个坑:每次执行函数,都会把$JAVA_HOME/bin再次插到PATH最前面。如果之前PATH里已经有了旧的JDK路径,你会看到PATH不断变长,从而引起混乱。更严谨的写法是先把PATH里旧的JDK路径清理掉,再重新设置。但实际测试下来,只要$JAVA_HOME/bin始终排在最前面,旧的路径即便存在也不会被优先命中,所以简单场景下这个坑可以不纠结。
用法很简单:
switchjdk 8切换后用java -version验证。这个方法最大的优势是快,而且不用改文件、不用重启终端,非常适合日常开发。
如果想再进一步加固,可以加一个当前版本的提示符显示,比如修改PS1:
export PS1="\u@\h [JDK:\$(basename $JAVA_HOME)] \w$ "这样每次输入命令时都会看到当前用的哪个JDK,避免切错版本导致一些诡异的问题。
4.3 方案三:基于符号链接的切换(适合系统级统一控制)
如果你需要多个用户共用一套JDK,或者需要让java命令在系统级别统一指向某个版本,可以用符号链接的方式。
首先把系统默认的java命令指向某个链接:
sudo ln -sf /usr/local/java/jdk-17/bin/java /usr/bin/java sudo ln -sf /usr/local/java/jdk-17/bin/javac /usr/bin/javac切换时,只需要重新替换链接:
sudo ln -sf /usr/local/java/jdk-8/bin/java /usr/bin/java sudo ln -sf /usr/local/java/jdk-8/bin/javac /usr/bin/javac这种方式的好处是即使某些脚本写死了/usr/bin/java,也能生效。缺点是你必须使用sudo,每次切换要敲两行命令,而且对JAVA_HOME没有直接帮助——很多Java工具(比如Maven、Gradle)是看JAVA_HOME的。
所以,我的经验是:符号链接用于解决“某些脚本或者systemd服务直接调/usr/bin/java”的场景,而环境变量用于解决“Shell会话中开发工具”的场景。两个配合起来才完整:先改JAVA_HOME,再改符号链接。这也是为什么官方推荐用update-alternatives,它能同时管理这两件事。
4.4 方案四:update-alternatives 系统级切换
Debian/Ubuntu系统其实自带了一个管理命令工具,就是update-alternatives。它本质上是一个符号链接的管理器,但可以做得更规范。
首先要注册我们手动解压的JDK:
sudo update-alternatives --install /usr/bin/java java /usr/local/java/jdk-17/bin/java 1 sudo update-alternatives --install /usr/bin/java java /usr/local/java/jdk-8/bin/java 2 sudo update-alternatives --install /usr/bin/javac javac /usr/local/java/jdk-17/bin/javac 1 sudo update-alternatives --install /usr/bin/javac javac /usr/local/java/jdk-8/bin/javac 2最后那个数字是优先级,数字越大优先级越高。如果想给某个JDK更高的默认优先级,给它一个更大的数字就行。
切换时执行:
sudo update-alternatives --config java系统会把所有已注册的JDK列出来,让你输入序号选择。选了之后,/usr/bin/java会自动指向对应的JDK路径。
这个方法适合系统级的版本管理,尤其是服务器上有多用户时。但它仍然不能直接解决JAVA_HOME的切换问题——update-alternatives管理的是命令链接,不是环境变量。所以用这个方法时,建议再加上JAVA_HOME的手动设置,或者配合前面的switchjdk函数一起用。
4.5 方案五:SDKMAN 现代化的多版本管理工具
如果你觉得上面的方式都太“手工”,那必须试试SDKMAN。它跟nvm(Node Version Manager)、pyenv类似,是一个针对JVM生态的版本管理工具。SDKMAN不仅能管理JDK,还能管理Maven、Gradle、Spring Boot CLI等,非常强大。
安装SDKMAN:
curl -s "https://get.sdkman.io" | bash source ~/.bashrc安装JDK(以17为例):
sdk install java 17.0.12-tem查看当前可用版本:
sdk list java切换版本:
sdk use java 17.0.12-temSDKMAN会在你的用户目录下创建.sdkman/candidates/java/目录,所有版本都放在那里,然后用符号链接current指向当前版本。它改的是你Shell会话里的JAVA_HOME和PATH,并不会动系统级的/usr/bin/java。
SDKMAN还支持项目级配置:在项目根目录创建.sdkmanrc文件,内容写上java=17.0.12-tem,然后执行sdk env init,以后每次进入这个目录时执行sdk env activate,它会自动切换到该文件规定的版本。如果你经常同时开发多个版本的项目,这个功能会让你感觉非常舒服。
不过需要提醒一点:SDKMAN对网络有要求,官方源在下载JDK时可能比较慢,需要配置镜像或更换下载源。如果你在国内,建议提前设置SDKMAN_CANDIDATES_API指向国内镜像源,否则安装过程可能卡在下载环节。
5. 版本切换过程中的常见问题与排查技巧
5.1 问题速查表
| 症状 | 可能原因 | 解决思路 |
|---|---|---|
| java -version 版本不对 | PATH顺序不对或未source | 查看which java、type -a java,调整PATH |
| JAVA_HOME 一直为空 | 配置写在错误的文件里 | 检查 ~/.bashrc、~/.profile,确认source过 |
| java和javac版本不一致 | 两个命令分别指向不同JDK | 用which javac定位,统一符号链接或环境变量 |
| Maven/Gradle仍用旧版本 | Maven读的不是JAVA_HOME | 确认mvn -version显示的环境变量 |
| 切换后IDEA等工具失效 | IDE缓存了旧路径 | 在IDE里重设JDK路径或清除缓存 |
| 脚本中调/usr/bin/java | 符号链接未更新 | 用 update-alternatives 或手动 ln -sf |
这个表格基本覆盖了我这些年踩过的绝大部分坑。但这里展开讲几个最典型的。
5.2 排查实例:java和javac版本不一致
有一次我在一台机器上装好了JDK 17,java -version也显示17,但运行javac -version愣是显示8。找了好久才发现,java来自/usr/local/java/jdk-17/bin/java,而javac来自/usr/bin/javac——之前系统里用apt装的OpenJDK 8留下了这个命令,它没被PATH的JDK 17覆盖。
这种问题的排查路径是:
which java which javac如果两个命令不在同一个目录,说明它们分别来自不同的安装方式。解决办法是把/usr/bin/javac的符号链接重新指到目标JDK上:
sudo ln -sf /usr/local/java/jdk-17/bin/javac /usr/bin/javac或者干脆把所有相关命令都通过update-alternatives注册一遍,一劳永逸。
5.3 排查实例:切换JDK后Maven仍然用旧版本
Maven、Gradle这类构建工具启动时,会去找JAVA_HOME来决定自身运行在哪个JDK上。如果你用switchjdk改变了Shell环境,但Maven还是旧版本,优先检查:
mvn -version如果输出里显示的Java版本不对,查看输出中的Java home路径。Maven在读取环境变量时,会先看系统的JAVA_HOME,再看自己bin/mvn脚本里的默认配置。某些情况下你改了~/.bashrc,但Maven脚本是从另外的位置读取的。
解决办法:在启动Maven前显式指定:
export JAVA_HOME=/usr/local/java/jdk-17 export PATH=$JAVA_HOME/bin:$PATH mvn -version如果还是不行,检查/etc/environment文件,看是否有全局的JAVA_HOME覆盖了你的配置。
5.4 实战心得:如何避免切换版本时把系统搞崩
最后分享几个经验教训。
不要轻易动/usr/bin/java。很多Linux系统工具(比如Hadoop、Elasticsearch)会依赖这个路径下的Java。如果你用符号链接把它指向了一个新版本,但这些工具脚本并没有适配新版本,它们可能直接报错。所以,切换/usr/bin/java时,务必确认所有常用的系统服务都是兼容的。
用自解压JDK时,给每个JDK单独建一个目录,不要把两个版本混放在同一个目录里。哪天你升级JDK,直接把整个旧目录删掉换新的,干净利落。
不要频繁地在~/.bashrc和/etc/profile之间来回切换配置。坚持只用其中一种方式,否则配置会非常混乱,排查问题时也会非常痛苦。
还有一个容易被忽视的细节:有些项目用.mvn/jvm.config或 Gradle 的gradle.properties指定了项目级JDK版本,这些配置优先级高于环境变量。也就是说,即使你终端里切到了JDK 17,项目可能仍然在JDK 8上运行。遇到这种情况,不是环境变量的问题,而是项目自身的构建配置需要改动。
5.5 防御性检查技巧
最后养一个好习惯:每次切换完JDK,不要只看java -version,建议再跑一条测试命令确认整个工具链都是通的:
java -version javac -version echo $JAVA_HOME which java which javac mvn -version 2>/dev/null || echo "Maven not found"把这五条命令绑成一个别名或函数,比如jdkcheck,每次切换后直接调用,一目了然:
alias jdkcheck='java -version; javac -version; echo "JAVA_HOME=$JAVA_HOME"; which java; which javac'这个习惯帮我省了不知道多少排查时间。环境变量毕竟是个“隐性问题”,多一步检查,比等问题浮现再去翻找要高效得多。
6. 多JDK版本管理的最佳实践总结
聊到这里,整套多JDK管理的方案已经全部铺开了。如果让我给一个明确的推荐组合,我会这样选:
在个人开发机上,优先使用手动解压 + shell函数(方案二),配合SDKMAN作为备选工具。在多人共享的服务器上,优先使用/usr/local/java统一存放 +update-alternatives管理命令链接,同时规定所有用户的JAVA_HOME指向的路径。
从实际维护经验来看,最稳妥、最不容易出问题的做法是:JDK固定在/usr/local/java/下,命令链接用update-alternatives控制,环境变量用统一的一段脚本管理,并且每个用户都会加载同一个切换函数脚本。这样团队里所有人用的是同一套JDK管理规范,不会出现“你机器上能跑,我机器上跑不了”这种版本差异问题。
其实多JDK切换这件事,说难不难,但说简单也不简单,关键还是把底层逻辑搞清楚:JAVA_HOME负责告诉工具链“我要用哪个JDK”,PATH负责告诉Shell“我要去哪儿找命令”,update-alternatives负责管理系统级符号链接。三者各司其职,配合起来就能做到随意切换且不互相干扰。
根据我个人的经验,真正卡住大家的往往不是安装步骤本身,而是环境变量生效范围和优先级的问题。把一个文件里配置对了不算结束,还要验证它是否在所有需要读取的进程(Shell、构建工具、IDE)里生效。多花两分钟做防御性检查,后面就能省下两小时的查错时间。