Fixing the Java error "Unsupported class file major version"
The number in the error tells you everything: 52 is Java 8, 55 is Java 11, 61 is Java 17, 65 is Java 21. Here is how to find out which Java is really running and how to activate the right version.
You start a Minecraft server, a plugin or a build tool, and instead of the expected startup the terminal shows a number that says nothing at first glance: class file version 65.0. The good news is that this number already contains the complete diagnosis. You only have to know how to read it. This article shows how to derive the required Java version from that number, how to find out which Java is actually running on your server (often not the one you assume), and how to activate the correct version permanently.
Two different error messages, two different causes
The exact wording matters, because the two common messages hide exactly opposite problems. The first one comes from the Java Virtual Machine itself:
Error: LinkageError occurred while loading main class net.minecraft.bundler.Main
java.lang.UnsupportedClassVersionError: net/minecraft/bundler/Main has been compiled
by a more recent version of the Java Runtime (class file version 65.0), this version
of the Java Runtime only recognizes class file versions up to 61.0
This message always means the same thing: the application was built with a newer Java than the one you have installed. The first number is what the application brings along, the second number is what your runtime understands. In the example above the software wants Java 21, while Java 17 is installed.
The second variant looks similar, but it comes from a different corner:
java.lang.IllegalArgumentException: Unsupported class file major version 65
This short form is not thrown by the JVM but by a library that reads and inspects bytecode, usually ASM. It shows up with Gradle, with old plugin loaders and with older server cores. Here the situation is normally reversed: your Java is too new for the software that wants to read the bytecode. Anyone who confuses the two messages installs in the wrong direction and then wonders why the error stays. As a rule of thumb: if the long sentence with has been compiled by a more recent version is there, you need a newer Java. If only the short sentence is there, you probably need an older one.
Decoding the number: major version minus 44
The conversion is simpler than most guides make it look. The class file major version minus 44 gives you the Java version. 65 minus 44 is 21, done. The rule holds without a single gap from Java 1.1 onwards, and it will not change in the future either, because every new major Java release raises the number by exactly one.
| Class file version | Java version | Typically found in |
|---|---|---|
| 52 | Java 8 | Minecraft up to 1.16.5, old Forge modpacks, older enterprise software |
| 53 | Java 9 | rare, transitional release |
| 55 | Java 11 | many libraries, older Spring applications |
| 60 | Java 16 | Minecraft 1.17.x |
| 61 | Java 17 | Minecraft 1.18 to 1.20.4, a great many current plugins |
| 65 | Java 21 | Minecraft from 1.20.5 on, so the entire 1.21 line, modern server cores |
| 69 | Java 25 | brand new builds, current LTS release |
The values in between follow the same rule: 62 is Java 18, 63 is Java 19, 64 is Java 20, 66 is Java 22 and so on. The .0 behind the number is the minor version and is practically never relevant.
What this means for Minecraft in practice
The mapping for Minecraft servers is unambiguous and easy to memorize. Up to and including 1.16.5 it is Java 8, for 1.17.x at least Java 16, from 1.18 to 1.20.4 at least Java 17, and from 1.20.5 on (which covers the entire 1.21 series) Java 21. So if class file version 65.0 turns up in the log right after you updated to a current release, you have found the cause before you even look at the configuration.
Which Java is really running?
The most common mistake with this class of error is assuming that the output of java -version in your SSH session also applies to the running service. That is often not true. Start here anyway:
java -version
The first line of the output names the version, for example openjdk version "21.0.11" 2026-04-15. If you get bash: java: command not found instead, there is no Java in the search path at all, and the application is being started by a start script with an absolute path. Check which runtimes are installed:
ls /usr/lib/jvm
readlink -f "$(command -v java)"
update-alternatives --display java
readlink -f resolves the symlink chain and shows you the binary that is actually executed, for example /usr/lib/jvm/java-17-openjdk-amd64/bin/java. That matters more than the version number alone, because you will need this path later on.
The middle line deliberately uses command -v and not the more widespread which. which is a separate program, and it is not part of the minimal installation of AlmaLinux 9 and 10, Rocky Linux 9 and Oracle Linux 9. There the which variant fails twice: first which: command not found, then readlink: missing operand. command -v is built into the shell itself and works everywhere.
For a process that is already running there is a way that leaves no room for doubt. It answers the question of which Java the service was really started with, regardless of search path and environment variables:
ls -l /proc/$(pgrep -f server.jar | head -n 1)/exe
The exe symlink points to the executable file of the process. If it shows a different path than expected, you have found the cause: the service starts with a different Java than your shell. This happens regularly with systemd units, because they bring their own minimal environment and your JAVA_HOME from .bashrc simply does not exist there.
Reading the class version straight from the JAR file
Sometimes you want to know which Java a file requires before you even start it. That works without installing any additional tools. Every .class file begins with the signature CAFEBABE, followed by two bytes of minor version and two bytes of major version. So the eighth byte is the number you are looking for:
unzip -p server.jar net/minecraft/bundler/Main.class | od -An -tu1 -N8
The output then reads something like 202 254 186 190 0 0 0 65. The first four numbers are the signature, the last one is your answer: 65, so Java 21. For other programs you replace the class path accordingly, and the matching name comes from unzip -l server.jar or from the Main-Class entry in META-INF/MANIFEST.MF. If a JDK is installed, there is a more convenient way:
javap -verbose -cp server.jar net.minecraft.bundler.Main | grep major
This trick is particularly useful with plugins. If a single plugin aborts the server start, check its main class and you know immediately whether the plugin is too new for your server core or the other way round.
Installing the matching Java version
This is where the distributions differ sharply, and where blanket instructions fall apart. The state of real systems, verified in July 2026:
| Package | Debian 13 | Debian 12 | Ubuntu 24.04 | Ubuntu 22.04 |
|---|---|---|---|---|
| openjdk-8-jre-headless | missing | missing | 8u492 | 8u492 |
| openjdk-11-jre-headless | missing | missing | 11.0.31 | 11.0.31 |
| openjdk-17-jre-headless | missing | 17.0.19 | 17.0.19 | 17.0.19 |
| openjdk-21-jre-headless | 21.0.11 | missing | 21.0.11 | 21.0.11 |
| openjdk-25-jre-headless | 25.0.3 | missing | 25.0.3 | 25.0.3 |
Read this table once in peace, it will save you a lot of time. Debian 12 knows Java 17 and nothing else, Debian 13 does not know Java 17 any more and only ships 21 and 25. So if you want to run a 1.21 Minecraft server on Debian 12, or a program with class file version 61.0 on Debian 13, the standard repositories have nothing suitable for you. Ubuntu is more generous here and delivers everything from Java 8 to 25 out of one hand.
Before you install, check whether the package is available at all:
apt update
apt-cache policy openjdk-21-jre-headless
If the Candidate line reads (none), the package does not exist in your release; on a German system the same line is called Kandidat and shows (keine). Otherwise go ahead and install it. For simply running an application the JRE is enough, and the -headless package saves you the graphical dependencies, which is always the right choice on a server:
apt install -y openjdk-21-jre-headless
If you compile yourself or use Gradle or Maven, you need the JDK instead of the JRE, so openjdk-21-jdk-headless. Our articles on Java 17 on Debian and Java 21 on Debian describe the longer routes.
When the distribution does not offer the version: Adoptium Temurin
For every case the standard repositories do not cover, there is the package repository from Adoptium. It provides Temurin 8 to 26 for Debian 12, Debian 13, Ubuntu 22.04 and Ubuntu 24.04, which makes it the only solution that supplies every required version on each of these systems. Important: the formerly common route via apt-key is deprecated. Today the key belongs in /etc/apt/keyrings/ and is bound to exactly this one repository with signed-by.
apt install -y wget gpg apt-transport-https
mkdir -p /etc/apt/keyrings
wget -qO - https://packages.adoptium.net/artifactory/api/gpg/key/public | gpg --dearmor | tee /etc/apt/keyrings/adoptium.gpg > /dev/null
echo "deb [signed-by=/etc/apt/keyrings/adoptium.gpg] https://packages.adoptium.net/artifactory/deb $(awk -F= '/^VERSION_CODENAME/{print$2}' /etc/os-release) main" | tee /etc/apt/sources.list.d/adoptium.list
apt update
apt install -y temurin-21-jdk
The awk call inserts the correct codename automatically, so trixie, bookworm, noble or jammy. If apt update then aborts with Conflicting values set for option Signed-By, an older Adoptium repository already exists, usually created through extrepo. Look for it in /etc/apt/sources.list.d/ and remove the duplicate file.
AlmaLinux, Rocky Linux and RHEL
On the Red Hat family the packages carry different names, with the version inside the name and without an openjdk prefix at the front:
dnf install -y java-21-openjdk-headless
AlmaLinux 9, Rocky Linux 9 and Oracle Linux 9 carry the full range from java-1.8.0-openjdk-headless to java-25-openjdk-headless. AlmaLinux 10, on the other hand, made the same cut as Debian 13 and only knows 21 and 25, so a dnf install java-17-openjdk-headless ends there with No match for argument.
The tool for switching versions is simply called alternatives here, and update-alternatives is only a symlink pointing to it. It does not behave the same way, though: alternatives --list takes no argument. The update-alternatives --list java you know from Debian only prints the help text there and exits with return code 2. Use alternatives --list | grep java or alternatives --display java, and the latter then reports java - status is auto. instead of the Debian form java - auto mode.
On the Red Hat family the installation paths carry the complete package version in the directory name, for example /usr/lib/jvm/java-21-openjdk-21.0.11.0.10-1.el9.x86_64, and not the short Debian form with an architecture suffix. Only AlmaLinux 10 additionally creates the short symlink java-21-openjdk. So do not copy the path from here, take it from alternatives --display java.
One behavior is especially nasty here: after installing Java 17 next to an already present Java 21, alternatives on AlmaLinux 9 switched the /usr/bin/java link over to the older version 17 all by itself. So if you install a second version only to use it for one specific legacy application, you unintentionally change the default Java of the entire system. After every additional installation, set alternatives --set java <path> explicitly and verify with java -version.
Activating the right version
Installed does not mean active. After installing a second JDK, java -version often still shows the old version, because the /usr/bin/java symlink stays unchanged. On Debian and Ubuntu the alternatives mechanism takes care of this:
update-alternatives --list java
update-alternatives --config java
The second command shows a numbered list and asks for your choice. After that the selection is set to manual and future package installations will no longer overwrite it, which is exactly the intention. If you want to script this without a prompt, use the set variant with the full path:
update-alternatives --set java /usr/lib/jvm/temurin-21-jdk-amd64/bin/java
Also set JAVA_HOME when build tools are involved. Gradle and Maven ignore the symlink and follow this variable instead:
export JAVA_HOME=/usr/lib/jvm/temurin-21-jdk-amd64
$JAVA_HOME/bin/java -version
Keep in mind that an export only applies to the current session. For services it does not help at all: after the next reboot the service picks the old Java version again, because it never saw the variable. To make it permanent, JAVA_HOME belongs in the systemd unit as Environment= or, system-wide, in /etc/environment.
The most important step with services
If your application runs as a systemd service, the alternatives symlink is only half the job. A service starts with its own environment, and if an old absolute path sits in ExecStart, no update-alternatives in the world will change that. Enter the full path so the version is fixed independently of the system state:
[Service]
Environment="JAVA_HOME=/usr/lib/jvm/temurin-21-jdk-amd64"
ExecStart=/usr/lib/jvm/temurin-21-jdk-amd64/bin/java -Xms4G -Xmx4G -jar server.jar nogui
Afterwards you have to reload, otherwise the old definition keeps running:
systemctl daemon-reload
systemctl restart minecraft
What such a unit looks like in full is shown in our articles creating a systemd service and starting a Minecraft server automatically. The same trap applies to start scripts that run under screen or tmux, and to panels such as Pterodactyl, which pin the Java version in the container image and not on the host system.
The reverse case: Java is too new
Considerably rarer, but more confusing, is the other case. You start an older modpack or an old build tool on a freshly installed server with Java 21 and get to see the short sentence Unsupported class file major version 65, even though everything is up to date. Here a library is reporting that it cannot do anything with the bytecode format of your new runtime. Typical triggers are Forge modpacks for 1.12.2, older Gradle versions and plugin loaders that have aged badly.
The solution is not to remove the new Java. Install the old version alongside it instead and address it deliberately with an absolute path. That is exactly what the alternatives mechanism was built for, and exactly why it pays off to keep Java 8 and Java 21 on one system at the same time. On Ubuntu that works straight from the standard repositories, on Debian through Temurin:
apt install -y temurin-8-jdk
/usr/lib/jvm/temurin-8-jdk-amd64/bin/java -version
In the start script of the affected service you then replace java with this full path. The system-wide default Java stays untouched and all other applications keep running as before.
When it still does not start after the switch
The version error is gone, but the server still does not start. That is normal and almost always has one of three causes.
Old start flags. If you move from Java 8 to 17 or 21, you often drag along start parameters that no longer exist. The classic one is the old garbage collector, which was removed in Java 14:
Unrecognized VM option 'UseConcMarkSweepGC'
Error: Could not create the Java Virtual Machine.
Error: A fatal exception has occurred. Program will exit.
Remove -XX:+UseConcMarkSweepGC and all related CMS options from the start command. Modern Java versions use G1 by default, and for Minecraft servers the current Aikar flags are the better basis. The message always names the offending option, so there is no need to guess.
Not enough RAM. If Could not reserve enough space for object heap appears after the switch, the value behind -Xmx is larger than the free memory. Newer Java versions respect container and cgroup limits more strictly than Java 8 did. Check the free memory and, if needed, read our article on swap and out of memory.
No Java left in the alternatives system. If you uninstall the old version too eagerly, you get update-alternatives: error: no alternatives for java or plainly command not found. Reinstalling any JRE package repairs this within a minute, and nothing is lost in the process. Caution is only needed with apt autoremove when other packages build on default-jre: check the list of packages to be removed before you confirm.
How you can tell that it worked
Do not rely on the absence of the error message, check three points one after the other instead.
First the version in your shell:
java -version 2>&1 | head -n 1
Second the version the process is really running with. The look at /proc shown above is the most honest tool here, because it trusts neither environment variables nor symlinks. If ls -l /proc/PID/exe points to the JVM directory you wanted, the switch has really arrived.
Third the log of the application. On a Minecraft server the line Done (12.345s)! For help, type "help" is the proof that the start ran through completely. On a systemd service you check that with:
systemctl status minecraft
journalctl -u minecraft -n 50 --no-pager
If you want to know it precisely, you can also make the JVM print its own configuration. That is handy when several Java installations are in play and you have to identify one of them beyond doubt:
java -XshowSettings:properties -version
Among other things, the output contains java.home and java.version. That settles the question of which installation is currently working, once and for all.
In short
The number in the error is not an error code but a version: major minus 44 gives the Java version, 52 is Java 8, 55 is Java 11, 61 is Java 17, 65 is Java 21. The long message with has been compiled by a more recent version means your Java is too old, while the short text Unsupported class file major version without further context usually points to a Java that is too new. Install the matching version, keep in mind that Debian 12 carries only Java 17 and Debian 13 only Java 21 and 25 in the standard repositories, and then really activate the version, if in doubt with an absolute path in the systemd unit. A look at /proc/PID/exe is enough to check, and then you know for certain instead of approximately which Java is executing your application.
If you are setting up a server from scratch and want to avoid these pitfalls from the start, our articles on setting up a new root server and on installing a Minecraft server on Debian will help you get off to a clean start.
Frequently asked questions
What exactly does "class file version 65.0" mean?
Which Java version do I need for my Minecraft server?
I installed Java 21, but java -version still shows Java 17. Why?
Why is there no openjdk-21-jre-headless on Debian 12?
The error only says "Unsupported class file major version 65" with no further text. What now?
How do I find out which Java an already running process uses?
After moving to Java 21 the server starts with "Unrecognized VM option". What happened?
2026 KernelHost GmbH. All rights reserved. This guide is protected by copyright. Republishing it on other websites, in whole, in part or in edited form, is not permitted without our written consent. Quoting with a source credit and a link is expressly welcome.

