1. 装 JDK 之前,先把版本、发行版和安装方式这三件事定下来
很多人搜“Linux系统安装JDK”,一上来就开始复制命令,结果装到一半发现装出来的版本不对,或者装好了找不到 java,最难受的是服务器上已经有一套 JDK 8,项目又要求 JDK 17,死活不敢动。其实 JDK 安装本身非常简单,真正麻烦的是“装之前没想清楚装哪个”“装完之后不会切版本”。我就按平时帮同事处理问题的思路,把这几个环节拆开讲。
1.1 版本选择:优先 LTS,新项目闭眼选 17 或 21
JDK 版本不是越新越好,生产环境的核心原则是“选 LTS(长期支持)版本”。目前主流 LTS 是 JDK 8、11、17、21,短期版本像 18、19、20、22、23、24 这些不建议在生产环境用,因为官方支持周期太短,补丁跟不上,出了问题只能自己扛。
很多团队现在还踩在 JDK 8 上,这很正常,老项目依赖的第三方库不一定兼容新版,强行升版风险很大。但如果是新项目,我一般建议直接上 JDK 17 或者 21。JDK 17 是目前兼容性最均衡的版本,Spring Boot 3、主流中间件全都支持,而且比 8 和 11 多了很多语法和性能上的改进;JDK 21 则把虚拟线程做成了正式特性,适合做高并发服务端。我自己的服务器上现在默认就是 17,个别老项目单独锁 8,互不干扰。
这里也想回应一下热搜里那个“jdk降级到17”。不少朋友装了 JDK 25 甚至更高,结果项目一编译就报错,要么语法不兼容,要么字节码版本不被构建工具识别,最后又灰溜溜降回来。这不是你操作有问题,而是你用了一个还没被生态完全接纳的新版本。生产中不用追求最新,稳定优先,这一点一定要想明白。
1.2 发行版选择:OpenJDK、Temurin、Corretto 还是 Oracle JDK
第二个常见困惑是“到底该下哪个”。你打开搜索,出来的有 Oracle JDK、OpenJDK、Eclipse Temurin、Amazon Corretto、Zulu、Liberica 等等,名字一堆,其实绝大多数都是同一套源码编译出来的。Oracle JDK 在许可政策调整后,到了 JDK 17 其实也允许免费用于生产环境,但它的更新策略和商业支持方式仍然让很多人谨慎;OpenJDK 是各个发行版的基础,但“OpenJDK 官网”本身并不直接发放通用安装包,它只是一个代码仓库,所以新手在官网找不到下载入口,就会觉得“java 的 JDK 怎么那么难下载”。
这里我直接给出结论:服务器上不用纠结,选 Eclipse Temurin 就好,它就是原来的 AdoptOpenJDK,由 Eclipse 基金会维护,免费、开源、更新及时,社区用得最广。Amazon Corretto 也不错,特别是你跑在 AWS 上时,它经过 AWS 生产环境大量验证。Oracle JDK 适合需要 Oracle 官方技术支持的企业。你装的 OpenJDK 和你官网下载的 Oracle JDK,在日常业务开发上基本不会有感知差异。
1.3 安装方式:包管理器、tar.gz 压缩包、SDKMAN 各适合什么场景
Linux 下装 JDK 就三条路:
- 包管理器安装(apt / dnf / yum):一条命令装完,系统自动帮你管好路径、注册到 alternatives,适合快速搭环境。缺点是仓库里的版本通常偏老,比如 Ubuntu 默认源里可能还是 openjdk-8 / openjdk-11,装不了最新,或者你想要的某个小版本根本不在源里。
- tar.gz 手动解压安装:自己去下载 JDK 压缩包,解压到指定目录,配置环境变量。听起来麻烦,但这是最可控的方式,生产环境我基本都是这么装。因为你可以精确锁定版本,可以统一规划 JDK 放在哪个目录,以后升级、回滚都清楚。
- SDKMAN 安装:类似 Node 的 nvm,一条命令装任意版本,随时切换。这个在开发机或个人服务器上非常舒服,缺点是不能完全自定义 JDK 目录,不适合需要严格管控的生产服务器。
我在下面的内容里会把这三条路都走一遍,重点讲讲为什么手动解压反而是最值得熟练掌握的方法,以及环境变量配置失败时应该怎么排查。
2. 包管理器安装实操:apt 和 dnf 两条完整命令链条
先说大多数人都能马上上手的包管理器方式。不管你是 Ubuntu / Debian 还是 CentOS / Rocky / Fedora,只要选对命令,几分钟就能装好。
2.1 Ubuntu / Debian 系列用 apt 安装 OpenJDK
先更新索引,然后安装 JDK:
sudo apt update sudo apt install openjdk-17-jdk -y装完验证三件事:java 版本、javac 版本、java 实际所在的路径。
java -version javac -version readlink -f $(which java)readlink -f $(which java)很关键,因为你直接用which java看到的往往是一个符号链接,并不是 JDK 真实路径。Ubuntu 上 apt 安装的 JDK 默认放在/usr/lib/jvm/下面,systemd 和很多脚本都会默认去这个目录找 JVM,这也是很多人配环境变量时 get 不到的点——手动解压时我通常不用这个目录,但 apt 装的时候系统已经帮你规划好了。
2.2 CentOS / Rocky / Fedora 系列用 dnf / yum 安装
CentOS 7 还在用 yum,CentOS 8 以上、Rocky Linux、AlmaLinux、Fedora 都用 dnf,命令参数基本一样:
sudo dnf install java-17-openjdk-devel -y这里有个特别容易踩的坑:CentOS 上有个包叫java-17-openjdk,还有个包叫java-17-openjdk-devel。前者只带 JRE 和基础运行环境,没有 javac 编译器。如果你只是跑别人打好的 jar 包,装前者够了;但你本地要编译代码、要配 IDE、要看javac -version,必须装带-devel后缀的那个。我在给同事排查“java 能用,javac 不存在”这类问题时,十次里有九次是只装了不带 devel 的包。
2.3 包管理器安装的隐藏机制:update-alternatives
apt 和 dnf 安装 JDK 后,系统会自动把java、javac注册到 alternatives 机制里。你可能会在系统里同时装多个版本,比如先装了 openjdk-11,再装 openjdk-17,那默认java命令指向哪个版本?由 alternatives 的优先级决定,一般新装的包优先级更高。
查看当前所有已注册的 JDK:
sudo update-alternatives --config java sudo update-alternatives --config javac进入交互界面,输入对应数字就能切换。这个机制很省心,但它只管java、javac这些系统命令,不会帮你改JAVA_HOME。很多 IDE 和构建工具(Maven、Gradle)启动时认的是JAVA_HOME,不是你 PATH 里的java。所以包管理器装完之后,你依然需要单独配置JAVA_HOME,这点别漏了。
包管理器方式的优点是好上手、卸载干净(直接sudo apt remove openjdk-17-jdk),缺点也非常明显:源里的版本滞后。Ubuntu 20.04 默认源里只有 JDK 8 和 11,你想装 17 得先加 PPA 或者换用手动安装;企业内网如果用的还是老版本系统,这个问题会更突出。所以真正靠谱的,还是手动解压安装。
3. 手动解压 tar.gz 安装,生产环境我最推荐的方案
手动安装步骤不多,但每一步都有讲究。下面我按生产环境标准来写。
3.1 JDK 安装包去哪里下载,以及为什么很多人觉得“下载难”
说句实话,JDK 下载之所以让很多人头疼,主要有三个原因:第一,Oracle JDK 的下载页面动不动要你登录账号,界面交互还绕;第二,OpenJDK 官网不直接提供安装包,你得找对构建商;第三,国内直连某些国外源速度很慢,下载到一半就断了。
我的习惯是去Adoptium(也就是 Eclipse Temurin 的官方发布站)下载,地址就是模型里能直接搜到的那几个常见镜像站之一。如果你是国内服务器,直接用华为云、阿里云、腾讯云的 JDK 镜像,速度快很多,文件名也都清晰标明了版本和架构。
下载前最重要的动作是确认架构:x86_64 的选x64,ARM 服务器的选aarch64。云上现在很多机器是 ARM 架构,下错包会直接报cannot execute binary file: Exec format error。用下面这条命令确认架构:
uname -m3.2 解压与目录规划:为什么统一放 /usr/local/java
我习惯把所有 JDK 放在/usr/local/java下,比如:
sudo mkdir -p /usr/local/java sudo tar -zxvf jdk-17.0.10_linux-x64_bin.tar.gz -C /usr/local/java解压完会生成一个类似jdk-17.0.10的目录。很多教程到这里就配环境变量了,但我还会多做一步——创建一个不带版本号的软链接:
sudo ln -s /usr/local/java/jdk-17.0.10 /usr/local/java/jdk-17这样做的道理很简单:以后升级 JDK 小版本,只要解压新包,改一下软链接的指向,JAVA_HOME和 PATH 都不用动。如果你直接把JAVA_HOME写死到jdk-17.0.10,下次升到jdk-17.0.11就要改一堆配置文件,非常容易漏。这也是手动安装比包管理器更适合生产环境的原因——目录结构完全可控,升级回滚都靠软链接解决。
3.3 手动安装后的环境变量写入方式
这一步是全流程里最值得写清楚的部分。我见太多人直接改/etc/profile,然后 source 一下发现生效了,但换一个终端、换一个用户,又失效了,或者 SSH 登录后环境变量就是不对。
正确做法是在/etc/profile.d/下新建一个独立脚本。/etc/profile在被读取时会遍历/etc/profile.d/*.sh并执行,所以你在那里放脚本,效果等同于直接改/etc/profile,但互相隔离、便于管理:
sudo tee /etc/profile.d/java17.sh > /dev/null <<'EOF' export JAVA_HOME=/usr/local/java/jdk-17 export PATH=$JAVA_HOME/bin:$PATH EOF写完后重新加载:
source /etc/profile.d/java17.sh验证:
echo $JAVA_HOME java -version which java这里有两个容易犯的错误。第一个:写完文件后直接在当前终端source /etc/profile,但当前终端不一定加载/etc/profile.d/java17.sh,要看你当前 shell 是谁、当时加载了哪些文件。第二个:有人把 PATH 配成了export PATH=$JAVA_HOME/bin,把原来 PATH 里的/usr/bin、/usr/local/bin全丢掉了,结果ls、vim等基础命令全部失效,看着像系统崩了一样。记住一定要在后面追加:$PATH。
4. JAVA_HOME 与环境变量配置失败,完整排查链路与根源分析
“jdk环境变量配置失败”能成为热搜,说明这个问题坑人太普遍了。我之前带过一个刚转行做运维的同事,他照着教程在/etc/profile里加了JAVA_HOME,然后自己开了一个新终端,echo $JAVA_HOME居然还是空的,整个人就懵了。这不是他操作不对,而是他弄混了这几个配置文件的加载顺序。
4.1 不同配置文件的生效范围:登录 shell 与非登录 shell
Linux 里有几个环境变量文件,各自的生效范围完全不同:
/etc/profile:系统级配置,登录 shell启动时读取。/etc/profile.d/*.sh:被/etc/profile统一加载,同样只在登录 shell 生效。/etc/environment:系统级环境变量,PAM 登录时就会读取,不区分 shell 类型,连图形界面登录也生效。~/.bashrc:非登录 shell(比如你在终端里再敲 bash 开一个子 shell,或者用 gnome-terminal 开的终端)读取。~/.bash_profile或~/.bash_login:交互式登录 shell读取,内容通常会在结尾去 source.bashrc。
看到没有,坑就藏在“登录”和“非登录”这两个词里。你用 SSH 登录服务器,是登录 shell,会读/etc/profile和~/.bash_profile;你在桌面系统里打开一个终端,这个终端默认是非登录 shell 模式,读的是~/.bashrc。所以你只改/etc/profile,在桌面终端里 source 一下,当前窗口可能有了,但重新打开终端又没了;反过来,你只改了~/.bashrc,用 SSH 登录时又看不到。
我的建议是:系统级 JDK 配置统一放/etc/profile.d/java.sh,用户级配置统一放~/.bashrc。这样不管哪种 shell 都能识别。生产服务器上我基本只用/etc/profile.d/这一种。
4.2 配置完成后不生效,按这个顺序逐层排查
如果你配置完了,echo $JAVA_HOME还是空的,按下面的顺序检查,基本上两三分钟就能定位:
第一步,确认文件内容写对了没有。直接查看文件:
cat /etc/profile.d/java17.sh注意看有没有写错路径、有没有拼错 JAVA_HOME、PATH 那行有没有漏掉$PATH。顺便ls -l /usr/local/java/jdk-17确认软链接存在。
第二步,确认当前 shell 是否重新读取了配置。如果新开的终端还是不行,说明问题大概率在加载环节。手动执行一次:
source /etc/profile.d/java17.sh echo $JAVA_HOME执行完有值,说明配置文件没问题,是 shell 没有自动加载。这种情况一般是你改的是/etc/profile但当前用户的 shell 是/bin/sh而不是 bash,或者你登录方式非常规(比如通过某些工具直接执行命令,没有走完整的 login shell 流程)。
第三步,排查是否被其他脚本覆盖。有时候你明明配好了,但~/.bashrc、~/.bash_profile或者系统其他初始化脚本里又有一个export JAVA_HOME=...指向旧路径,后执行的文件会把前面的覆盖掉。特别常见的是安转了一些软件后,软件的安装脚本自动往.bashrc里写了自己的 JAVA_HOME。对策是把你需要优先生效的配置放在最后面执行。
4.3 配置导致系统命令全部失效:PATH 覆盖事故处理
这个我亲自经历过的场景值得单独写一下。有人配 JDK 环境变量时写成了:
export PATH=$JAVA_HOME/bin然后 source,结果终端突然什么都干不了:ls: command not found,sudo: command not found。原理很简单,PATH 被替换成了 JDK bin 目录,而那里没有ls、没有cat、没有sudo,系统命令全部找不到。
这时候千万别慌,也别去重启终端,因为你必须用绝对路径修复。/usr/bin/ls这种绝对路径不依赖 PATH:
/usr/bin/export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:$JAVA_HOME/bin然后把配置文件改回去就好。日常配置记得加上$PATH前缀,写成export PATH=$JAVA_HOME/bin:$PATH,把 JDK 的命令放在前面,这样java、javac会优先命中你自己装的版本。
5. 多版本 JDK 共存与降级切换:update-alternatives、软链接与项目层面的三板斧
环境变量配好只是第一步。真实工作中,一台服务器同时跑多个项目、多个 JDK 版本的情况太常见了。这里把我的切换思路完整讲一遍。
5.1 项目要求降级到 JDK 17,先判读这台机器属于哪种安装方式
“jdk降级到17”这个搜索词背后通常有两种场景:一种是用包管理器装了 JDK 21,项目报错想换 17;另一种是手动装了新版本,但构建工具还在找旧版本。处理方式取决于你最初是怎么装的。
- 如果是包管理器安装的,直接使用
update-alternatives --config java切换。 - 如果是手动解压安装的,切换软链接指向就行。
- 如果是 SDKMAN 管理的,直接
sdk use java 17.0.10-tem,一条命令搞定。
所以我说安装方式直接影响后续管理成本,这也是为什么我在生产环境坚持手动安装并配合软链接。
5.2 包管理器场景:update-alternatives 切换默认 java 和 javac
假设系统里装了 openjdk-11 和 openjdk-17:
sudo update-alternatives --config java sudo update-alternatives --config javac交互界面里选择你想要的版本编号即可。但这里要特别提醒,update-alternatives 不会同步改 JAVA_HOME。你切了 java 命令,可 Maven 里的 JAVA_HOME 还指向旧的,那就等于白切。正确姿势是两条腿走路:java命令用 alternatives 管,JAVA_HOME用/etc/profile.d/下的脚本管,两边的版本必须对得上。
5.3 手动安装场景:用软链接做版本切换,一分钟切完
手动安装多版本时,我建议这样规划目录:
/usr/local/java/jdk-8 /usr/local/java/jdk-11 /usr/local/java/jdk-17 /usr/local/java/jdk-21 /usr/local/java/current --> jdk-17current指向当前默认版本,JAVA_HOME统一写成/usr/local/java/current。切换版本时,只需要:
sudo rm -f /usr/local/java/current sudo ln -s /usr/local/java/jdk-17 /usr/local/java/current然后重新加载配置:
source /etc/profile.d/java17.sh java -version这样 PATH 和 JAVA_HOME 永远不用改,切来切去就是一两条命令的事。项目要临时用 JDK 8 跑一个 jar,就先切到 8,跑完切回来,干净利落。
5.4 IDE 和构建工具层面的版本:IDEA、Maven、项目 pom 三个地方别打架
服务器配置搞定不等于开发机没问题。很多人配置 IDEA 时,Project SDK 选的是 17,Maven 的 JDK for importer 却是 11,项目编译就一直在报UnsupportedClassVersionError。这类报错的本质是:编译生成的字节码版本高于运行时 JVM 能识别的版本。class 文件有版本号,JVM 只能加载等于或低于自身版本号的文件。
开发机上我建议按这个顺序统一:先项目pom.xml里确认<java.version>,再 IDEA 的 Project Structure 里把 SDK 设为对应版本,最后 Maven Settings 里的 JDK 选同一套。三个地方不一致,构建工具会优先用 Maven 运行时指定的 JDK,而不是你在 IDEA 里选的 SDK。
6. 安装完必做的验证清单与几个高频报错速查
很多人装完java -version能出来就认为搞定了,其实还差得远。我列一个自己每次装 JDK 后都会跑的验证流程,照着走一遍能提前排除掉绝大多数坑。
6.1 从环境变量到实际编译,逐项确认
# 1. 确认 JAVA_HOME 指向有效路径 echo $JAVA_HOME # 2. 确认 PATH 里的 java 是你预期的版本 which java java -version # 3. 确认 javac 也正常 javac -version # 4. 写一个最简单的 HelloWorld,确认编译和运行链路都没问题 cd /tmp cat > Hello.java <<'EOF' public class Hello { public static void main(String[] args) { System.out.println("Hello JDK"); } } EOF javac Hello.java java Hello # 5. 确认 jps、jmap 等 JVM 工具可访问(排查问题时要经常用) jps # 6. 确认当前运行中的 Java 进程启动用的 JDK ps -ef | grep java第 4 步是最容易被忽略的。有人 java 和 javac 都存在,但编译一个带中文注释的文件老是乱码,或者编译报“编码 GBK 的不可映射字符”,这时候还得注意代码文件的编码和编译器参数,纯命令行环境建议用-encoding UTF-8显式指定。
6.2 “java: command not found”与“Could not find or load main class”的区别
很多新手分不清这两个报错,排查方向会跑偏。
java: command not found是 shell 在 PATH 里根本没找到 java 可执行文件。原因要么是 PATH 没配好,要么是java所在目录不在 PATH 中。用echo $PATH看一眼前面 JDK 路径在不在里面;再用ls -l $JAVA_HOME/bin/java确认这个文件是否真实存在且可执行。有时/usr/local/java/jdk-17/bin/java的文件权限不对,执行位没有给到当前用户,也会出现奇怪的行为。
“Could not find or load main class Hello”则是 Java 运行时找不到你要执行的那个类。它跟 JDK 没任何关系,是你编译产物位置不对,或者 classpath 没指对。自己手动跑类时,正好当前目录运行java Hello,如果当前目录不在 classpath 里,把.加进去即可:java -cp . Hello。
6.3 装机常见的“两个版本”幻觉问题
还有一种情况,你明明切到了 JDK 17,java -version也显示 17,但某些项目跑起来还是老版本文本特征。原因通常是进程启动时没有用 PATH 里的 java,而是用了另一个绝对路径。比如 Tomcat 的catalina.sh里能设JAVA_HOME,会优先用你在这个脚本里指定的 JDK;systemd 服务里也可以写Environment=JAVA_HOME=。这种问“java 显示 17,项目为什么还是 JDK 8”的情况,十有八九就是启动脚本里写死了 JDK 路径。碰到时不要光盯着系统环境变量,把服务的启动脚本、systemd unit 文件都翻一遍。
最后分享点我自己的安装习惯
装了这么多年 JDK,我现在最常和同事重申的一点就是:安装本身不是重点,重点是你对它的管理方式。手动安装 + 软链接统一目录 +/etc/profile.d/隔离配置,这套组合让我在几十台服务器上从来没有为 JDK 切换发过愁。别人问我 JDK 装在哪里,我只有一个答案:看/usr/local/java/current指向哪里。别人问我怎么把 JDK 从 25 降到 17,我让他们看一眼软链接三秒钟内就解决了。
如果你刚开始接触 Linux 上的 JDK 安装,建议先把包管理器方式和手动解压方式各实践一遍,然后自己亲手制造一次环境变量配置失败,再按我上面的排查链路把它修好。这个过程走完,你再去应对任何一台 Linux 服务器上的 Java 环境问题,都会从容很多。