做Java开发的朋友迟早会撞上一件事:昨天还在用JDK 8写老项目,今天新接的Spring Boot 3工程非得要JDK 17,手头还有一个个微服务要用JDK 11,几套环境在Linux服务器上并存,切换JDK版本就成了绕不过去的日常。这篇教程就是来把手教你怎么在Linux上安装多版本JDK、随时切换版本,同时把环境变量、update-alternatives、SDKMAN这些工具的原理和坑都讲清楚。不管你是刚入门的小白,还是被环境搞到头秃的运维/开发,照着做就能搞定。
1. 为什么要切换JDK版本?先搞清楚需求再动手
1.1 不同项目对JDK版本的硬性要求
先说需求。很多人以为JDK版本越高越好,实际上项目用什么版本,往往是被框架、依赖和上线环境逼出来的,不是你想升就升。
老一些的Spring项目、Hibernate 3.x、Apache Struts这些,主流还是跑在JDK 8上。哪怕JDK 8已经停止免费商用更新那么久了,Java 8依然是很多老系统的“钉子户”,因为它稳定、认证多、踩坑资料也全。我自己见过不少核心交易系统,生产环境清一色JDK 8,谁提升级谁背锅。
而新版技术栈就很不一样了。Spring Boot 3和Spring Framework 6都明确要求JDK 17起步,想用Spring Boot 3的新特性、GraalVM原生镜像,就别想着用JDK 8混过去。Java 11是很多现代化项目的基线,微服务架构里用JDK 11跑还是很常见的。Java 21是新的LTS版本,虚拟线程正式可用,新项目选型也越来越多人往上靠。
除了项目本身,依赖包也会卡你。比如我遇到过fastjson老版本在JDK 17下反射直接报错,jjwt某些版本在JDK 11以上运行时也有一堆坑。Maven的maven.compiler.source/target如果设了低于当前JDK的版本,表面看着能编译,实际运行还会碰到UnsupportedClassVersionError。所以切换JDK不是单纯图新鲜,是实打实的项目兼容性需求。
1.2 切换JDK的三种典型思路对比
搞清楚了“为什么”,再看“怎么办”。Linux下切换JDK版本,市面上常见思路其实就三种,没有绝对的好坏,只有适不适合你的场景。
| 方案 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
修改JAVA_HOME环境变量 | 单用户、临时切换 | 简单直接,几行命令搞定 | 全局影响大,切换后需重新加载shell;容易改乱 |
update-alternatives | 系统级、多用户共用 | 官方机制,统一管理符号链接 | 需要注册每个版本,操作步骤多一点 |
| SDKMAN | 开发者本机、频繁切换 | 支持项目级自动切换,安装管理一条龙 | 多了个依赖工具;部分环境不能直接用 |
从我的经验看,个人开发机用SDKMAN最舒服,因为可以按目录自动切版本;正式服务器上反而用update-alternatives更稳,因为别人登录机器也能保持一致;临时应急就直接改JAVA_HOME,越近越好。
2. 准备工作:安装多个JDK版本的正确姿势
2.1 从发行版仓库安装 OpenJDK
不管后面用哪种方法切换,前提都是机器上有多个JDK版本。最简单的方式是用Linux发行版自带的包管理工具装OpenJDK。
Ubuntu/Debian系,包名一般是openjdk-8-jdk、openjdk-11-jdk、openjdk-17-jdk:
sudo apt update sudo apt install -y openjdk-8-jdk openjdk-11-jdk openjdk-17-jdkCentOS/RHEL/Fedora系,包名风格不太一样,是java-1.8.0-openjdk-devel这种:
sudo yum install -y java-1.8.0-openjdk-devel java-11-openjdk-devel java-17-openjdk-devel有个坑我得提前说:Ubuntu 20.04及之后的官方源里其实已经不带openjdk-8-jdk了,源里只有11和17。想用JDK 8,得先加第三方PPA:
sudo add-apt-repository ppa:openjdk-r/ppa sudo apt update sudo apt install -y openjdk-8-jdk装完后,可执行文件一般在/usr/lib/jvm下,每个JDK一个独立目录,比如/usr/lib/jvm/java-8-openjdk-amd64。这个路径后面配环境变量要用,心里先有个数。
2.2 手动解压官方JDK到自定义目录
仓库里的版本不一定全,比如你公司非要Oracle JDK 17,或者要让开发机和生产的补丁版本完全一致,那就最好手动下载官方tar.gz包解压安装。
以JDK 17为例,先把包下载到某个目录,然后统一放到/opt/jdk或/usr/lib/jvm这类标准位置:
sudo mkdir -p /opt/jdk sudo tar -zxvf jdk-17_linux-x64_bin.tar.gz -C /opt/jdk sudo mv /opt/jdk/jdk-17.0.9 /opt/jdk/jdk-17这里我习惯把目录改成不带小版本号的jdk-17,方便后面JAVA_HOME的写法统一。再比如JDK 8:
sudo tar -zxvf jdk-8u392-linux-x64.tar.gz -C /opt/jdk sudo mv /opt/jdk/jdk1.8.0_392 /opt/jdk/jdk-8手动解压的好处是可控性强,想留几个版本就留几个,服务商自带的包管理不会干扰。缺点是更新要自己管,不像仓库版能蹭自动升级。
2.3 设置JDK环境变量的常见误区
环境变量配置失败,是这主题下面搜得最多的关键词,我先把几个反复踩的误区摆出来。
误区一:把配置写进/etc/profile却忘了让当前shell重新加载。每次改完都执行一句source /etc/profile,或者干脆重新登录,不然当前终端不会知道修改。
误区二:搞混.bashrc和.bash_profile。~/.bashrc在每次打开新的交互式bash终端时都会执行,~/.bash_profile只在登录shell时执行一次。个人环境建议写进~/.bashrc,因为你的IDE终端、远程工具连接后基本都是交互式非登录shell,写错地方会导致JAVA_HOME没生效。
误区三:PATH顺序写反。必须把$JAVA_HOME/bin放在PATH前面,不然系统会在前面的目录里找到老版本的java,JAVA_HOME就算设对了也白搭。
误区四:还在配CLASSPATH。现在JDK 8以后根本不需要手动设CLASSPATH,设了反而容易干扰依赖管理,出现一堆“找不到类”的奇怪问题。规规矩矩只配JAVA_HOME和PATH就够了。
3. 核心操作:Linux下三种切换JDK版本的实用方法
3.1 方法一:修改~/.bashrc中的JAVA_HOME
这个方法最直白,适合个人开发机临时切。编辑~/.bashrc,把当前要用的JDK地址写进JAVA_HOME,同时更新PATH:
export JAVA_HOME=/opt/jdk/jdk-17 export PATH=$JAVA_HOME/bin:$PATH保存后让配置生效:
source ~/.bashrc接下来验证:
echo $JAVA_HOME java -version想换JDK 8,就把JAVA_HOME改成/opt/jdk/jdk-8,再source ~/.bashrc。原理不复杂:java命令是Linux通过PATH环境变量找到的,你把JAVA_HOME/bin排在最前面,它自然优先用这个JDK。
但这个方法有个明显毛病:JAVA_HOME是所有终端共用的,一改全改。如果你同时开俩终端,一个想用8一个想用17,改完第二个终端也会跟着变。所以我自己只把它当应急方案,日常不这么搞。
3.2 方法二:用update-alternatives管理
update-alternatives是Debian/Ubuntu系统自带的机制,CentOS 7以后也有类似功能(CentOS是alternatives,使用习惯差不多)。它的核心思路是:在/usr/bin下建一个统一的软链接java,再由它指向/etc/alternatives/java,最终指向你选中的那个真实JDK。
先把想管理的JDK注册进alternatives,这一步每个版本都要执行一次。比如:
sudo update-alternatives --install /usr/bin/java java /opt/jdk/jdk-17/bin/java 1700 sudo update-alternatives --install /usr/bin/java java /opt/jdk/jdk-8/bin/java 1800后面的1700、1800是优先级数字,数字越大优先级越高。但要注意,这只影响安装时默认选谁,手动切换时优先级不起决定性作用。
如果JDK是从仓库装的,通常已经自动注册好了,不用手动加。然后列出可选版本并切换:
sudo update-alternatives --config java命令会列出所有已注册的JDK,输入编号回车,再验证:
java -version千万别只注册java就完事。很多工具链还会调用javac、jar,它们同样是独立的alternatives项。最好一起注册:
sudo update-alternatives --install /usr/bin/javac javac /opt/jdk/jdk-17/bin/javac 1700 sudo update-alternatives --install /usr/bin/jar jar /opt/jdk/jdk-17/bin/jar 1700不然你java -version是17,javac -version却是8,编译出来的class字节码版本对不上,运行时就报UnsupportedClassVersionError。
这个方法的好处是系统级生效,不管谁登录机器,/usr/bin/java指向哪就是哪。它适合服务器、多人共用环境。缺点就是注册命令有点繁琐,而且要记得把相关命令都处理全。
3.3 方法三:写一个jdk切换函数写
频繁在两个版本之间横跳的时候,我最推荐写个shell函数放进~/.bashrc里。这样每次切换就敲一个参数,不用去翻路径。
在~/.bashrc末尾加这段:
function jdk() { case "$1" in 8) export JAVA_HOME=/opt/jdk/jdk-8 ;; 11) export JAVA_HOME=/opt/jdk/jdk-11 ;; 17) export JAVA_HOME=/opt/jdk/jdk-17 ;; 21) export JAVA_HOME=/opt/jdk/jdk-21 ;; *) echo "Usage: jdk {8|11|17|21}" return 1 ;; esac export PATH=$JAVA_HOME/bin:$PATH java -version }保存后source ~/.bashrc,以后切换只需要:
jdk 17 jdk 8函数内部做的事情和方法一完全一样,只是把重复劳动封装起来。我在这基础上还会加一个jdk list参数,把机器上有哪些版本和当前JAVA_HOME打印出来,用的时候心里有数。
要注意:函数里return 1是用在函数里的,别写exit 1,否则会把整个终端都关掉。我一开始就犯过这低级错误,切错版本直接给终端踢出去了,活没save,人差点也气得删库。
4. 实测验证与常见问题排查
4.1 怎么确认当前生效的JDK到底是哪个
切换完之后别急着写代码,先用一组命令确认到底哪个JDK在生效。这个组合拳基本是固定的:
echo $JAVA_HOME which java java -version readlink -f $(which java)逐一解释:
echo $JAVA_HOME看环境变量值,能确认你这是不是我们设置过的路径。which java从PATH中找出第一个叫java的可执行文件位置。如果输出是/usr/bin/java,说明你直接调用的其实是系统的符号链接,而不是$JAVA_HOME/bin/java。java -version是最终结论,JVM民调报告自己版本号。readlink -f $(which java)会一路追踪符号链接,最终落到真实JDK的二进制文件。在CentOS上尤其有用,因为你看到/usr/bin/java的时候,根本不知道它指向哪。
我习惯把这几条写成一行别名:
alias jv='echo "JAVA_HOME=$JAVA_HOME"; which java; java -version; readlink -f $(which java)'以后敲jv就能一次看全。
4.2 切换后java -version没变化?排查思路
这是后台留言里问得最多的情况:“明明改了JAVA_HOME,也source了,java -version还是老版本。”遇到这种情况,按顺序排查。
第一步,确认你的JAVA_HOME是不是真的设置成功了。执行echo $JAVA_HOME,如果为空或者显示旧路径,说明配置没生效,回头检查是不是写错文件、没source。
第二步,看which java指向哪。如果它返回/usr/bin/java,那说明你在命令行敲的java走的是系统符号链接,不是$JAVA_HOME/bin/java。这种情况下,即使JAVA_HOME改成新版本,/usr/bin/java仍然拿着旧版本的软链接不放。解决办法要么用update-alternatives --config java切换系统全局链接,要么把JAVA_HOME/bin加进PATH并保证它在/usr/bin前面。
第三步,检查PATH顺序。执行echo $PATH看/opt/jdk/jdk-17/bin是不是排在第一位。如果发现JDK 8的路径在前面,那就是PATH顺序没弄对。
第四步,想想是不是新开的终端。有些配置只对当前终端session生效,新开一个终端不重新加载~/.bashrc的话,它还是会按系统旧配置来。
还有一个隐蔽情况:你是通过IDE或服务管理工具(systemd、docker等)启动的Java进程。这类进程不会因为你改shell环境变量就跟着变,必须重启IDE或服务才行。
4.3 符号链接、软链接与PATH顺序的坑
围绕/usr/bin/java的链接链是这样的:
/usr/bin/java -> /etc/alternatives/java -> /opt/jdk/jdk-8/bin/java这条链在update-alternatives体系下很典型。好处是系统所有用户敲java时都能统一指向一个版本;坏处是,你用update-alternatives --config java切换时,改变的只是这条链的末端,而JAVA_HOME环境变量完全没被动过。
所以最精分的坑出现了:命令行java -version是17,但Maven、Gradle、IDEA甚至Tomcat启动脚本却在用旧的JAVA_HOME,因为它们优先读环境变量。我曾在服务器上排查一个Java进程内存配置半天,最后发现它用的是/usr/lib/jvm里的老版本JDK,和我/etc/profile里写的JAVA_HOME根本不是一回事。
如果你同时用update-alternatives管理/usr/bin/java、又通过配置文件设了JAVA_HOME,一定要做到“两者指向同一个版本”。最省事的做法是:在~/.bashrc里显式把JAVA_HOME设置成readlink -f $(which java)的结果,让环境变量跟随系统链接,这样就不会左右打架:
export JAVA_HOME=$(dirname $(dirname $(readlink -f $(which java))))再配合update-alternatives切换,一次性让JAVA_HOME、PATH、/usr/bin/java全部指向同一版本。实测下来很稳。
5. 进阶技巧:用SDKMAN管理JDK版本
5.1 SDKMAN的安装与基本用法
如果追求“自动化”,我强烈推荐SDKMAN。它本来是管SDK的,但管理JDK版本是一绝,尤其适合个人开发机。安装只要一行:
curl -s "https://get.sdkman.io" | bash装完激活:
source ~/.sdkman/bin/sdkman-init.sh然后查看可用Java版本:
sdk list java这个列表会按厂商、版本号、标识列出来,比如17.0.9-tem就是Eclipse Temurin(Adoptium)的JDK 17。安装指定版本:
sdk install java 17.0.9-tem同时装8和21也行。切换当前shell版本:
sdk use java 8.0.392-tem设置默认版本:
sdk default java 17.0.9-temSDKMAN会自动管理JAVA_HOME和PATH,不会象手改环境变量那样留下残留,还能查当前版本列表:
sdk current java它的本质是在~/.sdkman/candidates/java路径下装多个JDK,并通过软链接切换。和前面讲的机制是相通的,只是做了很好的封装。
5.2 项目级自动切换JDK的实践
SDKMAN最打动我的一点是.sdkmanrc文件。你可以在项目根目录放一个文件,写上需要的Java版本:
java=17.0.9-tem然后让SDKMAN自动生成并切换:
sdk env init sdk env进入项目目录时执行一次sdk env,它马上读取.sdkmanrc并切换到对应JDK,换到别的项目时再执行一次就行。加一层direnv之类的hook甚至可以做到“cd进目录自动切”,彻底告别手动。
我个人的工作流是:项目A是老系统,.sdkmanrc写java=8.0.392-tem;项目B是新服务,写java=17.0.9-tem。平时在两个目录间切换,最多各敲一次sdk env,JAVA_HOME永远跟项目走,基本不冲突。
要提醒的是,.sdkmanrc建议提交进Git仓库,这样团队其他成员拉代码后也能用SDKMAN自动切换到同一版本,减少“我本机是好的啊”这类扯皮。
5.3 我的组合拳:SDKMAN + update-alternatives
日常开发我用SDKMAN,但一旦要把服务部署到服务器,我会老老实实回到update-alternatives把JDK固定好。原因很简单:服务器上很少有~/.sdkman这种个人工具,系统里可能有好几个用户要跑不同任务,大家认的还是/usr/bin/java这个统一入口。
我的建议是:
- 本机开发:SDKMAN管一切,项目级自动切换,省心。
- 服务器部署:解压官方JDK到
/opt/jdk,注册进update-alternatives,让系统级命令稳定指向一个版本。 - 应急排障:直接改
JAVA_HOME,怎么简单怎么来,别纠结优雅。
6. 最后再分享一个实用小技巧
既然看到这里了,我补一个自己用了很久的小技巧:写脚本或开终端的时候,不要只依赖java -version,把$JAVA_HOME也打出来。很多诡异问题不是JDK本身有问题,而是你用的工具链没走同一个JAVA_HOME。
比如我用的一键切换函数就是这么设计的:
function jdk() { case "$1" in 8) export JAVA_HOME=/opt/jdk/jdk-8 ;; 17) export JAVA_HOME=/opt/jdk/jdk-17 ;; *) echo "Usage: jdk {8|17}"; return 1 ;; esac export PATH=$JAVA_HOME/bin:$PATH echo "Now using: $JAVA_HOME" java -version }每次切换完,它马上告诉我当前JAVA_HOME和版本,眼睛一扫就知道对不对。
另一个心得是:改环境变量之前,一定先把当前配置备份一下。比如把~/.bashrc复制一份成~/.bashrc.bak,出问题了随时还原。听起来土,但真的能救命。尤其是你在服务器上配环境变量,配错了连普通命令都可能受影响,有备份就不至于手忙脚乱。
最后,如果你也在Linux下被多版本JDK折磨过,不妨按上面几种方法挑一个顺手的试试。多数人最后都会固定在“SDKMAN管理本机 + update-alternatives固定服务端”这个组合上,稳定、省心、可复现。也希望这篇教程能让你少走几步弯路,毕竟配环境的时间,留给写代码才是最香的。