
如果你在 Debian 或 Ubuntu 上同时装过两个版本的 Java大概率遇到过这种场面明明你自己把新版 JDK 的路径加进了 PATH输入java -version打出来的还是旧版本或者用 apt 老老实实装好了 OpenJDK 11 和 OpenJDK 17想切默认版本时只能手动改/usr/bin/java的符号链接改完发现java和javac对不上编译直接报错。这种时候系统其实已经给你准备好了一个标准的“默认版本管理器”名字就叫update-alternatives。这篇文章我会从头到尾讲清楚update-alternatives的完整用法包括它是怎么工作的、核心命令参数怎么理解、Java/Python/编辑器这些常见场景怎么配置以及我在实际运维中踩过的坑。无论你是刚接触 Linux 的新手还是要应付多版本开发环境的运维看完应该都能直接上手用。1. 为什么需要 update-alternatives多版本共存的版本管理痛点先说个最典型的场景。你在一台 Ubuntu 服务器上同时需要 JDK 8 和 JDK 17JDK 8 跑老项目JDK 17 跑新服务。你会怎么做最简单的想法是把两个 JDK 解压到不同目录然后在~/.bashrc里改JAVA_HOME和PATH。但这只是单用户级别的临时方案换个用户登录又变回去了而且系统里很多服务、脚本依赖的是/usr/bin/java这个全局路径你改了 PATH 根本不影响它们。另一个常见做法是直接用ln -sfn强制改/usr/bin/java符号链接指向。这确实简单粗暴但问题在于一旦你通过 apt 安装、升级、卸载任何 Java 相关软件包dpkg 会重建/usr/bin/java这个链接你手动改的配置立刻被覆盖。到时候新旧版本切换变成了一场拉锯战排查起来特别痛苦。update-alternatives解决的正是这个痛点。它是 Debian 系发行版包括 Ubuntu里维护符号链接的一套标准机制相当于给系统的“默认命令”做了一个统一的管理层。软件包安装时不需要直接去动/usr/bin/java而是把自己注册成某个“候选版本”系统再去决定最终哪个版本生效。这样一来软件包之间的竞争被解耦了切换默认版本变成了一条命令的事情而且不会因为其他包的操作而悄悄失效。除了 JavaPython、编辑器、浏览器、编译工具链等只要存在多版本共存的场景这套机制都能用。它本质上是在回答一个问题当系统里有多个程序提供同一个命令时这个命令到底该指向谁谁说了算1.1 update-alternatives 的链路原理要理解它为什么稳定你得先看清它管理的符号链接层级。以 Java 为例实际生效的路径大概是这样一条链/usr/bin/java ↓ 符号链接 /etc/alternatives/java ↓ 符号链接 /usr/lib/jvm/java-17-openjdk-amd64/bin/java第一跳/usr/bin/java-/etc/alternatives/java这个链接是由 update-alternatives 自动维护的你不需要也不应该手动去改它。第二跳/etc/alternatives/java- 真正生效的 JDK 路径这才是由 update-alternatives 切换的。软件包安装时会把候选版本注册进来dpkg 并不会去覆盖/usr/bin/java它只操作/var/lib/dpkg/alternatives/下的数据库文件和/etc/alternatives/下的链接。所以手动改/usr/bin/java的方式很容易被系统重置而通过 update-alternatives 管理就是系统目前唯一“正规”的方式。1.2 适用场景与边界在哪里我用下来感觉比较适用的场景有三类同一命令的多个版本共存典型就是 JDK还有 Python、GCC、Clang 这一类编译器和解释器。同一条命令的不同实现比如系统里装了多个文本编辑器editor这个命令应该默认调用哪一个又比如装了多个浏览器x-www-browser应该打开谁。自定义安装的软件接入系统管理自己编译安装的软件如果想让系统里的其他命令通过统一方式引用也可以注册进 alternatives。它不适合的场景也很明确如果你需要的是相互隔离、互不干扰的环境比如每个项目一套完全独立的 Python 环境那么应该用 pyenv、conda、virtualenv 这类更重度的环境管理工具。update-alternatives管理的是“全局默认命令”这一层而不是“隔离环境”这一层。两者不冲突甚至可以配合使用但你要清楚各自的边界别指望着用 alternatives 解决环境隔离的问题。2. 核心细节解析命令语法与优先级机制update-alternatives的命令本体不长核心子命令就五个--install、--config、--set、--remove、--display再加上一个--list看数据。别被它的参数吓住拆开看其实非常清晰。2.1 --install把候选版本注册进系统这是最核心的一条命令语法如下update-alternatives --install 链接路径 组名 候选程序绝对路径 优先级 [--slave 链接 组名 绝对路径]四个必填参数的意思链接路径最终要生成的符号链接路径比如/usr/bin/java。组名这一组候选版本的标识同一个软件的所有备选版本用同一个组名比如java。候选程序绝对路径你安装的这个版本实际执行文件在哪里必须是绝对路径。优先级一个整数数字越大在 auto 模式下越优先选择它。举个例子把 JDK 17 注册为java的候选版本sudo update-alternatives --install /usr/bin/java java /usr/lib/jvm/java-17-openjdk-amd64/bin/java 100这里优先级给 100。为什么是 100其实没有硬性规定纯粹是一个习惯用法主要看你想让它在 auto 模式下的排位如何。如果你希望这个版本默认胜出优先级就比其他候选给得高如果只是备选就调低一点。关键是理解这个数字只影响 auto 模式下的选择不影响手动切换。2.2 --slave 与同组联动命令Java 不只是java一条命令还有javac、jar、javadoc等一堆配套命令。只切换java不切换javac会出现灾难性后果java已经是 17 了javac还在 8编译用的字节码版本和运行环境对不上。--slave就是用来解决这个问题的它把一组命令绑定在一起切换。sudo update-alternatives --install /usr/bin/java java /usr/lib/jvm/java-17-openjdk-amd64/bin/java 100 \ --slave /usr/bin/javac javac /usr/lib/jvm/java-17-openjdk-amd64/bin/javac \ --slave /usr/bin/jar jar /usr/lib/jvm/java-17-openjdk-amd64/bin/jar \ --slave /usr/bin/javadoc javadoc /usr/lib/jvm/java-17-openjdk-amd64/bin/javadoc这样你切java版本时javac、jar这些会跟着一起切。我在实际维护多套 JDK 环境时最怕的就是切换后版本不一致这个参数基本属于必用项。不过要小心--slave的组名不能和主组名重复而且同一个路径不能同时作为多个组的主链接或从链接否则会报错。2.3 --config、--set、--remove 与模式机制注册好候选版本后查看或切换默认版本用--configsudo update-alternatives --config java执行后会列出这个组里所有候选版本让你输入序号选择。这个交互式界面在手动调试时很好用但是不适合脚本自动化。想静默切换用--setsudo update-alternatives --set java /usr/lib/jvm/java-8-openjdk-amd64/bin/java--set不需要交互直接指定生效的候选路径我写自动化脚本时基本只用它。移除某个候选版本用--removesudo update-alternatives --remove java /usr/lib/jvm/java-8-openjdk-amd64/bin/java注意--remove如果删的是当前生效的那个版本系统会自动从剩下的候选里重新选一个不会让你陷入“没有默认版本”的尴尬。查看当前状态可以用--displayupdate-alternatives --display java输出里会显示当前是 auto 模式还是 manual 模式、链接指向哪个路径、有哪些候选版本。--list则只列出所有组名没有细节。关于 auto 和 manual 模式这里有一个我一开始没注意的重要点。当你用--config或--set手动指定过版本之后这个组就变成 manual 模式了之后就算有更高优先级的软件包装了进来也不会自动切换。想让系统重新按照优先级自动选择用sudo update-alternatives --auto java举个真实例子有一次我装了 OpenJDK 17系统里原本有 OpenJDK 11因为我之前手动切过 11所以装好 17 后什么都没发生。后来排查下用--auto java切回自动模式才让优先级发挥作用。这个细节很容易被忽略。3. 实操过程与核心环节实现从 Java 到 Python 到自定义程序讲了半天原理下面直接上案例。我会以一台 Ubuntu 22.04 服务器为例完整跑一遍注册、切换、移除的流程。同时把常见坑位标出来。3.1 场景一JDK 8 / 11 / 17 三版本切换假设我已经通过 apt 装好了三个版本的 JDKsudo apt update sudo apt install openjdk-8-jdk openjdk-11-jdk openjdk-17-jdk但实际上 Debian 系仓库不一定三个版本都齐全更接近真实的做法是手动解压每个 JDK 到/usr/local/jdk-8、/usr/local/jdk-11、/usr/local/jdk-17。无论哪种方式先看当前状态java -version ls -l /usr/bin/java如果之前装过某一个 JDK/usr/bin/java已经存在且指向/etc/alternatives/java那我们不需要额外创建。如果/usr/bin/java不存在注册的第一个候选版本会自动先顶上。把三个版本都注册进来注意用不同的优先级我把 17 的优先级调最高表示希望默认使用新版sudo update-alternatives --install /usr/bin/java java /usr/local/jdk-17/bin/java 200 \ --slave /usr/bin/javac javac /usr/local/jdk-17/bin/javac \ --slave /usr/bin/jar jar /usr/local/jdk-17/bin/jar \ --slave /usr/bin/javadoc javadoc /usr/local/jdk-17/bin/javadoc sudo update-alternatives --install /usr/bin/java java /usr/local/jdk-11/bin/java 150 \ --slave /usr/bin/javac javac /usr/local/jdk-11/bin/javac \ --slave /usr/bin/jar jar /usr/local/jdk-11/bin/jar \ --slave /usr/bin/javadoc javadoc /usr/local/jdk-11/bin/javadoc sudo update-alternatives --install /usr/bin/java java /usr/local/jdk-8/bin/java 100 \ --slave /usr/bin/javac javac /usr/local/jdk-8/bin/javac \ --slave /usr/bin/jar jar /usr/local/jdk-8/bin/jar \ --slave /usr/bin/javadoc javadoc /usr/local/jdk-8/bin/javadoc说明一点如果java、javac这类命令已经被 openjdk 包注册过上面命令再执行一次也没事相当于更新候选列表不会破坏已有配置。查看当前生效版本sudo update-alternatives --display java想手动切到 JDK 11sudo update-alternatives --set java /usr/local/jdk-11/bin/java验证一下三条命令是否一致java -version javac -version which java which javacwhich java输出/usr/bin/java然后ls -l /usr/bin/java能看到它指向/etc/alternatives/java再ls -l /etc/alternatives/java指向/usr/local/jdk-11/bin/java。链路完整没有断裂切换成功。这个验证流程也是我在排查问题时的标准动作一级一级看符号链接指向哪里。3.2 场景二Python 版本的切换在 Ubuntu 20.04 之后系统里默认是 Python 3但很多老脚本还在用 Python 2。这时也可以用 alternatives 做默认python命令的切换。注册 Python 3 和 Python 2sudo update-alternatives --install /usr/bin/python python /usr/bin/python3 200 sudo update-alternatives --install /usr/bin/python python /usr/bin/python2 150然后用--config切sudo update-alternatives --config python执行后界面里会列出两个候选版本输入序号回车即可。但这里我要多说一句Python 的版本管理我其实更推荐 pyenv 或 conda因为 Python 不是只有python一条命令还有pip、pip3、python-config等一堆配套而且很多项目依赖的包版本跟解释器版本强相关。用 alternatives 管理 Python 只适合“全局默认 python 指向谁”这种粗粒度需求细粒度的项目级隔离就别用它了。实际项目里我见过更稳妥的方案python由 alternatives 管理pip则通过python -m pip调用。这样就可以保证你用哪个 python就会用对应的那个 pip不会出现 pip 装到了另一个版本去的诡异问题。这个思路也推荐给你注册python时别急着给pip单独建一个 alternatives 组最好让 pip 跟随主 python 走。3.3 场景三editor 命令与自定义程序接入还有一个很实用的例子。很多 Linux 系统里editor是一个通用命令默认通常指向vim或nano。查看当前编辑器sudo update-alternatives --display editor通过--config editor可以直接在 nano、vim、vim.tiny 之间切换这也是系统安装时给用户选择默认编辑器的底层机制。如果自己编译安装了一个程序想让全系统都能直接用并且以后可切换版本也可以手动注册。比如我编译安装了某个自定义工具链到/opt/mytool/bin/mytoolsudo update-alternatives --install /usr/bin/mytool mytool /opt/mytool/bin/mytool 100 \ --slave /usr/bin/mytool-config mytool-config /opt/mytool/bin/mytool-config这样mytool就进入系统标准的命令管理范围了。对运维同学来说这种做法的好处是以后卸载软件时通过update-alternatives --remove mytool /opt/mytool/bin/mytool就能把相关符号链接全部清理干净不用自己一个个手工找。3.4 与 RHEL 系 alternatives 的区别这里补充一个容易被绕晕的点RHEL、CentOS、Rocky 上也有alternatives命令而且语法和 Debian 系几乎一样。区别主要在配置存储路径Debian/Ubuntu 存在/var/lib/dpkg/alternatives/RHEL 系存在/var/lib/alternatives/。另外 RHEL 系的命令可能是alternatives需要安装chkconfig或alternatives包。其余像--install、--config、--set这些参数基本通用。如果你两套系统都在维护学会一套命令另一套基本能直接上手。4. 常见问题与排查技巧实录用的时间久了总会遇到几个反复出现的坑。我整理了一份速查表基本都是我实际踩过的。现象原因解决方式提示alternative path is not absolute注册时候选路径没有写绝对路径--install里的候选程序路径必须是/开头的绝对路径--config只显示部分候选版本有些包没在 alternatives 注册或注册时用了不同组名用update-alternatives --list查看所有组名切换以后java -version没变化shell 缓存了命令路径或者 PATH 里有更靠前的同命令目录执行hash -r清缓存检查echo $PATH中是否有/usr/local/bin之类的优先路径手动改了/usr/bin/java升级软件后又变回去直接改动绕过了 alternatives 系统用update-alternatives --set重新指定别手动改符号链接新装了更高优先级的包但默认版本没变该组处于 manual 模式先--auto 组名切回自动模式删除候选后没有默认版本理论上不会发生但链接可能已损坏用--install重新注册或检查/etc/alternatives/下对应链接4.1 注册时路径写错怎么修复有一次我注册 JDK 时把路径写成了/usr/local/jdk-11/bin/java但实际上该目录里没有 java 可执行文件。命令执行不报错但后面一旦真正切换就会出现符号链接断裂。排查方法很简单ls -l /etc/alternatives/java如果指向一个不存在的文件直接重新注册正确的路径覆盖掉即可sudo update-alternatives --install /usr/bin/java java /usr/lib/jvm/java-11-openjdk-amd64/bin/java 100这里有个坑系统不会校验候选程序是否真的存在。所以注册前最好先手动执行一下候选路径确认有可执行权限/usr/lib/jvm/java-11-openjdk-amd64/bin/java -version确认能跑再注册能省掉不少后续排查时间。4.2 切换后命令版本不一致的排查思路如果你切换完java后javac还是旧版本第一反应要检查注册时有没有配--slave。很多人只注册了主命令没把配套命令绑进去导致它们各自独立切换。检查方法update-alternatives --display java输出里会列出跟随主命令切换的 slave 链接。如果发现从命令不在列表里说明当时没有用--slave注册。解决办法是把全套命令重新注册一遍或者单独把javac也注册成一个独立的 alternatives 组。但注意独立组的切换跟java组是分开的以后要切就得多排一条命令不如重新注册成 slave 干净。4.3 alternatives 被破坏后的手动修复极端情况下比如误删了/etc/alternatives下的文件或者 dpkg 中途异常中断就会出现update-alternatives --config java提示“没有可用的候选版本”这种情况。我的修复步骤是# 先查看数据库里还有没有这个组的记录 ls /var/lib/dpkg/alternatives/ # 如果有记录尝试强制重新触发 sudo update-alternatives --remove java /usr/local/jdk-17/bin/java sudo update-alternatives --install /usr/bin/java java /usr/local/jdk-17/bin/java 200如果数据库文件本身也损坏最粗暴但有效的办法是从备份恢复/var/lib/dpkg/alternatives/或者干脆把所有候选版本重新注册一遍。重启后一般就恢复正常了。4.4 容器和脚本里的非交互式切换在 Dockerfile 或 CI 脚本里--config这种交互式命令完全没法用。我通常会用--set配合绝对路径来固定版本RUN update-alternatives --set java /usr/lib/jvm/java-17-openjdk-amd64/bin/java但如果脚本执行的用户没有 root 权限或者 /usr/bin 目录是只读的切换会直接失败。尽量避免在只读根文件系统的容器里依赖 alternatives 切换最好在构建镜像时就把默认版本写死。另外自动化脚本里如果判断当前默认版本是否符合预期可以用这条命令readlink -f /usr/bin/java | grep -q java-17readlink -f会把符号链接一路解析到最终真实路径比只看一层/etc/alternatives/java可靠得多。5. 写在最后一套命令解决全系统默认版本的治理我在多台服务器上维护过好几套 Java、Python 和编译工具链混合的环境update-alternatives算不上花哨但它把“全局默认命令”这件事系统化了。以前我靠改 PATH、改符号链接管理多版本每次升级软件包都会惊出一身冷汗现在全部走--install注册、--set切换、--display查询链路一眼看穿出问题也容易回滚。最后分享一个我个人的使用习惯注册任何候选版本时永远把配套命令用--slave绑好。哪怕当时觉得某个配套命令用不上也绑上免得以后切换主版本时留下一个半新半旧的环境。这个习惯救过我很多次建议你从一开始就养成。