Linux 安装 Oracle JDK

摘要

发布节奏

正式环境用 LTS。本文安装的 25.0.5 仍在 NFTC 免费商用窗口内,这个窗口到 2028 年 9 月。

Oracle 当前的节奏:

  • 每 6 个月一个功能版本,3 月和 9 月。功能版本同时带上当季的安全修复。

  • 每 3 个月一次更新版本,1 月、4 月、7 月、10 月。

  • 每 2 年一个 LTS,落在 9 月。已经发布的 LTS 是 8、11、17、21、25。下一个计划是 2027 年 9 月的 JDK 29,再下一个是 2029 年 9 月的 JDK 33。29 和 33 的日期 Oracle 标明可能调整。

非 LTS 在下一个功能版本发布后即被取代。它的 Premier Support 只覆盖到那时,没有 Extended Support。Oracle JDK 的非 LTS 在这大约 6 个月里按 NFTC。

发布日历见 Java Releases。

许可证

从 2021 年 9 月的 JDK 17 起,Oracle 对当时的当前 LTS 使用 NFTC(No-Fee Terms and Conditions)。NFTC 允许任何用户使用,包括商业和生产;可以再分发,再分发不能收费。

免费窗口收到下一个 LTS 发布之后的一年。按现在每两年一个 LTS 的节奏,这段时间是 3 年。窗口结束后的更新改为 OTN(Oracle Technology Network License)。OTN 允许个人使用、开发、测试、原型和演示。生产环境里的商业使用需要 Java SE Universal Subscription。

已经下载的那一版,继续按下载当时的许可证使用。换上窗口结束后的更新包,才适用 OTN。

Premier Support 和 Extended Support 是订阅用户还能拿到补丁的期限。它和 NFTC 的免费商用窗口是两件事。JDK 17 的免费商用收到 17.0.12,Extended Support 到 2029 年 9 月;到 2026 年 11 月,JDK 17 已经过了 Premier Support,后续补丁落在 Extended Support。

版本 发布 NFTC 免费商用截止 Premier Support Extended Support
8 2014-03 无。8u202(2019-01)及更早按当时的 BCL;8u211(2019-04)起按 OTN 2022-03 2030-12
11 2018-09 无。Oracle JDK 11 全系列按 OTN。Oracle 构建的 OpenJDK 提供到 11.0.2(2019-01),许可证是 GPLv2+CPE 2023-09 2032-01
17 2021-09 17.0.12 及更早(窗口到 2024-09)。17.0.13 起按 OTN 2026-09 2029-09
21 2023-09 21.0.12 及更早(窗口到 2026-09)。21.0.13 起按 OTN 2028-09 2031-09
25 2025-09 2028-09。按季度节奏,2028-07 的 25.0.12 仍在窗口内,2028-10 的更新起按 OTN 2030-09 2033-09
29 2027-09(计划) 2030-09(计划,下一个 LTS 是 2029-09 的 JDK 33) 2032-09(计划) 2035-09(计划)

下载

当前版本和历史版本都在 Oracle 的下载页。本文用的是 JDK 25 的 Linux x64 Compressed Archive,文件名是 jdk-25.0.5_linux-x64_bin.tar.gz。aarch64 把文件名里的 x64 换成 aarch64,解压后的目录名不变。

同页上的 Oracle OpenJDK 是另一套构建,许可证是 GPLv2+CPE,不在下面的安装步骤里。

从 class 文件看编译版本

class 文件的主版本号等于 44 + 功能版本号。JDK 8 是 52,JDK 11 是 55,JDK 25 是 69。这个号是编译目标(--release / -target),记录的是这份字节码要求的最低 JDK。

本机有任意一个 JDK 的 javap 就能读。它只打印文件里的版本,不执行这个类。

1
2
# 读一个 class 的主版本号
javap -verbose App.class | grep "major version"

jar 里的 class 可以彼此版本不同。下面只查你点名的那一个类,类名换成 jar tf 列出的路径,并去掉 .class。

1
2
jar tf app.jar | grep '\.class$'
javap -classpath app.jar -verbose com.example.App | grep "major version"

MANIFEST.MF 里的 Build-Jdk 或 Created-By 是打包时用的 JDK,和 class 的编译目标可以不同。war 的清单在包根上的 META-INF/MANIFEST.MF。

1
2
unzip -p app.jar META-INF/MANIFEST.MF | grep -E '^(Build-Jdk|Created-By)'
unzip -p app.war META-INF/MANIFEST.MF | grep -E '^(Build-Jdk|Created-By)'
主版本号 JDK
45 1.1
46 1.2
47 1.3
48 1.4
49 1.5
50 1.6
51 1.7
52 8,LTS
53 9
54 10
55 11,LTS
56 12
57 13
58 14
59 15
60 16
61 17,LTS
62 18
63 19
64 20
65 21,LTS
66 22
67 23
68 24
69 25,LTS
70 26
71 27
72 28
73 29,LTS,计划于 2027-09

安装

下面把发行目录固定在 /usr/local/jvm/jdk。tar.gz 解压出来的是带版本号的一层,例如 jdk-25.0.5。用符号链接指过去之后,升级时只改链接,JAVA_HOME 不用改。

1
2
3
4
# 解压到 /usr/local/jvm。顶层目录名以 tar 实际解出的为准
sudo mkdir -p /usr/local/jvm
sudo tar -xzf jdk-25.0.5_linux-x64_bin.tar.gz -C /usr/local/jvm
ls /usr/local/jvm

/usr/local/jvm/jdk 已经是普通目录、并且里面直接有 bin/java 时,环境变量就用这个目录,下面的链接不用做。

解压出来的是 jdk-25.0.5 这一层时,再用符号链接固定路径。升级时只改链接。ln -sfn 的目标如果已经是普通目录,链接会进到该目录里面,所以这一步只在 /usr/local/jvm/jdk 不存在、或它本身已是符号链接时执行。

1
2
# 目录名不同时,改成上面 ls 看到的那一层
sudo ln -sfn /usr/local/jvm/jdk-25.0.5 /usr/local/jvm/jdk

能输出版本号,说明这个路径上的 java 可以执行。当前 shell 要等后面 source 之后,才会从 PATH 里找到它。

1
/usr/local/jvm/jdk/bin/java -version

25.0.5 的第一行类似 java version "25.0.5"。

环境变量

JAVA_HOME 指向发行目录本身,也就是里面直接有 bin/java 的那一层。PATH 把 $JAVA_HOME/bin 放在最前面,当前 shell 会先找到这一份。

$JAVA_HOME/bin 放在 PATH 最前面之后,会盖住系统里已经安装的 java。包管理器装的那一份还在,当前 shell 只是先用到 /usr/local/jvm/jdk。

所有用户的登录 shell

/etc/profile 在登录时 source /etc/profile.d/*.sh。这个文件被 source,需要可读。ssh host 命令、systemd、cron 不读它。

下面的 tee 会覆盖已有的 /etc/profile.d/jdk.sh。文件里如果写过别的变量,先把内容拷出来。

1
2
3
4
5
6
# 单引号 EOF,避免当前 shell 把 $JAVA_HOME 提前展开
sudo tee /etc/profile.d/jdk.sh >/dev/null <<'EOF'
export JAVA_HOME=/usr/local/jvm/jdk
export PATH="$JAVA_HOME/bin:$PATH"
EOF
sudo chmod 644 /etc/profile.d/jdk.sh

写入文件之后,已经打开的 shell 不会变。让当前这个登录 shell 读入:

1
source /etc/profile.d/jdk.sh

新开的登录 shell 会自己读 /etc/profile,不用再 source。

只给当前用户

交互式 bash 读 ~/.bashrc。只影响这一个用户时,把同样两行追加到这个文件。已经有这段时不要再追加,否则每次打开 shell,PATH 都会再被加上一遍。

1
2
3
4
5
grep -q 'JAVA_HOME=/usr/local/jvm/jdk' ~/.bashrc || cat >> ~/.bashrc <<'EOF'
export JAVA_HOME=/usr/local/jvm/jdk
export PATH="$JAVA_HOME/bin:$PATH"
EOF
source ~/.bashrc

/etc/profile.d/jdk.sh 和 ~/.bashrc 都设置了 JAVA_HOME 时,后被 source 的那个生效。登录 shell 先读 /etc/profile,再读用户的 bashrc。两处要写同一个路径。

确认已经生效

1
2
3
echo "$JAVA_HOME"
java -version
javac -version

三者分别应是:

1
2
3
/usr/local/jvm/jdk
java version "25.0.5" ...
javac 25.0.5

java -version 能跑,只说明 PATH 上有一个 java。echo "$JAVA_HOME" 为空时,依赖 JAVA_HOME 的程序仍然找不到安装目录。两条都要对。

升级

下一次更新把版本号换成新包的版本。新的 java -version 正确之后,再删上一版的目录。

1
2
3
4
5
# 文件名和目录名换成这次下载的版本。25.0.6 是 25.0.5 的下一档更新
sudo tar -xzf jdk-25.0.6_linux-x64_bin.tar.gz -C /usr/local/jvm
sudo ln -sfn /usr/local/jvm/jdk-25.0.6 /usr/local/jvm/jdk
hash -r
/usr/local/jvm/jdk/bin/java -version

JAVA_HOME 仍是 /usr/local/jvm/jdk。已经打开的 shell 要重新 source,或者新开一个终端。hash -r 清掉 bash 记住的 java 路径。

换上 2028 年 10 月及以后的 JDK 25 更新之前,先看上一节的 OTN 窗口。那之后的包做商业生产,需要订阅。

卸掉

从 ~/.bashrc 里删掉 JAVA_HOME 和对应的 PATH 两行,再删系统文件、符号链接和解压目录。

1
2
3
4
sudo rm -f /etc/profile.d/jdk.sh
# jdk 是符号链接时,rm -f 只删链接,解压目录还在
sudo rm -f /usr/local/jvm/jdk
sudo rm -rf /usr/local/jvm/jdk-25.0.5

/usr/local/jvm/jdk 本身就是解压出来的目录时,先 ls -ld /usr/local/jvm/jdk 确认,再 sudo rm -rf /usr/local/jvm/jdk。

新开一个 shell,确认当前用户已经不再落到这份 JDK 上:

1
2
3
hash -r
echo "$JAVA_HOME"
type -a java

JAVA_HOME 应为空。type -a java 如果还有输出,那是系统里另一份 JDK,和这次解压的目录无关。/usr/local/jvm 已经空了的话,可以 sudo rmdir /usr/local/jvm。

排错

版本仍是旧的时,先看当前 shell 到底找到哪一个文件:

1
2
3
type -a java
echo "$PATH"
ls -l /usr/local/jvm/jdk/bin/java

type -a java 的第一行应是 /usr/local/jvm/jdk/bin/java。第一行是别的路径时,$JAVA_HOME/bin 不在 PATH 前面,或者这个 shell 还没 source。bash 如果记住了旧路径,执行 hash -r 后再看 java -version。

ls 报没有 bin/java 时,JAVA_HOME 多指了一层,或少指了一层。它必须是 bin 的上一级。符号链接用 ls -ld /usr/local/jvm/jdk 看它指向哪里。

ls -l 里这个文件没有执行位时,当前用户跑不了它。发行包解压后,bin/java 一般是所有用户可执行。

systemd 服务、桌面启动器不读 /etc/profile.d。单元里写绝对路径,并单独给进程 JAVA_HOME:

1
2
3
[Service]
Environment=JAVA_HOME=/usr/local/jvm/jdk
ExecStart=/usr/local/jvm/jdk/bin/java -jar /opt/app/app.jar