rea-reverse

Fix 'Unsupported class file major version' in Java and Gradle

Published by rea-reverseUpdated 8 min read
On this page

Two different errors carry the same kind of number. One comes from the JVM when it loads a class:

java.lang.UnsupportedClassVersionError: demo/Hello 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

The other comes from a tool that reads bytecode, such as Gradle’s Groovy compiler or Android’s D8:

java.lang.IllegalArgumentException: Unsupported class file major version 61

The number is the class file’s major version, and it says which Java release compiled the class. The two messages mean different things, though: the first says your runtime is too old, the second usually says a tool is too old for the JDK or the classes it is given. This guide reproduces both on purpose with small programs, shows four ways to read the version of a class or jar, and fixes each case.

It is for Java, Kotlin and Android developers who hit one of these errors in a build or at startup. Everything below was run on macOS 27.0.1 (Apple silicon) with Homebrew OpenJDK 11.0.32.1, 17.0.18 and 25.0.2, Gradle 6.7.1 and 9.6.1, and the D8 versions shipped in Android SDK build-tools 30.0.3 and command-line tools 20.0. Output is copied from those runs.

Fix 'Unsupported class file major version' in Java and GradleSilent walkthrough with English captions.

The number: major version = Java release + 44

Bytes 4 to 7 of every .class file hold the minor and major version. From Java 5 on, the major version is the release number plus 44. These are the ones you will meet:

Major Java Major Java
52 8 (LTS) 62 18
53 9 63 19
54 10 64 20
55 11 (LTS) 65 21 (LTS)
56 12 66 22
57 13 67 23
58 14 68 24
59 15 69 25 (LTS)
60 16 70 26
61 17 (LTS) 71 27

Older ones: 45 is Java 1.0.2/1.1, 46 to 48 are 1.2 to 1.4, 49 to 51 are Java 5 to 7. A minor version of 65535 means the class uses preview features and runs only on the exact release that compiled it, with --enable-preview.

Reproducing the runtime error

A one-line Hello class, compiled with JDK 25 for five targets:

for r in 8 11 17 21 25; do
  javac --release $r -d out/r$r src/demo/Hello.java
done

The --release 8 run already warned:

warning: [options] source value 8 is obsolete and will be removed in a future release
warning: [options] target value 8 is obsolete and will be removed in a future release
warning: [options] To suppress warnings about obsolete options, use -Xlint:-options.
3 warnings

It still produced a class. Running the --release 21 class on JDK 17 gives the error from the top of this page:

$ /opt/homebrew/opt/openjdk@17/bin/java -cp out/r21 demo.Hello
Error: LinkageError occurred while loading main class demo.Hello
	java.lang.UnsupportedClassVersionError: demo/Hello 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

Both numbers are in the message: 65 is what the class needs (Java 21), 61 is what the runtime can load (Java 17). The --release 17 jar on JDK 11 failed the same way with class file version 61.0 and up to 55.0.

javac produces two related errors that people search for in the same breath. JDK 17 asked to target 21:

$ javac --release 21 ...
error: release version 21 not supported

And JDK 11 compiling code that uses a class from the Java 17 jar:

out/use/demo2/Use.java:2: error: cannot access Hello
  bad class file: out/hello-17.jar(/demo/Hello.class)
    class file has wrong version 61.0, should be 55.0

In all three, the fix is the same direction: run or compile with a JDK at least as new as the class, or rebuild the class for the older release.

Four ways to read the version

file is already on macOS (here file-5.41):

out/r11/demo/Hello.class: compiled Java class data, version 55.0 (Java SE 11)
out/r17/demo/Hello.class: compiled Java class data, version 61.0
out/r21/demo/Hello.class: compiled Java class data, version 65.0
out/r25/demo/Hello.class: compiled Java class data, version 69.0
out/r8/demo/Hello.class:  compiled Java class data, version 52.0 (Java 1.8)

It prints the number for all of them but names the release only for 52 and 55. That is the magic database of this file build, not a problem with the classes. It also only works on loose .class files, not on classes inside a jar.

The raw bytes. xxd -l 8 on the release 21 class prints cafe babe 0000 0041. CAFEBABE is the magic, 0000 the minor version, 0x41 is 65.

javap -v works on a file or on a class inside a jar:

$ javap -v out/r21/demo/Hello.class | grep version
  minor version: 0
  major version: 65
$ javap -v -cp out/hello-17.jar demo.Hello | grep version
  minor version: 0
  major version: 61

grep version also matched three more lines in our class (the "java.version" string in the constant pool and the instruction that loads it), so grep for major version if your code has such strings.

The Java class version checker reads every class in a jar, WAR, EAR or AAR, nested jars included, in the browser. This matters because one jar can hold several versions. We built mixed.jar with our release 11 Hello, a release 21 class Last, and the release 17 jar nested under lib/. We ran the tool’s code (src/tools/java/analyze.ts from this site’s repository) under Node.js on it:

== mixed.jar kind=jar archives=2 classes=3
  archive "" manifest=[["Created-By","25.0.2 (Homebrew)"],["Main-Class","demo.Hello"]]
  archive "lib/hello-17.jar" manifest=[["Created-By","25.0.2 (Homebrew)"]]
  [(upload)] demo/Hello.class 55.0
  [(upload)] demo/Last.class 65.0
  [lib/hello-17.jar] demo/Hello.class 61.0

Two things in that output are worth slowing down for. First, Created-By: 25.0.2 (Homebrew) is the JDK that ran the jar tool. It says nothing about the target: the same JDK 25 produced classes at 55, 61 and 65. Do not use Created-By or Build-Jdk to answer “which Java does this need”. Second, the JVM checks versions per class, when the class is loaded. On JDK 17, java -cp mixed.jar demo.Hello printed hello from 17.0.18 and exited 0; only demo.Last failed with class file version 65.0. A jar can work for weeks and then break the first time a code path touches the newer class. A version check of the whole jar finds that class before a user does.

The tool also has an error explainer. We fed it the messages captured above. It recognized the runtime error (class demo/Last, major 65, runtime up to 61), the Gradle and D8 “Unsupported class file major version” lines, javac’s “wrong version 61.0, should be 55.0” and “release version 21 not supported”. Our first version returned nothing for javac output under a Chinese system locale, such as 错误: 不支持发行版本 21. We then ran javac 11, 17 and 25 with -J-Duser.language=zh -J-Duser.country=CN, copied what they printed, and added those messages to the explainer: 类文件具有错误的版本 61.0, 应为 55.0 (wrong version), 不支持发行版本 21 (--release too new), and 无效的目标发行版:17 / 无效的源发行版:17 (-target / -source too new). Pasting those Chinese captures now gives the same findings as their English versions. Other locales, Japanese for example, are still not recognized; for those, run javac with -J-Duser.language=en before you paste. The JVM’s own UnsupportedClassVersionError text stayed English under the Chinese locale; only the launcher’s first line (错误: 加载主类 … 时出现 LinkageError) was translated.

Reproducing the tool error: old Gradle on a new JDK

Gradle 6.7.1 on JDK 17, with a build file that only says plugins { id 'java' }:

* What went wrong:
Could not compile settings file '.../gr6/settings.gradle'.
> startup failed:
  General error during semantic analysis: Unsupported class file major version 61

  java.lang.IllegalArgumentException: Unsupported class file major version 61
  	at groovyjarjarasm.asm.ClassReader.<init>(ClassReader.java:196)

Here 61 is not one of our classes. Our settings.gradle is one line. The class that failed is a JDK 17 class Gradle’s bundled ASM tried to read while compiling the build script, and that ASM is older than Java 17. Nothing in the project needs fixing; the Gradle version and the JDK it runs on do not fit. The same project on JDK 11 built fine, and its output was version 55.0 (Java SE 11).

So when you see this message from Gradle, check which JDK runs Gradle (./gradlew --version prints it) before you look at dependencies.

Reproducing it in an Android build: D8

D8 converts class files to DEX, so it has its own ceiling. Build-tools 30.0.3 ships D8 2.1.7-r1. We fed it our jars:

Jar D8 2.1.7-r1 (build-tools 30.0.3) D8 9.0.32 (cmdline-tools 20.0)
--release 11 (55) classes.dex written not tried
--release 17 (61) Unsupported class file major version 61 not tried
--release 21 (65) Unsupported class file major version 65 not tried
--release 25 (69) Unsupported class file major version 69 classes.dex written

The old D8 prints Compilation failed with an internal error. followed by the IllegalArgumentException. In a Gradle build the D8 version comes from the Android Gradle plugin, so the fix is a newer AGP, or a library version compiled for an older Java. Check the AAR or JAR named in the log with the version checker to see which one it is.

Fix 1: compile with --release, not -source/-target

The most common way to produce the error is to build on a new JDK and deploy on an old one. --release N makes javac write version N class files and compile against the Java N API. -source N -target N only does the first part. JDK 25 warns about it:

warning: [options] location of system modules is not set in conjunction with -source 17
  not setting the location of system modules may lead to class files that cannot run on JDK 17
    --release 17 is recommended instead of -source 17 -target 17 because it sets the location of system modules automatically

We tested what that warning means with a class that calls List.getLast(), a Java 21 method:

$ javac -source 17 -target 17 -d out/t17 src2/demo/Last.java     # exit 0, 1 warning
$ file out/t17/demo/Last.class
out/t17/demo/Last.class: compiled Java class data, version 61.0
$ /opt/homebrew/opt/openjdk@17/bin/java -cp out/t17 demo.Last
Exception in thread "main" java.lang.NoSuchMethodError: 'java.lang.Object java.util.List.getLast()'
	at demo.Last.main(Last.java:9)

The class says 61, every version check passes, and it still breaks on Java 17. With --release 17 the same file fails at compile time instead:

src2/demo/Last.java:9: error: cannot find symbol
    System.out.println(xs.getLast());
  symbol:   method getLast()

A class version checker cannot catch the -target case, because the version number is correct. Only --release (or compiling with the old JDK itself) does.

Fix 2: a Gradle toolchain

In Gradle, the toolchain sets which JDK compiles, independently of the JDK that runs Gradle:

java {
  toolchain {
    languageVersion = JavaLanguageVersion.of(11)
  }
}

We ran Gradle 9.6.1 on JDK 25 with that block and auto-download off, and hit a setup problem first:

Cannot find a Java installation on your machine (Mac OS X 27.0.1 aarch64) matching: {languageVersion=11, vendor=any vendor, implementation=vendor-specific, nativeImageCapable=false}. Toolchain auto-provisioning is not enabled.

JDK 11 was installed, but as a keg-only Homebrew formula, which is not registered with /usr/libexec/java_home, so Gradle did not see it. Pointing Gradle at it fixed that:

gradle -Porg.gradle.java.installations.paths=/opt/homebrew/opt/openjdk@11 jarForCheck

gradle javaToolchains then listed Homebrew JDK 11 (11.0.32.1+0) as Detected by: Gradle property 'org.gradle.java.installations.paths', and the compiled class was version 55.0 (Java SE 11). For a permanent setup put that property in gradle.properties. In Maven the matching setting is the maven.compiler.release property; we did not test Maven for this guide.

Fix 3: run on a newer JVM

If the class is from a dependency and you cannot rebuild it, the runtime has to be at least the class’s release. The error tells you the minimum: class file version 65.0 means Java 21 or newer. For Android apps this does not apply, since the device runs DEX, not class files; there the limit is D8 in your AGP, covered above.

Where REA fits

REA, the open-source project by morluto, does not check class file versions. Its Android analysis in [email protected] runs headless JADX and, per the package README, needs “a full JDK” on the machine. If REA’s Android tools fail because of the JDK, the same reading applies: check which JDK runs JADX before you look at the APK. The JADX setup is in Android APK analysis with REA.

Limits

  • We tested the JDKs we had: 11, 17 and 25. The Java 8 runtime’s older wording (“Unsupported major.minor version 52.0”) was not reproduced here.
  • The Gradle result is for Gradle 6.7.1. We did not test which later Gradle release first runs on Java 17; check Gradle’s compatibility matrix for that.
  • Version numbers do not catch API use, as the -target example shows.

Next steps