1. 为什么在Ubuntu上装JDK总是状况百出——先理清四层关系
我见过太多人装JDK装到怀疑人生,尤其在Ubuntu上。明明在Windows里双击exe一路点“下一步”就行,到了Ubuntu却要面对命令行、环境变量、符号链接、权限……每一步都可能翻车。其实问题不在于“命令记不住”,而在于没搞懂Linux下JDK是怎么被系统找到、被命令调用的。
先说一个最底层的事实:你安装JDK,最终目标无非是让shell敲下java、javac时能执行对应程序,并且让各种构建工具(Maven、Gradle)通过JAVA_HOME找到JDK目录。所谓“安装”,本质上是两件事:把JDK文件放到某个目录,再告诉系统“去哪儿找它”。理解了这一点,后面所有配置都是围绕它展开的。
1.1 JDK、JRE、Server JRE,到底该装哪个
很多初学者分不清JDK和JRE。简单说:JRE是Java程序的运行环境,只包含Java虚拟机和核心类库,没有编译器和工具;JDK包含了JRE的全部内容,外加javac编译器、javadoc、javap、jconsole等开发工具。你要是只跑别人写好的Java程序,装JRE足够;要写代码、编译、调试,就必须装JDK。
至于Server JRE,那是给服务器部署用的精简版,很少作为桌面或开发环境的选择,普通开发直接上JDK就行。在Ubuntu的软件仓库里,包名通常以openjdk-8-jdk、openjdk-11-jdk、openjdk-17-jdk这种形式出现,看清楚是jdk结尾而不是jre,光装openjdk-17-jre是编译不了代码的。
1.2 OpenJDK与Oracle JDK:别被“企业级”带偏
网上不少教程会给一张对比表,说Oracle JDK性能更好、商业支持更完善。对绝大多数个人开发、中小项目来说,OpenJDK和Oracle JDK的运行时差异小到感知不出来。你日常写Spring Boot、跑LeetCode、做课程设计,OpenJDK完全够用。而且Ubuntu的apt仓库里默认就是OpenJDK,装起来最省事。
什么情况下才需要Oracle JDK?比如公司明确要求特定版本、特定补丁等级,或者你要用Oracle官方提供的商业支持服务。还有一个场景是某些老项目依赖Oracle JDK专有的JMC(Java Mission Control)工具,或者需要其特定的图形渲染表现。除此之外,我建议你直接用OpenJDK,少折腾。
1.3 版本选择:8、11、17、21,你的项目说了算
版本选择是另一个高频翻车点。有人觉得“装新不装旧”,直接上最新版,结果项目用的框架不兼容,编译报错让人抓狂。有人觉得“8最稳”,但新框架早就放弃对8的支持了。
我的经验是,优先看三个约束:
- 项目或团队指定版本,这是硬约束,说什么都得遵守;
- Spring Boot等框架的兼容清单,比如Spring Boot 3.x要求Java 17以上,Spring Boot 2.x一般用Java 8或11;
- 长期支持版本优先,目前最常见的是8、11、17、21。8虽然是LTS但确实老了,新项目我一般推荐17,老项目维护再根据实际情况选8或11。
如果你只是个人学习、写写算法题,装最新LTS版本问题不大,但建议同时保留一个多版本切换方案,后面第五章会专门讲。
1.4 三种安装方式:apt、手动解压、版本管理工具
Ubuntu装JDK的路径大致有三条,各有适用场景:
| 安装方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| apt / apt-get | 命令简单,自动配置环境变量和系统路径 | 仓库版本通常滞后,可能不是指定版本 | 快速使用,对版本不敏感 |
| 手动解压官方包 | 完全控制版本和目录,不受仓库限制 | 需要自己配置环境变量,步骤多 | 指定版本、生产环境精准控制 |
| SDKMAN / jenv | 多版本切换方便,纯用户态管理 | 需要先装基础工具,对初学者有认知门槛 | 日常开发,多项目多版本切换 |
不要一上来就纠结哪条路最“高级”。新手用apt跑通流程,遇到版本卡住了再学手动解压,等用顺了再上SDKMAN,这符合大多数人的学习路径。
2. 最快路径:用apt安装OpenJDK,两分钟跑通
如果你的需求是“能装就能跑”,那apt是最短路径。Ubuntu官方仓库已经打包好了OpenJDK,一条命令就能搞定,连环境变量都不用你操心。
2.1 先更新索引,再安装,避免“找不到包”
很多人在这个环节犯的第一个错是:没有先sudo apt update就直接安装,结果提示Package not found,以为软件源有问题。实际上Ubuntu的软件源列表只是索引,需要定期拉取最新的包元数据,尤其是刚装完系统或者换了源之后,不更新索引就搜索不到新包名。
正确的命令顺序是这样:
sudo apt update sudo apt install openjdk-17-jdk第一条命令会刷新软件源索引,输出一大堆Hit、Get、Ign,耐心等它跑完。第二条命令才是真正安装JDK。如果你想装其他版本,把17替换成8、11、21等数字即可。
注意一点:执行apt install时如果提示某个依赖包缺do-release-upgrade,多半是你镜像源配置有问题,后面会有解决方案。
2.2 安装完成后,用java -version验证是第一道关卡
装完之后先别急着写代码,验证一下:
java -version javac -version如果输出类似这样的内容,说明安装成功:
openjdk version "17.0.10" 2024-01-16 OpenJDK Runtime Environment (build 17.0.10+7-Ubuntu-0ubuntu122.04) OpenJDK 64-Bit Server VM (build 17.0.10+7-Ubuntu-0ubuntu122.04, mixed mode, sharing)如果java -version有输出但javac -version报“找不到命令”,说明你装的是JRE而不是JDK,或者JDK的bin目录没有进入PATH。前者重新装-jdk包,后者需要手动检查路径配置。
2.3 为什么“装好的Java”可能不是你想用的Java
apt安装OpenJDK后,会把文件放到/usr/lib/jvm/目录下,比如/usr/lib/jvm/java-17-openjdk-amd64,然后通过update-alternatives机制把java、javac等命令链接到/usr/bin/下。
问题来了:系统可能同时也装了其他Java版本,比如某个系统组件自己带了OpenJDK 11,或者你以前装过另一个版本。你执行java -version时看到的可能是旧版本,而不是你刚刚装的17。
判断方法很简单:
which java ls -l $(which java)如果which java显示的是/usr/bin/java,再用ls -l查看,你会看到类似这样的输出:
lrwxrwxrwx 1 root root 22 Feb 15 10:00 /usr/bin/java -> /etc/alternatives/java这是一个符号链接,说明系统并不是直接把真实的java放在/usr/bin/,而是通过alternatives机制转发。真正管用的是/etc/alternatives/java指向哪个JDK。这个问题不解决,即使你装了新版本,也可能始终跑的是旧版本。后面第五章专门讲怎么切换。
3. 指定版本安装:手动解压是绕不开的路
apt仓库里的版本往往滞后,比如Ubuntu 22.04的仓库里OpenJDK 8和11的包还算齐全,但如果你要的是OpenJDK 21、某个特定构建版本,或者Oracle JDK,就得手动下载解压安装。
3.1 下载链接不要乱找,先认准三个可靠来源
手动下载JDK,最怕从搜索引擎随便点进去,下载到捆绑广告、木马修改过的安装包。我一般只用这些来源:
- OpenJDK官方构建:访问
jdk.java.net,这里提供GPL协议的官方构建版本,版本更新及时,文件名类似openjdk-21_linux-x64_bin.tar.gz; - Eclipse Adoptium:也就是常说的
Temurin,地址adoptium.net,提供OpenJDK的社区构建版本,补丁更新非常及时,而且带TCK认证,生产环境用得很广泛,国内还有清华镜像加速; - Oracle官网:下载Oracle JDK需要先接受许可协议,页面交互比较繁琐,但如果你确实需要Oracle官方版本,只有这一条路。
顺带说一个下载加速的技巧:Adoptium的镜像在很多高校和开源镜像站都有同步,比如清华TUNA、阿里云镜像,直接搜“Adoptium 清华镜像”就能找到对应路径,速度比直连官网快不少。我实际试过,即使在非高峰时段,官网下载速度可能只有几百KB,换镜像源能直接拉满带宽。
3.2 解压和目录规划:/usr/lib/jvm还是/opt,按系统习惯来
拿到tar.gz后缀的文件之后,先别急着解压到任意目录。Linux对目录规划有一套约定俗成的规矩,虽然不是强制的,但按规矩来会省掉很多后续的坑。
我推荐把JDK解压到/usr/lib/jvm/目录下,因为这是Ubuntu系统存放Java的默认位置,各种工具和脚本会优先到这里找JDK。命令这样写:
sudo mkdir -p /usr/lib/jvm sudo tar -zxvf openjdk-21_linux-x64_bin.tar.gz -C /usr/lib/jvm解压完你会得到一个如/usr/lib/jvm/jdk-21的目录。有的教程建议放到/opt/下,这个目录通常放第三方独立软件包,也没有错。但如果你后续想用update-alternatives来管理版本,放在/usr/lib/jvm下会更顺手,因为很多构建脚本默认从这里探测。
我建议给目录改个规范的名字,比如带版本号和架构,避免以后多个版本混在一起分不清谁是谁:
sudo mv /usr/lib/jvm/jdk-21 /usr/lib/jvm/jdk-21-amd64当然,如果你的项目需要用绝对路径写死JDK目录,那解压到哪个目录反而是次要的,保持好记录就行。
3.3 手动安装后必须做的收尾:创建链接与确认权限
手动解压和apt安装有一个关键区别:apt会自动帮你创建命令链接和环境变量,手动解压则不会。你解压完,打开终端敲java -version,大概率会报“找不到命令”,这不奇怪。
要让系统“看到”这个JDK,有两个办法:
- 方法一(推荐):通过
update-alternatives注册版本,让系统统一管理; - 方法二:手动把JDK的
bin目录加进PATH环境变量。
方法二虽然简单直接,但如果你以后装了多个版本,手动改PATH会越来越乱。我更推荐方法一,具体命令后面第五章会写。现在只需要确认一下目录权限没有问题:
ls -l /usr/lib/jvm/jdk-21/bin/java确保这个文件有执行权限,如果没有,加一下:
sudo chmod +x /usr/lib/jvm/jdk-21/bin/java4. 环境变量配置失败的真正原因——PATH、JAVA_HOME、软链接三者的关系
网上“JDK环境变量配置失败”能上热搜,不是没有理由的。我看了太多人照着教程把export JAVA_HOME=...写进/etc/profile,然后source一下,当时能用,关掉终端再开就失效;或者干脆配完毫无反应。这些问题的根源,是没搞清楚PATH、JAVA_HOME和软链接三者的分工。
4.1 JAVA_HOME与PATH各管什么,谁先谁后
JAVA_HOME是一个约定俗成的环境变量,用于指向JDK安装的根目录。Maven、Gradle、Tomcat、IDEA等工具启动时会读取JAVA_HOME来找JDK。它本身不影响shell命令。PATH是shell查找命令的搜索路径集合。你执行java时,shell就是按PATH里列出的目录,逐个进去找同名程序。
所以,只配JAVA_HOME而不把JDK的bin目录加入PATH,命令行还是找不到java;只配PATH而JAVA_HOME为空,命令行能运行,但Maven等工具会直接报错。两者都要配。
推荐的配置方式是:
export JAVA_HOME=/usr/lib/jvm/jdk-21 export PATH=$JAVA_HOME/bin:$PATH4.2 最容易被忽略的坑:profile文件加载顺序
Ubuntu环境变量配置文件有很多:/etc/profile、/etc/environment、~/.profile、~/.bashrc、~/.bash_profile。它们的加载时机不同,作用范围也不同:
/etc/profile:系统级配置,用户登录时加载,对所有用户生效;~/.profile:当前用户登录时加载,优先于bashrc;~/.bashrc:当前用户的bash会话启动时加载,每次打开终端都会执行;/etc/environment:系统级环境变量,PAM加载,语法不允许使用变量引用。
很多教程让你把配置写进/etc/profile,但如果你是在桌面环境里通过图标打开终端,并不是每次终端启动都会重新加载/etc/profile,导致“配置了但没生效”。反过来,写到~/.bashrc里,每次开新终端都会执行,符合大多数人的日常使用习惯。
所以我的个人建议是:个人开发环境优先写~/.bashrc,服务器全局环境写/etc/profile,两边统一用追加而不是覆盖的方式。例如:
echo 'export JAVA_HOME=/usr/lib/jvm/jdk-21' >> ~/.bashrc echo 'export PATH=$JAVA_HOME/bin:$PATH' >> ~/.bashrc然后验证:
source ~/.bashrc echo $JAVA_HOME4.3 配置完没有生效的排查链路
如果你照着配了依然失败,不要急着改文件,按这条链路排查:
- 确认安装目录是否存在:
ls -d /usr/lib/jvm/jdk-21,目录不存在说明路径写错了; - 确认变量值是否正确:
echo $JAVA_HOME,如果输出空,说明export语句没被执行; - 确认当前shell有没有加载配置:
bash -c 'echo $JAVA_HOME',如果这个有值但当前终端没有,说明当前终端是配置加载之前打开的,开个新终端或用source重新加载; - 确认PATH里有没有被其他版本截胡:
which -a java,会列出所有能找到的java路径,按PATH顺序从上到下生效。如果你手动配置的路径排在另一个旧版本后面,实际执行的是旧版本; - 确认是否被
.bash_profile覆盖:有些用户家里同时存在.bash_profile和.bashrc,登录shell优先加载前者,而前者覆盖了后者的变量。
4.4 桌面版Ubuntu与WSL环境下的配置差异
桌面版Ubuntu和WSL里的Ubuntu,在环境变量加载上有一个明显区别:桌面版通常通过轻量显示器管理器登录,不会自动加载/etc/profile,所以文件管理器、IDE启动的进程环境经常和终端环境不一致。你在终端里source ~/.bashrc配置好了IDEA能用的JAVA_HOME,不代表从桌面图标启动的IDEA也能看到。
解决办法有两个:一是把配置写到/etc/environment,这个文件对所有进程生效,但注意语法简单,不能写$PATH这种引用;二是通过修改桌面启动器的.desktop文件,在启动项里指定环境变量。
WSL环境则简单得多,因为默认shell就是bash,配置写在~/.bashrc里几乎不会出问题。唯一要注意的是WSL和Windows文件系统互访时的路径格式,如果你在WSL里跑Windows版JAVA_HOME,会出现路径格式不匹配的问题,尽量统一用WSL里的Linux版JDK。
5. 多JDK版本共存与切换:update-alternatives的原理和操作
开发中遇到“这个项目要JDK 8,那个项目要JDK 17”太常见了。如果每次都卸载重装,效率低且容易弄坏配置。Ubuntu的update-alternatives机制就是为这种多版本共存设计的,只是很多人不太会用。
5.1 为什么系统里会出现多个Java命令,到底听谁的
开头提到过,系统是通过软链接把真实的JDK暴露给命令行的。你可以把/usr/bin/java看作一个“门面”,它指向/etc/alternatives/java,后者再指向真实的JDK路径。谁被alternatives选中,谁就是当前生效的java。
查看当前系统里注册过的所有Java版本:
sudo update-alternatives --config java执行后你会看到类似这样的列表:
There are 2 choices for the alternative java (providing /usr/bin/java). Selection Path Priority Status ------------------------------------------------------------ 0 /usr/lib/jvm/java-11-openjdk-amd64/bin/java 1101 auto mode 1 /usr/lib/jvm/java-11-openjdk-amd64/bin/java 1101 manual mode * 2 /usr/lib/jvm/jdk-21/bin/java 1001 manual mode带*的是当前生效版本。输入数字可以切换。但注意,update-alternatives --config java只能切换java命令,javac等命令是独立的链接,需要分别配置。这也是很多人切完版本后发现java -version变了,javac -version却没变的直接原因。
5.2 手动注册JDK到alternatives
如果你手动解压安装了JDK,默认不会出现在alternatives列表里,得先注册:
sudo update-alternatives --install /usr/bin/java java /usr/lib/jvm/jdk-21/bin/java 1001 sudo update-alternatives --install /usr/bin/javac javac /usr/lib/jvm/jdk-21/bin/javac 1001这里/usr/bin/java是链接放置的位置,java是链接组名,/usr/lib/jvm/jdk-21/bin/java是真实程序路径,最后的1001是优先级数字,数字越大优先级越高。
如果你还要用jar、javadoc、jconsole等工具,同样可以注册。但一般注册java和javac两个就够日常用了,其他工具需要时再补:
sudo update-alternatives --install /usr/bin/jar jar /usr/lib/jvm/jdk-21/bin/jar 10015.3 版本切换后的连带问题:JAVA_HOME也要跟着变
这是个极易踩的坑。你通过update-alternatives切换了java命令,但JAVA_HOME还是指向旧版本的目录。命令行跑java -version看的是PATH,而Maven、Gradle看的是JAVA_HOME,两者不一致,就会出现“命令行明明是新版,构建工具编译出来却是旧版字节码”这种诡异现象。
解决办法是让JAVA_HOME也随alternatives变化。可以这样设置:
export JAVA_HOME=$(dirname $(dirname $(readlink -f $(which java))))但这行命令在版本切换后需要重新加载shell才生效。更好的方案是用/etc/alternatives/java做一次解析:
export JAVA_HOME=$(readlink -f /usr/bin/java | sed "s:/bin/java::")我把这行写进~/.bashrc后,每次切换版本,只需要重新source ~/.bashrc,JAVA_HOME就会自动跟随。实测多次,暂时没发现比这更省心的写法。
对于日常开发,还有一种替代方案是使用SDKMAN来管理多版本,完全绕开系统级的alternatives机制,不污染全局配置,更适合那种“每个项目要不同Java版本”的密集切换场景。它的安装方式很简单:
curl -s "https://get.sdkman.io" | bash安装后重启终端,用sdk list java查看可安装版本,sdk install java 17.0.10-tem安装指定版本,sdk use java 17.0.10-tem切换当前shell版本,sdk default设置默认版本。它的原理是把版本放在~/.sdkman/candidates/java/下,通过修改PATH实现切换,属于用户态管理,不改系统文件和全局配置,对个人开发来说非常省心。
6. 安装完成之后的验证清单和几个常见坑
环境配完只是开始,能不能编译、运行、被工具识别,才算真正安装成功。这里给出一份我每次装完JDK都会跑的验证流程,以及几个高频问题的解决思路。
6.1 写一个最小的Java文件验证编译与运行
不要只盯着java -version,那一行输出只能说明JVM能启动、版本对得上。真正要验证的是“类能编译、能加载、能执行”。
创建/tmp/Test.java,内容:
public class Test { public static void main(String[] args) { System.out.println("JDK Path: " + System.getProperty("java.home")); System.out.println("Version: " + System.getProperty("java.version")); } }然后执行:
cd /tmp javac Test.java java Test看到两行输出,说明编译器和运行时都正常。java.home属性打印出来的路径,才是当前真正生效的JDK目录。
如果javac能编译但java执行时报告“无法找到或加载主类”,十有八九是classpath问题或文件名与类名不一致。注意Test.java的文件名必须和public class的名字完全一致,大小写也要一样。另外如果你手动设过CLASSPATH环境变量,把它指到奇怪的位置也会导致加载失败。
6.2 下载慢、安装源失效的替代方案
很多人手动下载JDK时卡在下载环节,尤其是Oracle官网或某些国外镜像,速度慢到怀疑人生。这里分享几个实测有效的替代路径:
- 清华TUNA镜像,访问
mirrors.tuna.tsinghua.edu.cn下的adoptium路径,能找到Temurin的长期支持版本,目录结构清晰,支持按版本、平台筛选; - 阿里云镜像,同样同步了Adoptium的构建产物,直接用wget就能拉取;
- 华为云镜像,也是不错的备选。
如果你连编译工具都要下载,那不如先sudo apt install -y build-essential装好基础工具链,减少后续编译过程中的网络依赖。
另外,如果apt安装时提示某个软件源无法访问,很可能是你之前换了源但没执行apt update,或者源地址写错了。检查一下/etc/apt/sources.list及其/etc/apt/sources.list.d/目录下的文件,确认源地址是可达的,然后重新apt update。
6.3 几个我实际踩过的坑
第一个坑是环境变量多配了CLASSPATH。有些教程会让你设CLASSPATH=.:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar,我在JDK 9之后遇到过问题,因为这些jar包已经不存在了,设置后反而干扰类加载。现代JDK里CLASSPATH通常不需要手动设置,真要指定就写.(当前目录)就够,剩下交给工具去管理依赖。
第二个坑是IDE里配置的JDK版本和命令行不一致。我在IDEA里用了Project Structure指定JDK 11,但命令行环境默认是JDK 8,结果Maven编译使用了命令行环境,产生一堆奇怪错误。后来统一用SDKMAN管理版本,IDE直接读取SDKMAN的候选路径,才彻底解决这个问题。
第三个坑是update-alternatives --config配置完javac但忘记配置jar,导致一些需要打jar包的项目报错。虽然不如java和javac频繁,但用的时候找不到还是会影响效率。建议手动安装JDK后,一口气注册常用工具链:
for tool in java javac jar javadoc jshell; do sudo update-alternatives --install /usr/bin/$tool $tool /usr/lib/jvm/jdk-21/bin/$tool 1001 done这种做法能省掉后续不少麻烦。最后补充一句,随着时间的推移JDK版本更新很快,教程里的具体版本号可能会过时,但底层机制和操作思路不会变,你只要理解了“命令查找靠PATH、工具识别靠JAVA_HOME、多版本管理靠alternatives”,这套逻辑放在任何版本的Ubuntu和JDK上都成立。