
做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-jdksudo 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得先加第三方PPAsudo 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前面不然系统会在前面的目录里找到老版本的javaJAVA_HOME就算设对了也白搭。误区四还在配CLASSPATH。现在JDK 8以后根本不需要手动设CLASSPATH设了反而容易干扰依赖管理出现一堆“找不到类”的奇怪问题。规规矩矩只配JAVA_HOME和PATH就够了。3. 核心操作Linux下三种切换JDK版本的实用方法3.1 方法一修改~/.bashrc中的JAVA_HOME这个方法最直白适合个人开发机临时切。编辑~/.bashrc把当前要用的JDK地址写进JAVA_HOME同时更新PATHexport 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是17javac -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 jvecho 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 TemurinAdoptium的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版本java17.0.9-tem然后让SDKMAN自动生成并切换sdk env init sdk env进入项目目录时执行一次sdk env它马上读取.sdkmanrc并切换到对应JDK换到别的项目时再执行一次就行。加一层direnv之类的hook甚至可以做到“cd进目录自动切”彻底告别手动。我个人的工作流是项目A是老系统.sdkmanrc写java8.0.392-tem项目B是新服务写java17.0.9-tem。平时在两个目录间切换最多各敲一次sdk envJAVA_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固定服务端”这个组合上稳定、省心、可复现。也希望这篇教程能让你少走几步弯路毕竟配环境的时间留给写代码才是最香的。