
搞Java开发的人谁电脑里没装过两三个JDK版本老项目要Java 8新项目已经升到Java 17甚至21偶尔还得试一下预览版本的新特性。SDKMAN就是这样一个命令行工具专门解决“装JDK、换JDK、删JDK”这一整套管理问题支持在同一个系统里并存多个JDK版本并且随时切换。不管你是刚入门的新手还是被多版本折腾过好几次的开发者这篇文章能帮你把整套流程理顺。我会直接告诉你怎么装、怎么用、怎么切顺便把我踩过的坑也一起列出来。1. 为什么你的电脑需要装多个JDK版本1.1 日常开发里的版本分裂场景很多人的第一反应是JDK装一个最新的不就行了吗真不行。实际项目里版本分裂的情况太常见了。比如有些老业务系统用的是Java 8那一套生态依赖库里有些组件在JDK 8之后就不再维护了强行用新版本跑起来会出现兼容性问题。而新起的微服务项目一般会考虑Java 17或者Java 21带来的新特性和性能改进。两边同事协作时你手里如果只有一个最新的JDK切换项目时就会频繁碰到编译不过、运行报错、依赖包反射调用失败之类的问题。还有一类场景平时不太容易想到你在本地跑一些中间件或者脚本工具时它们内置的要求往往比项目本身更严格。有些工具只认某个特定版本的JVM版本高了或者低了启动脚本的行为都不一样。这种时候多版本共存几乎是唯一省心的方案。1.2 手动管理JDK的痛点没有工具之前我见过不少同事是这样的从官网下载压缩包解压到某个目录再打开系统环境变量设置界面手动加JAVA_HOME再把$JAVA_HOME/bin拼到PATH里。这流程听起来不难真正烦的是切换。今天切到Java 8明天要切到Java 17你得重新打开环境变量配置把JAVA_HOME改成另一个路径然后保存、重开终端运行java -version确认。改一次还行一天改好几次就会特别烦躁而且万一有几个终端窗口没重开内存里的环境变量还是旧的还会产生“明明切换了但没生效”的错觉。手动解压的方式还有一个隐藏问题JDK版本信息是分散的。你下载了好几个版本的压缩包时间一长你自己都分不清哪个目录对应哪个版本也不敢乱删怕删了某个正在跑的项目就崩了。系统的包管理器虽然能装但版本更新通常滞后而且它会把JDK和软件仓库的其他组件混在一起很多时候并不适合精确控制项目的运行时。所以我之后换成专用的版本管理工具整个流程就清爽多了。2. 先弄懂SDKMAN是什么2.1 包管理器之外的另一个身份SDKMAN全称是Software Development Kit Manager很多人把它当成包管理器其实它的主要身份是“版本管理工具”。它不直接参与编译你的代码也不会把某个SDK“装进系统里”让你在系统设置中看到它而是负责在你的用户目录下维护一套“候选版本”列表并且通过符号链接的方式随时切换当前生效的版本。这套思路和Node生态里的nvm、Python生态里的pyenv非常相似。如果你熟悉其中任何一个工具理解SDKMAN会很快。它会把自己的目录放在~/.sdkman/candidates/java/下面每个已安装的JDK对应一个子目录比如17.0.12-tem、21.0.3-tem。同时会有一个current符号链接指向当前正在使用的版本。你的JAVA_HOME只需要指向~/.sdkman/candidates/java/current切换版本时SDKMAN会修改这个链接的指向其他所有配置都不用动。2.2 为什么选它而不是去官网下载官网下载本身没什么问题但那种方式只适合你一次性装好、再也不换版本的情况。只要你有两个以上版本的需求手工管理方式的效率低到可以忽略。SDKMAN相比其他管理方式有几点很实用。第一它维护的候选版本列表非常全不仅仅是原始JDK发行版还包括各种OpenJDK发行版比如Temurin、Zulu、Liberica等。第二所有操作都在用户目录里完成不需要sudo权限也不会污染系统级别的环境变量。第三它统一了安装、卸载、切换的入口命令风格非常一致学一次就能用在Java之外的SDK上比如Maven、Gradle、Spring Boot CLI等也都支持管理。还有一点对我个人来说很省心它可以帮你记录当前安装了哪些版本、每个版本对应哪个项目。你不需要再维护一份文本清单来记录谁是谁了。2.3 运行环境的前置条件SDKMAN本身是用Shell脚本写的所以在Unix-like系统上运行最顺畅常见的Linux发行版、macOS都支持。你需要的基本上是一个能跑bash和curl的终端环境Windows下可以通过WSL或者Git Bash之类的环境来用。安装之前最好先确认一下curl命令可用方法是在终端里执行curl --version如果提示找不到命令先补上curl再继续。3. SDKMAN的安装与基础配置3.1 安装脚本的完整执行过程官方给的安装命令是一行比较长的管道命令curl -s https://get.sdkman.io | bash执行后脚本会在你的用户目录下创建一个.sdkman目录把运行需要的脚本、配置、候选版本目录结构都准备好。同时它还会自动往你的shell配置文件中追加几行目的是在每次开启新的终端窗口时自动加载SDKMAN环境。如果你用的是bash它改的是~/.bashrc如果是zsh则改~/.zshrc。装完之后立刻在当前终端窗口内执行source $HOME/.sdkman/bin/sdkman-init.sh然后执行sdk version如果能正常输出版本号就说明安装成功了。下次打开新的终端时就不需要再手动执行这条初始化命令了因为它已经被写进了shell配置文件里。3.2 初始化与验证我建议安装完以后先确认三件事一是sdk version能正常打印版本号二是echo $SDKMAN_DIR能输出路径三是which sdk能找到可执行命令。如果前两步都正常但第三步提示找不到命令多半是当前shell环境没有重新加载配置导致的。对于zsh用户如果sdk version在交互终端中能用但在脚本中调用失败检查一下.zshrc里加载SDKMAN的几行代码的位置是否太靠前。有个比较常见的问题是用户把alias相关的代码写在了SDKMAN初始化之前导致某些场景下初始化没有正常执行。这时候把init相关代码调整到文件靠前位置基本就能解决。3.3 常见安装报错排查安装过程中最容易出的问题就是下载脚本超时。如果curl执行到一半卡住或者报连接失败的错可以先检查网络环境是否正常然后尝试把get.sdkman.io换成社区提供的镜像源或者设置系统级的环境变量让curl走可用的通道。这里不展开细节但有一点要重点提示注意官方安装脚本本质是从远程下载并直接执行因此在运行之前最好先把它下载到本地快速浏览一遍脚本内容确认没有问题后再执行。谨慎一点不是坏事。如果之前装过SDKMAN但中途失败重装前建议先把~/.sdkman目录和shell配置文件里相关的一小段配置清理掉否则可能出现两个版本的初始化代码互相干扰的情况。4. 用SDKMAN安装JDK版本4.1 先列出可选版本安装之前先看看官方仓库里目前有哪些版本可用执行sdk list java输出信息很丰富。每行会给出一个版本标识符identifier比如17.0.12-tem、21.0.3-tem后面会标明它属于哪个发行版比如Temurin、Zulu、Liberica、Oracle等还会标出当前安装状态、已安装和本地版本的区别。刚接触这个命令的人容易对着屏幕发懵其实需要注意的只有四列标识符、发行版、状态、使用类型。在这之前还有必要了解发行版之间的差异。Temurin是Eclipse社区维护的OpenJDK发行版稳定且免费适合大多数后端项目Zulu对多平台的支持比较全移动端和一些嵌入式场景经常用到Liberica在桌面开发和特殊架构上的支持做得不错Oracle发行版则更多面向需要商业支持的场景。不同发行版之间整体API几乎一致只是补丁节奏和支持策略不同。多数情况下选Temurin就能顺利跑完整个项目周期。4.2 安装指定发行版选好版本后直接输入sdk install java 17.0.12-tem这个时候SDKMAN会开始下载对应的压缩包自动解压到~/.sdkman/candidates/java/17.0.12-tem目录下。下载速度取决于网络环境和所选发行版一般几分钟内能完成。安装完成后它还会在终端里问你是否把这个版本设为默认版本输入Y表示确认输入n表示仅安装但不切换默认值。如果你需要好几个版本就可以反复执行sdk install java 8.0.432-tem sdk install java 21.0.3-tem装完整理后的目录大致是这样~/.sdkman/candidates/java/ ├── 8.0.432-tem ├── 17.0.12-tem ├── 21.0.3-tem └── current - 17.0.12-tem4.3 安装到指定路径的原理说明如果你愿意也可以通过环境变量控制安装目录的顶层路径。SDKMAN默认把一切放在~/.sdkman下但这个路径不是写死的靠的是SDKMAN_DIR环境变量。在安装之前设置这个变量就能把整个SDKMAN的数据目录挪到其他磁盘分区或者目录。有些公司会有统一的开发目录规范利用这个变量可以避免把个人数据放在默认目录下。不过我不太建议普通用户做这种自定义。因为很多工具和IDE默认会搜索~/.sdkman这个路径改了以后你需要额外告诉每个工具SDKMAN的位置反而多了一步配置。5. 多版本切换的几种姿势5.1 全局切换和当前窗口临时切换default和use是两个容易混淆的命令但区别很清晰。default是持久化操作会让某个版本成为以后每次打开终端时的默认版本修改的是~/.sdkman/etc/config中的配置。use则只管当前这个终端窗口窗口关闭后也就失效了。sdk default java 21.0.3-tem sdk use java 17.0.12-tem实际使用中我会按照项目性质来决定用哪个。长期在维护的老项目就把默认版本设置成Java 8临时要测试一个新框架就用use在当前窗口切换到Java 17测试完重开一个终端就回到了默认版本。5.2 按目录自动切换这个功能是SDKMAN里非常值得推荐的一个特性——.sdkmanrc。思路很简单在每个项目目录下放一个.sdkmanrc文件文件内容指定这个项目要用哪个SDK版本。进入目录后执行sdk envSDKMAN会读取当前目录和上级目录中最近的.sdkmanrc文件并按照文件内容自动切换版本。文件内容很简单比如java8.0.432-tem如果把这一行写进某个老项目根目录的.sdkmanrc文件之后进入这个目录执行sdk envJDK就会自动切到Java 8。同理在新项目目录里再写一行java21.0.3-tem切目录时就能大幅减少手动切换的工作量。需要注意sdk env只作用于当前终端会话。如果你开了多个终端窗口每个窗口都要单独执行一次不会自动跨窗口生效。5.3 设置默认版本默认版本的设置非常值得单独说一下。我们经常遇到“明明已经切换了但刷新环境后版本又变了”的情况。这种时候一般在重构某个老项目切回默认版本再切到目标版本时会发现java -version仍然显示旧的。原因往往是IDE保留了之前的JAVA_HOME缓存或者终端会话是从旧环境继承过来的。用default切换后重开终端是保证生效最实在的办法。如果不想重开就重新执行source $HOME/.sdkman/bin/sdkman-init.sh再验证。记住JAVA_HOME看着是current路径但进程里的环境变量是启动那一刻快照出来的不是实时动态变化。6. 常用命令速查与脚本化技巧6.1 常用命令速查我整理了一份使用频率最高的命令表方便复制到自己的笔记里。这里不包含全部功能只保留了日常能真正用到的部分。命令作用sdk list java查看所有可安装的JDK版本sdk install java 17.0.12-tem安装指定版本sdk uninstall java 17.0.12-tem卸载指定版本sdk use java 17.0.12-tem当前窗口切换到指定版本sdk default java 17.0.12-tem全局默认切换到指定版本sdk current查看当前所有SDK版本的状态sdk upgrade java升级Java相关的SDK包sdk offline进入离线模式只使用本地已安装版本sdk current这个命令经常被低估。执行以后能一次性看到Java、Maven、Gradle等所有由SDKMAN管理的候选版本当前状态比挨个java -version和mvn -version直观得多。6.2 在CI或脚本环境中使用SDKMAN不只是给本地终端用的在服务器、构建机上同样能发挥作用。打包脚本或者持续集成流程里可以先通过source加载初始化脚本再安装指定版本然后设置默认值一次性把环境准备到位。比如export SDKMAN_DIR$HOME/.sdkman source $HOME/.sdkman/bin/sdkman-init.sh sdk install java 17.0.12-tem sdk default java 17.0.12-tem这里有一个小技巧在脚本里反复执行sdk install java时如果版本已经安装过了它会显示“already installed”并直接跳过后续操作不会重复下载所以即使脚本被多次执行代价也很低。如果需要完全清除后重装可以先rm -rf ~/.sdkman/candidates/java/目标版本再执行安装命令。6.3 离线模式的使用场景我有时会遇到构建机网络受限的情况此时SDKMAN无法访问远程仓库列表但本机上其实已经装好了需要的版本。执行sdk offline进入离线模式后安装、切换、卸载都只基于本地缓存不会尝试连接远程的版本列表。这个模式在隔离环境里特别有用。退出离线模式执行sdk online即可操作很轻量。7. 踩坑记录与避坑指南7.1 踩坑一版本列表拉不下来或超时表现是执行sdk list java后长时间没有输出或者提示Could not resolve host之类的错误。常见原因除了网络不通还有可能是CDN节点响应过慢。解决办法是从官方发布页获取镜像源信息修改~/.sdkman/etc/config里的sdkman_webservice配置换成可用的镜像地址然后执行sdk update刷新列表。如果只是临时需要本地已有版本直接进离线模式用就好。7.2 踩坑二IDE不识别SDKMAN路径这个问题常在配置开发环境时出现。你在终端里java -version一切正常但IDE里项目SDK却显示无效路径。原因是IDE启动时是从桌面图标或启动器读取环境的它不一定继承了终端里source过的SDKMAN配置。解决办法有三个一是从终端直接启动IDE让IDE继承shell环境二是在IDE里手动添加JDK路径到~/.sdkman/candidates/java/特定版本三是把JAVA_HOME明确指向~/.sdkman/candidates/java/current并写进IDE的环境变量配置里。我个人比较喜欢第三种因为以后切换版本不用再回IDE改路径。7.3 踩坑三切换版本后JAVA_HOME没有变化有时候当前目录下执行java -version显示是Java 8但echo $JAVA_HOME还是指向Java 17的current你会觉得SDKMAN没有生效。多数情况下是因为当前shell加载SDKMAN初始化时JAVA_HOME被设置过一次静态路径而你手动用export JAVA_HOME...覆盖过它。SDKMAN在use和default切换时会重新生成JAVA_HOME但如果之前手动导出了固定路径就可能会覆盖掉SDKMAN的动态设置。解决思路很简单别在~/.bashrc或项目脚本里手动写死JAVA_HOME的绝对路径让SDKMAN来管理。如果非要在脚本里指定那就在设置完JAVA_HOME之后重新source一次初始化脚本把current链接的路径重新挂到环境变量上。7.4 踩坑四升级SDKMAN后本地版本丢失SDKMAN本身升级是一个常见的动作执行sdk selfupdate后极少数情况下会把candidates/java/目录下的部分内容重置好在已安装的JDK包并不会被删除只是current链接恢复到了默认。这种情况直接在sdk list java里把之前安装过的版本标记为已安装再重新default一次就可以了。真正需要上心的是不要手动去改~/.sdkman/candidates/java/目录下的文件结构比如往里面塞手动解压的文件夹否则很可能让SDKMAN的版本检测混乱。这么多年用下来我最深的一个体会是版本管理工具的价值不在于“能装多个版本”而在于“切换成本足够低”。当“切一下环境”的成本从十分钟变成一秒钟你就会愿意为每个项目都准备一个独立且干净的运行环境而不是将所有项目都硬塞到同一个JDK上。如果你目前还在手动解压、手动改PATH的阶段我建议你花十分钟把SDKMAN跑起来先把两个版本装好、切换试一遍然后配合.sdkmanrc给项目做自动环境绑定。这个小习惯能让你在同时维护多个项目时把纠结环境变量的精力省下来好好聚焦代码本身。