Skip to content
Community

EXPLORE TOPICS

Editorial approach

New to CoreTrailKit?

Observe your services. Keep the evidence close.

CoreTrailKit
Back to articles
GUIDE/ Java

Java UnsupportedClassVersionError: compare the build JDK with the runtime

A Java application compiles successfully but fails to start on a server with UnsupportedClassVersionError. Compilation success describes the build machine. The failing JVM has its own compatibility limits, and it may not be the runtime you expected your service or container to use.

By CoreTrailKit4 min read

01

Capture the class name and version numbers

The error identifies a class that the JVM cannot read because its class-file version is unsupported. Record that class and the reported versions. It may be your entrypoint, but it may also be a dependency compiled for a newer Java release.

As a concrete example, Java 21 class files use major version 65, while Java 17 uses 61. A Java 17 runtime cannot load ordinary Java 21 bytecode. Those numbers describe the artifact, not a missing classpath entry. Adding the same JAR to the classpath again will not lower its class-file version.

02

Inspect the exact build and execution environments

Run version checks in the build job and where the artifact actually executes. A shell on the server may use a different Java installation from a system service. A Docker multi-stage build can compile with one JDK and run with a different base image.

For a class you control, javap exposes its class-file major version. When the failing class belongs to a dependency, inspect the exact dependency version in the packaged artifact. Do not assume your compiler target rewrites third-party classes.

bash · EXAMPLE
java -version
javac -version
# Example for a class compiled into ./out:
javap -verbose -classpath out Hello | grep 'major version'
# In a deployed container, inspect that container's JVM:
docker exec example-java-api java -version

03

Choose a compatibility direction deliberately

There are two basic paths: move execution to a runtime compatible with the artifact, or rebuild an application that is compatible with the required older runtime. The right choice depends on the deployment contract and dependency support, not only on which command removes the error fastest.

If the application intentionally uses newer language features or libraries requiring a newer JVM, targeting an older release is not a magic downgrade. Review those requirements before changing the build. Conversely, upgrading a server JVM should include the application’s normal compatibility and operational tests.

04

Use --release for a controlled older target

For an application intended to run on Java 17, a newer supported javac can compile suitable source with --release 17. The option targets the language, class-file level, and supported platform API for that release. Check the compiler’s supported release values; they are not unlimited.

This minimal example deliberately uses no newer APIs. Put it in Hello.java, compile it, and inspect the result. In Maven or Gradle, express the same target through the build tool’s compiler and toolchain configuration rather than a manual command outside the real build.

java · EXAMPLE
public class Hello {
    public static void main(String[] args) {
        System.out.println("compatible artifact");
    }
}

05

Inspect the result and test the actual packaged application

Compile the example using the intended target, then inspect its metadata. Major version 61 is the expected result for Java 17. Running the class on your current JDK only proves that current JVM can load it; it is not a substitute for testing on Java 17 when that is the deployment requirement.

For a real application, run the packaged JAR or image on the declared runtime and exercise important paths. Inspect dependencies as well. One incompatible transitive JAR can fail at startup or when a rarely used class is first loaded, even if your own classes target the correct release.

bash · EXAMPLE
mkdir -p out
javac --release 17 -d out Hello.java
javap -verbose -classpath out Hello | grep 'major version'
java -cp out Hello

06

Make the runtime contract visible in CI

Record compiler and execution runtime versions alongside the artifact identity. Keep build and runtime image choices explicit. A test that runs only inside the compiler image can miss the exact mismatch introduced by a separate runtime image.

When the fix is complete, preserve both sides of the proof: the class-file target of the built artifact and a successful execution on the intended runtime. A green compile stage alone is the condition that allowed this problem to escape in the first place.

Know the limits

The Java 21/17 version numbers are a specific example. Preview features, native libraries, third-party dependencies, and runtime behavior can impose additional compatibility requirements. --release does not transform dependency bytecode.

Sources & further reading

A technical publication by CoreTrailKit.

Browse all articles

CONTINUE EXPLORING