Skip to content

Java UnsupportedClassVersionError: Class File Version F

java.lang.UnsupportedClassVersionError means your bytecode is newer than your JVM. Decode the class file version numbers and fix the mismatch for good.

Java java-errors unsupportedclassversionerror jvm class-loading java-17 java-21 core-java
Gopi Gorantala
Reading Progress

On This Page

1. The Error

The modern wording, printed by every JVM from JDK 8u onward. This is a JDK 21 build running on a JDK 17 runtime:

Error: LinkageError occurred while loading main class com.example.App
	java.lang.UnsupportedClassVersionError: com/example/App 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

Same build on a JDK 11 runtime — only the trailing number changes:

	java.lang.UnsupportedClassVersionError: com/example/App 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 55.0

Two details people miss: the class name is the internal name with slashes (com/example/App, not com.example.App), and the Error: LinkageError occurred while loading main class prefix is printed by the java launcher only when the main class itself fails to load. When a dependency trips the check, you get a real stack trace mid-run:

main started
Exception in thread "main" java.lang.UnsupportedClassVersionError: Greeter 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 55.0
	at java.base/java.lang.ClassLoader.defineClass1(Native Method)
	at java.base/java.lang.ClassLoader.defineClass(ClassLoader.java:1022)
	at java.base/java.security.SecureClassLoader.defineClass(SecureClassLoader.java:174)
	at java.base/jdk.internal.loader.BuiltinClassLoader.defineClass(BuiltinClassLoader.java:800)
	at java.base/jdk.internal.loader.BuiltinClassLoader.findClassOnClassPathOrNull(BuiltinClassLoader.java:698)
	at java.base/jdk.internal.loader.BuiltinClassLoader.loadClassOrNull(BuiltinClassLoader.java:621)
	at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:579)
	at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:178)
	at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:527)
	at Main.main(Main.java:4)

defineClass1 being the top frame is the tell: the failure happens in the define step of class loading, not in resolution. That is what separates this from ClassNotFoundException (the loader could not find the bytes at all) and NoClassDefFoundError (the bytes were found once but definition failed or a static initializer blew up).

Preview-feature variant — same exception type, different message:

Error: LinkageError occurred while loading main class Prev2
	java.lang.UnsupportedClassVersionError: Preview features are not enabled for Prev2 (class file version 65.65535). Try running with '--enable-preview'

Legacy wording — JDK 7 and earlier printed a different string, still worth knowing if you search old tickets:

java.lang.UnsupportedClassVersionError: com/example/App : Unsupported major.minor version 52.0

Compile-time counterpartjavac refuses a too-new class on the classpath before the JVM ever sees it. This is JDK 11's javac reading a 65.0 class:

app/Main.java:4: error: cannot access Greeter
        System.out.println(Greeter.greet());
                           ^
  bad class file: libout/Greeter.class
    class file has wrong version 65.0, should be 55.0
    Please remove or make sure it appears in the correct subdirectory of the classpath.
1 error

All of the above were produced on openjdk 21.0.10, 17.0.18 and 11.0.32.

2. How to Reproduce It (step-by-step)

Two JDKs, three commands:

cat > App.java <<'EOF'
public class App {
    public static void main(String[] args) {
        System.out.println("Started on Java " + System.getProperty("java.version"));
    }
}
EOF

/usr/lib/jvm/java-21-openjdk-amd64/bin/javac -d out App.java
/usr/lib/jvm/java-17-openjdk-amd64/bin/java -cp out App    # boom

Confirm the version stamped in the bytecode without running anything:

javap -verbose -cp out App | grep -E 'major|minor'
#   minor version: 0
#   major version: 65

od -A x -t x1z -v out/App.class | head -1
# 000000 ca fe ba be 00 00 00 41 ...   0xCAFEBABE, minor 0x0000, major 0x0041 = 65

Reproduce the dependency flavour by compiling the library at 21 and the app at 11:

javac -d libout Greeter.java                  # JDK 21 javac -> 65.0
javac --release 11 -d appout -cp libout Main.java
java -cp appout:libout Main                   # JDK 11 runtime -> stack trace above

Reproduce the preview flavour on a single JDK 21:

javac --enable-preview --release 21 -d prev2 Prev2.java   # class file 65.65535
java -cp prev2 Prev2                                       # fails
java --enable-preview -cp prev2 Prev2                      # works

Reproduce the build-tool flavour — a JDK 17 javac asked to target 21:

javac --release 21 App.java        # error: release version 21 not supported
javac -source 21 -target 21 App.java   # error: invalid source release: 21
javac -source 17 -target 21 App.java   # error: invalid target release: 21

Under Maven those surface as Fatal error compiling: error: release version 21 not supported. Note the three distinct strings — blogs conflate them, but they tell you exactly which option is wrong.

Environment-specific triggers worth knowing: a CI image whose JAVA_HOME differs from the java on PATH; a Docker runtime image (eclipse-temurin:17-jre) that is older than the builder stage (:21-jdk); an IDE compiling with a project SDK newer than the Gradle/Maven toolchain; a single third-party JAR upgraded to a 21-compiled release inside an otherwise Java 17 application.

3. Version Behaviour Matrix

The mapping is fixed by the JVM spec and exposed as constants on java.lang.classfile.ClassFile (Java 24+):

Java SEClass file majorRuntime accepts up to--release targets available in that JDK
85252.06–8
115555.06–11
176161.07–17
216565.08–21
256969.08–25

Behavioural changes across releases:

  • JDK 7 and earlier: message reads Unsupported major.minor version 52.0. JDK 8u and later: the has been compiled by a more recent version of the Java Runtime wording, still current in 25.
  • JDK 9: the Error: LinkageError occurred while loading main class launcher prefix, and java.base/... module prefixes in stack frames.
  • JDK 12+: preview class files carry minor_version = 65535 and are accepted only by the JVM whose major version matches exactly — a 21-preview class will not run on 25 even with --enable-preview.
  • JDK 12: --release gained -Xlint:options obsolescence warnings; on JDK 21, --release 8 still compiles but emits warning: [options] source value 8 is obsolete and will be removed in a future release.
  • JDK 20: --release 7 and below dropped, per JEP 182's "one plus three back" retirement policy. Expect --release 8 to go the same way.

The error itself is version-neutral in nature: no JDK release has ever made a JVM accept a higher class file version than it ships with, and none ever will — that would require executing bytecode whose semantics did not exist when the VM was built.

4. Why It Happens — Surface Level

javac stamps every .class file with the major version of the platform it targeted. A JVM refuses to define any class whose major version exceeds its own. Bytecode is forward-incompatible by design: Java 21 can run Java 8 classes, never the reverse.

So the mismatch is always the same shape — something compiled newer than the thing running it. Your build JDK, a dependency's build JDK, your runtime image, or your IDE's project SDK. The number in the message tells you which end moved: class file version 65.0 is the offender (Java 21), up to 61.0 is your runtime (Java 17).

5. Why It Happens — Under the Hood

Every class file starts with a fixed 8-byte prologue defined in JVMS §4.1:

u4 magic          0xCAFEBABE
u2 minor_version
u2 major_version

HotSpot's ClassFileParser reads those before it reads anything else — before the constant pool, before the verifier, before any name resolution. If major_version > JVM_CLASSFILE_MAJOR_VERSION (the constant baked into that build), parsing aborts immediately and throws UnsupportedClassVersionError, a subclass of ClassFormatErrorLinkageErrorError. That is why the frame is ClassLoader.defineClass1(Native Method): the failure is inside the native define call, in the loading phase of the load → link → initialize lifecycle, long before verification or linkage constraints get a chance to run.

This ordering has a practical consequence. The JVM has not yet parsed the constant pool, so it has no Class object and no source file name — hence the bare internal name com/example/App in the message rather than a nicely formatted binary name.

Why can't the VM just skip the unknown parts? Because a major version bump is a format and semantics contract, not a feature flag. Version 65 permits constant pool entries, attribute types, verification type inference rules and instruction semantics that a version-61 VM's verifier and interpreter simply do not implement. invokedynamic bootstrap shapes, CONSTANT_Dynamic, nest-based access control, PermittedSubclasses — each arrived with a version bump and a verifier change. Accepting the file would mean running unverifiable bytecode.

The preview mechanism reuses the same field. minor_version = 65535 (ClassFile.PREVIEW_MINOR_VERSION) marks a class as depending on preview semantics, and the VM enforces major_version == latestMajorVersion() and --enable-preview. This is deliberate: preview features carry no compatibility promise, so the JVM refuses to run preview bytecode from any release but its own. It is also why a library must never publish preview-compiled classes to a repository.

Related linkage failures on the same upgrade path — NoSuchMethodError and AbstractMethodError from shaded or conflicting dependency versions, LinkageError: loader constraint violation from duplicate classes across loaders, and InaccessibleObjectException from JPMS strong encapsulation — all arrive at the same phase of class loading but for different reasons; UnsupportedClassVersionError is the only one that is purely about the file header.

6. The Fix

Fix A — run on a JVM at least as new as the build. Usually the right answer.

- FROM eclipse-temurin:17-jre
+ FROM eclipse-temurin:21-jre
  COPY target/app.jar /app.jar
  ENTRYPOINT ["java","-jar","/app.jar"]

Verify at runtime, not by intuition — java -version and JAVA_HOME disagree constantly on CI:

java -version; echo "$JAVA_HOME"; readlink -f "$(which java)"

Fix B — compile for the older target with --release. When the runtime cannot move (a vendor appliance, a shared app server):

  <properties>
-   <maven.compiler.source>21</maven.compiler.source>
-   <maven.compiler.target>21</maven.compiler.target>
+   <maven.compiler.release>17</maven.compiler.release>
  </properties>

--release is not sugar for -source/-target. It also swaps in that release's API signature data, so List.of(...) compiled with --release 8 is a compile error instead of a runtime NoSuchMethodError. Plain -source 8 -target 8 on JDK 21 warns exactly about this:

warning: [options] bootstrap class path not set in conjunction with -source 8

Fix C — pin the toolchain so the build JDK is not whatever the agent happens to have.

// build.gradle
java {
    toolchain { languageVersion = JavaLanguageVersion.of(21) }
}
<!-- pom.xml -->
<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-toolchains-plugin</artifactId>
  <version>3.3.0</version>
  <executions><execution><goals><goal>toolchain</goal></goals></execution></executions>
  <configuration>
    <toolchains><jdk><version>21</version></jdk></toolchains>
  </configuration>
</plugin>

Fix D — find the one offending JAR when the failing class is not yours. Scan without running anything:

for e in $(unzip -Z1 app.jar | grep '\.class$'); do
  v=$(unzip -p app.jar "$e" | od -An -j6 -N2 -tu2 --endian=big | tr -d ' ')
  echo "$v $e"
done | sort -rn | head

Then downgrade that dependency, or move the whole application up.

Fix E — preview mismatch. Either add --enable-preview to the runtime command (and accept it must match the compile JDK exactly), or drop --enable-preview and rewrite with final APIs. Never ship preview-compiled artifacts to a shared repository.

7. Best Practices & The Better Design

  • Use --release, never -source/-target. It is the only option that validates against the target release's API surface.
  • Declare the toolchain in the build, not the environment. Gradle toolchains and maven-toolchains-plugin make the build JDK reproducible; the developer's JAVA_HOME stops mattering.
  • Fail the build on a wrong JDK with Maven Enforcer, so a mismatch is a build error, not a 3 a.m. page:
<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-enforcer-plugin</artifactId>
  <version>3.6.3</version>
  <executions><execution>
    <id>enforce-jdk</id><goals><goal>enforce</goal></goals>
    <configuration><rules>
      <requireJavaVersion><version>[21,22)</version></requireJavaVersion>
    </rules></configuration>
  </execution></executions>
</plugin>
  • Keep builder and runtime images in lockstep in the Dockerfile: FROM eclipse-temurin:21-jdk AS build must pair with FROM eclipse-temurin:21-jre. Parameterise the tag once.
  • Ship libraries at the lowest release you support, and use a multi-release JAR if you need newer bytecode for newer runtimes — META-INF/versions/21/ classes are invisible to a Java 17 VM by design, so you get the optimisation without the crash.
  • Log the runtime at startup so the artefact tells you what it ran on:
public final class RuntimeBanner {
    public static void log() {
        Runtime.Version v = Runtime.version();
        System.out.printf("java %s (feature=%d) vendor=%s%n",
                v, v.feature(), System.getProperty("java.vendor"));
    }
}

8. How to Prevent It Long-Term

  • CI matrix on the next LTS. Add a parallel job building and testing on the next LTS. Upgrades stop being events.
  • A class-version gate in the pipeline. Run the unzip | od scan above over the packaged artefact and fail if any class exceeds the deployment target. Ten lines, catches every transitive dependency drift.
  • jdeps --multi-release <n> -s app.jar to see module and version structure; jdeps --jdk-internals to catch sun.* usage before it becomes an InaccessibleObjectException on 17+; jdeprscan --release 21 to flag APIs removed in the target.
  • -Xlint:all -Werror, plus -Xlint:options left on so source value 8 is obsolete is visible rather than buried.
  • Watch for Error: LinkageError occurred while loading main class as a distinct log signature — it means the container image is wrong, and it is worth its own alert because the process never reaches your health endpoint.
  • Team convention: the JDK version lives in exactly one place per repo (the toolchain block), and the Dockerfile references the same number. Anything that hardcodes it twice will eventually disagree.

9. Key Takeaways

  • The two numbers in the message are the whole diagnosis: class file version X is the artefact, up to Y is the runtime. Map with 52=8, 55=11, 61=17, 65=21, 69=25.
  • The error is thrown in ClassLoader.defineClass1 from the 8-byte class file header, before the constant pool is parsed — earlier than NoClassDefFoundError, NoSuchMethodError or any verifier failure.
  • minor version 65535 means preview bytecode: it runs only on the exact same major version, only with --enable-preview, and must never be published.
  • --release N beats -source N -target N because it validates the API surface too — the latter compiles happily and fails at runtime with NoSuchMethodError.
  • Pin the JDK once, in the build toolchain, and make the Dockerfile and CI read the same number. Most occurrences of this error are two places disagreeing about one integer.
Javajava-errorsunsupportedclassversionerrorjvmclass-loadingjava-17java-21core-java

I'm Gopi — 15+ years in Java, building Kafka and Flink platforms for banks, where one lost event is a financial discrepancy. I write javahandbook.com because the guides I needed didn't exist. Everything here is tested against a real cluster first.

Comments