Ubuntu 26.04 and 24.04 both carry OpenJDK 21 in their own archive. One apt install gets you a patched Java 21 that updates with the rest of the system, and the part that bites people comes later: on 26.04 the default Java is 25, and the moment anything pulls in default-jdk, your java command quietly stops being 21. Oracle’s .deb does the same thing even harder, and Oracle’s free license for Java 21 updates ended with the September 2026 cutoff.
This guide shows you how to install Java 21 on Ubuntu from the Ubuntu repository, with Eclipse Temurin 21 and Oracle JDK 21 as alternatives. It then switches JDKs without splitting java from javac, sets a JAVA_HOME that follows the switch, and tests the result with a program that only compiles on 21. We ran the install, switching and test steps below in October 2026 on Ubuntu 26.04 and on 24.04, with OpenJDK 21.0.12 from the Ubuntu archive, Temurin 21.0.12 and Oracle JDK 21.0.12.
Pick a Java 21 build
All three are builds of the same OpenJDK 21 source and run the same bytecode, so the choice comes down to who patches it and under what license. For a server, take the Ubuntu package unless you have a reason not to, because its security fixes arrive through the same apt upgrade you already run.
| Build | Where it comes from | Updates | Install path | License |
|---|---|---|---|---|
| OpenJDK 21 (Ubuntu) | Ubuntu archive (universe on 26.04, main on 24.04) | apt, via the security pocket | /usr/lib/jvm/java-21-openjdk-amd64 | GPLv2 with Classpath Exception |
| Eclipse Temurin 21 | Adoptium apt repository | apt, from the Adoptium repository | /usr/lib/jvm/temurin-21-jdk-amd64 | GPLv2 with Classpath Exception |
| Oracle JDK 21 | .deb from oracle.com | manual; new 21 updates need an Oracle sign-in | /usr/lib/jvm/jdk-21.0.12.1-oracle-x64 (changes per update) | NFTC up to September 2026 updates, OTN after |
Temurin is the right call when your team runs the same JDK on Ubuntu, Rocky, macOS and Windows and wants one vendor everywhere, because the build string and the patch level then match on every machine. Oracle JDK only makes sense if a vendor contract or a support agreement names it, and the licensing section below explains why. If neither applies to you, stop after the first install section.
Prerequisites
The steps need very little, and nothing in them is specific to a desktop or a server install:
- Ubuntu 26.04 LTS or 24.04 LTS, desktop or server (we tested on amd64)
- A user with sudo rights
- Free disk: up to 685 MB for the full OpenJDK 21 JDK on a minimal server, about 310 MB for the headless JDK, and about 350 MB for each extra JDK you keep
- Outbound HTTPS if you add the Adoptium repository or download Oracle’s .deb
Install OpenJDK 21 from the Ubuntu repository
Refresh the package index first, so apt picks the latest security build rather than whatever the index cached at install time:
sudo apt update
Now install the full JDK. It gives you java, the javac compiler, jar, jshell and the rest of the tools:
sudo apt install -y openjdk-21-jdk
On a minimal server, that package is heavier than it looks. On our fresh 26.04 server image, openjdk-21-jdk pulled in 134 new packages and 575 MB, most of it X11, ALSA and font libraries for AWT and Swing. The headless JDK needed 22 packages and 312 MB, and the headless runtime 21 packages and 215 MB. Our 24.04 server showed the same split: 685 MB for the full JDK against 305 MB headless. If the box only builds or runs services, and never draws a window, install the headless variant instead:
sudo apt install -y openjdk-21-jdk-headless
A machine that only runs a packaged application, such as a Tomcat or Jenkins host, does not need the compiler at all, so openjdk-21-jre-headless is enough there. Pick one of the three, because the full JDK already depends on the headless one and installing both changes nothing. You can add the GUI libraries later without reinstalling Java if a desktop tool ever needs them.
Verify the Java 21 install
Check the runtime and the compiler separately, because they’re managed as two different alternatives and can point at different JDKs on a box that has more than one:
java -version
javac -version
On Ubuntu 26.04 the build string carries 26.04.4-Ubuntu, which tells you the binary came from the Ubuntu archive and not from another vendor:
openjdk version "21.0.12.1" 2026-08-18
OpenJDK Runtime Environment (build 21.0.12.1+1-1-26.04.4-Ubuntu)
OpenJDK 64-Bit Server VM (build 21.0.12.1+1-1-26.04.4-Ubuntu, mixed mode, sharing)
javac 21.0.12.1
On Ubuntu 24.04 you get the same Java 21 update, built for noble:
openjdk version "21.0.12.1" 2026-08-18
OpenJDK Runtime Environment (build 21.0.12.1+1-1-24.04.4-Ubuntu)
OpenJDK 64-Bit Server VM (build 21.0.12.1+1-1-24.04.4-Ubuntu, mixed mode, sharing)
javac 21.0.12.1
Both releases ship the same quarterly update, so moving a service between them does not change the JVM underneath it. The JDK itself lives under /usr/lib/jvm/java-21-openjdk-amd64, which is the path to use anywhere a tool asks for a Java home. That path stays the same across updates because the directory name carries only the major version.
Install Eclipse Temurin 21 instead
Adoptium publishes its own apt repository with a resolute and a noble suite, so Temurin installs and updates the same way as the Ubuntu package. Install the tools the repository setup needs:
sudo apt install -y wget apt-transport-https gpg
Import the Adoptium signing key, then add the repository for your release. The awk part reads the codename from /etc/os-release, so the same line works on 26.04 and 24.04:
wget -qO - https://packages.adoptium.net/artifactory/api/gpg/key/public | gpg --dearmor | sudo tee /etc/apt/trusted.gpg.d/adoptium.gpg > /dev/null
echo "deb https://packages.adoptium.net/artifactory/deb $(awk -F= '/^VERSION_CODENAME/{print$2}' /etc/os-release) main" | sudo tee /etc/apt/sources.list.d/adoptium.list
Update the index and install the Temurin 21 JDK:
sudo apt update
sudo apt install -y temurin-21-jdk
Adoptium publishes temurin-21-jdk and temurin-21-jre but no headless variant, and the JDK depends on the X11, ALSA and fontconfig libraries. On a clean 26.04 server it pulled in 18 packages and 380 MB, more than Ubuntu’s headless JDK (312 MB) but well under the full one (575 MB), and the JDK directory itself is 346 MB. Unlike Ubuntu, there is no headless variant to skip the desktop libraries. It reports its vendor in the runtime line:
/usr/lib/jvm/temurin-21-jdk-amd64/bin/java -version
Look for Temurin in the second line.
openjdk version "21.0.12.1" 2026-08-18 LTS
OpenJDK Runtime Environment Temurin-21.0.12.1+1 (build 21.0.12.1+1-LTS)
OpenJDK 64-Bit Server VM Temurin-21.0.12.1+1 (build 21.0.12.1+1-LTS, mixed mode, sharing)
We called the binary by its full path on purpose. Temurin registers with the same priority as Ubuntu’s OpenJDK 21, so installing it next to the Ubuntu package does not move /usr/bin/java. To make it the default, use the switch shown further down.
Install Oracle JDK 21
Read the license first. Oracle’s download page says JDK 21 updates were free under the No-Fee Terms and Conditions (NFTC) until September 2026. Later JDK 21 updates are licensed under the Java SE OTN License, and production use beyond its limited free grants requires a fee. The current download is still the last NFTC build, but the next security fix for Oracle JDK 21 will not be free for production. If you just need Java 21, the Ubuntu or Temurin build above gets the same fixes without that question.
If a vendor still requires Oracle’s build, download the last NFTC release from Oracle’s archive. Don’t rely on the /java/21/latest/ link: OTN-licensed updates need an Oracle account sign-in and are not published there, and JDK 17’s equivalent latest URL already returns 404.
wget https://download.oracle.com/java/21/archive/jdk-21.0.12.1_linux-x64_bin.deb #https://www.oracle.com/java/technologies/javase/jdk21-archive-downloads.html
Install it with apt, giving the local file path so apt resolves it like any other package:
sudo apt install -y ./jdk-21.0.12.1_linux-x64_bin.deb
Now look at what it did to the java alternative, because this is the part Oracle’s install notes don’t mention:
update-alternatives --display java | head -3
Oracle’s package registers with priority 352420864 against 2111 for Ubuntu’s OpenJDK 21 and Temurin, so in auto mode it takes over /usr/bin/java the moment it lands:
java - auto mode
link best version is /usr/lib/jvm/jdk-21.0.12.1-oracle-x64/bin/java
link currently points to /usr/lib/jvm/jdk-21.0.12.1-oracle-x64/bin/java
That is fine if Oracle’s build is what you wanted. If you only installed it to test something, every Java service on the box will run on Oracle JDK at its next restart, because they all start through /usr/bin/java. Either purge the package with sudo apt purge jdk-21 when you’re done, or pin the default as shown next.
Switch between Java versions
A box with more than one JDK picks the one with the highest priority unless you set a choice yourself. On 26.04 that matters even when you never asked for a second JDK: default-jdk and default-jre depend on OpenJDK 25, and plenty of packages pull them in. With both installed, Java 25 wins auto mode (priority 2511 against 2111), so java runs 25 even though your app was built for 21. On 24.04 default-jdk still depends on OpenJDK 21, so this trap is new with 26.04.
List what’s installed, using the names update-java-alternatives understands:
update-java-alternatives --list
Our 26.04 lab had all four builds side by side:
java-1.21.0-openjdk-amd64 2111 /usr/lib/jvm/java-1.21.0-openjdk-amd64
java-1.25.0-openjdk-amd64 2511 /usr/lib/jvm/java-1.25.0-openjdk-amd64
jdk-21.0.12.1-oracle-x64 352420864 /usr/lib/jvm/jdk-21.0.12.1-oracle-x64
temurin-21-jdk-amd64 2111 /usr/lib/jvm/temurin-21-jdk-amd64
Use update-java-alternatives rather than update-alternatives --config java, because the second one only moves java. When we switched that way to Java 25, java -version said 25 while javac -version still said 21. That is how a build ends up compiling for one release and running on another. update-java-alternatives reads the JDK’s .jinfo file and moves every tool in it together, including:
javaandjavacjarandjshellkeytool, which matters for TLS truststores
Set the Ubuntu OpenJDK 21 as the default for the whole system:
sudo update-java-alternatives --set java-1.21.0-openjdk-amd64
Swap in temurin-21-jdk-amd64 or the Oracle name from the list to pick those instead. The Oracle name includes the full update number, so a script that sets it by name breaks after each Oracle update. Setting a JDK this way puts every tool in manual mode, so a later install of a higher-priority JDK no longer takes over. We checked: after pinning OpenJDK 21 on 24.04, installing Oracle’s package left java on OpenJDK. Confirm both tools agree:
java -version 2>&1 | head -1; javac -version
Both lines should name the same Java 21 update.
openjdk version "21.0.12.1" 2026-08-18
javac 21.0.12.1
Switching to a headless JDK prints a warning such as jdk alternative does not exist: /usr/lib/jvm/java-25-openjdk-amd64/bin/jconsole. It is harmless, because headless builds leave out the GUI tools, and every command-line tool still switches. The default Java version guide covers the same mechanism on Debian.
Set JAVA_HOME
Maven, Gradle and most Java service wrappers read JAVA_HOME rather than /usr/bin/java, and a hardcoded path goes stale the moment you switch JDKs. Derive it from the current javac instead, so it follows update-java-alternatives:
echo 'export JAVA_HOME=$(dirname $(dirname $(readlink -f /usr/bin/javac)))' | sudo tee /etc/profile.d/java-home.sh
Files in /etc/profile.d are read by login shells, so open a new login shell (or log out and back in) and check the value:
bash -lc 'echo $JAVA_HOME'
It prints the JDK that javac resolves to right now:
/usr/lib/jvm/java-21-openjdk-amd64
After sudo update-java-alternatives --set temurin-21-jdk-amd64, the same login shell printed /usr/lib/jvm/temurin-21-jdk-amd64 with no edit to the file. The line points at javac on purpose, because a runtime-only install has no javac; on a JRE-only box, change it to /usr/bin/java.
systemd services do not read /etc/profile.d. We checked with systemd-run and the service environment had an empty JAVA_HOME, so a unit that needs it must set it itself. Add it to the unit with sudo systemctl edit <service>:
[Service]
Environment=JAVA_HOME=/usr/lib/jvm/java-21-openjdk-amd64
A unit is the one place where a fixed path is the right choice, and Temurin or Oracle users put their own JDK path there. It only pins the Java version if the service’s launcher reads JAVA_HOME: a unit whose ExecStart calls /usr/bin/java directly ignores it, so point ExecStart at /usr/lib/jvm/java-21-openjdk-amd64/bin/java in that case. Run sudo systemctl restart <service> afterwards so the new environment takes effect.
Test Java 21 with a sample program
A hello-world proves the binary starts, not that you’re on 21. This program uses two features that became final in Java 21, record patterns in switch and virtual threads, so it only compiles on 21 or newer. Create the file:
vim Hello.java
Paste this in, then save the file.
import java.util.concurrent.Executors;
import java.util.stream.IntStream;
public class Hello {
record Point(int x, int y) {}
static String describe(Object o) {
return switch (o) {
case Point(int x, int y) when x == y -> "a point on the diagonal at " + x;
case Point(int x, int y) -> "a point at " + x + "," + y;
default -> "something else";
};
}
public static void main(String[] args) {
System.out.println("Running on Java " + Runtime.version() + " (" + System.getProperty("java.vendor") + ")");
System.out.println("Record pattern says: " + describe(new Point(3, 3)));
long start = System.nanoTime();
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
IntStream.range(0, 10_000).forEach(i -> executor.submit(() -> {
Thread.sleep(1000);
return i;
}));
}
long ms = (System.nanoTime() - start) / 1_000_000;
System.out.println("10,000 virtual threads each slept 1 s, total " + ms + " ms");
}
}
Java 11 and newer can run a single source file directly, so there is no separate compile step for a quick test.
java Hello.java
Ten thousand threads that each sleep for a second finish in a little over one second, because virtual threads park instead of holding an OS thread:
Running on Java 21.0.12.1+1-1-26.04.4-Ubuntu (Ubuntu)
Record pattern says: a point on the diagonal at 3
10,000 virtual threads each slept 1 s, total 1109 ms
The vendor in brackets tells you which build ran: Ubuntu for the archive package, Eclipse Adoptium for Temurin and Oracle Corporation for Oracle JDK. The same run on our 24.04 box took 1142 ms. The screenshot shows the whole check on the 26.04 server, from the release string to the test run:

To see why the version matters, ask the compiler to target Java 17:
javac --release 17 -d /tmp/c17 Hello.java
It refuses at the first Java 21 feature. This is the error a Java 21 compiler prints when the build asks for a lower language level than the code uses:
Hello.java:9: error: patterns in switch statements are not supported in -source 17
case Point(int x, int y) when x == y -> "a point on the diagonal at " + x;
^
(use -source 21 or higher to enable patterns in switch statements)
If you see it in a Maven build, the JDK is fine and the project’s release setting is too low, so raise maven.compiler.release (or the Gradle toolchain version) to 21. A build that really runs on an older JDK fails differently, because that compiler doesn’t know release 21 at all. The Maven install guide shows how to check which JDK Maven picked up.
Remove a JDK you no longer need
Removing a JDK hands /usr/bin/java to the next highest priority, which may not be the one you expect. After we removed Oracle’s package on a 26.04 box that also had Java 25, auto mode moved java to 25, not to OpenJDK 21. Use purge rather than remove, for the reason in the next section:
sudo apt purge jdk-21
sudo apt purge temurin-21-jdk openjdk-25-jdk-headless
sudo apt autoremove
Leave out any package you never installed, because apt stops on an unknown name, and read the removal list before you answer yes. If Java 25 came in through default-jdk, purging it also removes default-jdk and anything that depends on it, so pinning 21 with update-java-alternatives is the better fix there. Then set the JDK you’re keeping explicitly with sudo update-java-alternatives --set java-1.21.0-openjdk-amd64 and check java -version again. To drop the Adoptium repository as well, delete /etc/apt/sources.list.d/adoptium.list and /etc/apt/trusted.gpg.d/adoptium.gpg.
Error: could not open `/usr/lib/jvm/jdk-21.0.12.1-oracle-x64/lib/jvm.cfg’
This is what Oracle JDK prints when you reinstall its .deb after an apt remove. Oracle’s package marks lib/jvm.cfg as a configuration file, and its removal script then deletes the whole install directory. On the next install, dpkg treats the missing config file as one you deleted on purpose and doesn’t put it back, so the JDK is installed but cannot start. We hit it on 24.04 with the current Oracle build:
Error: could not open `/usr/lib/jvm/jdk-21.0.12.1-oracle-x64/lib/jvm.cfg'
Purge the package so dpkg forgets its config files, then install the .deb again:
sudo apt purge -y jdk-21
sudo apt install -y ./jdk-21.0.12.1_linux-x64_bin.deb
After the purge and reinstall, jvm.cfg was back and the sample program ran with Oracle Corporation as the vendor. Removing Oracle JDK with purge from the start avoids the problem entirely.
Keep Java 21 patched
Java ships security fixes every quarter, on the Oracle Critical Patch Update dates in January, April, July and October. How a fix reaches your box depends on the build you picked:
- Ubuntu OpenJDK 21: the security pocket, which
unattended-upgradesinstalls by default - Temurin 21: the Adoptium repository, on your next
apt upgrade - Oracle JDK 21: a manual download behind an Oracle sign-in, under the OTN license
Only the first one patches itself without you doing anything, which is the main reason it is our default for servers. Temurin is close behind, because it only needs the apt upgrade you already run. Oracle’s build is the one that goes stale quietly on a box nobody remembers.
A JVM keeps running the old code until you restart it, so a patched package isn’t a patched service. On Ubuntu Server, needrestart comes with the ubuntu-server package, and since 24.04 it restarts affected services by default after apt runs, so expect your Java services to restart when a Java update lands. On a desktop install without it, restart them yourself. For a server, our verdict is short: run Ubuntu’s openjdk-21-jdk-headless, pin it with update-java-alternatives so default-jdk can’t swap it out, and set JAVA_HOME in each unit. If you need Java 25 rather than 21, the OpenJDK guide for Ubuntu 26.04 covers it, and the Debian Java guide uses the same packages. For Red Hat family servers, the Java 21 guide for Rocky and AlmaLinux is the equivalent, and a Java 21 box is ready for the Jenkins install.