The program refuses to start, or fails the first time it touches a particular library, with this:
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.0When the class is not your main class but something loaded later, such as a dependency, it appears as an ordinary exception:
Exception in thread "main" java.lang.UnsupportedClassVersionError: org/springframework/boot/SpringApplication has been compiled by a more recent version of the Java Runtime (class file version 61.0), this version of the Java Runtime only recognizes class file versions up to 55.0Every .class file records the Java version it targets. The message says a class was built for a newer Java than the JVM trying to load it. In the first example, the code was compiled for Java 21 (65) and is running on Java 17 (61). In the second, a library built for Java 17 is running on Java 11 (55). Nothing is wrong with the code; the build and the runtime disagree.
Quick fix checklist
- Translate the two numbers with the table below: the first is the class’s target, the second is the running JVM’s maximum.
- Run
java -versionin the same shell, container or service unit that starts the application. - Check
JAVA_HOME, the IDE’s run configuration JDK and the CI or Docker base image. - Either upgrade the runtime to at least the target version (usually the right fix), or rebuild with
--releaseset to the runtime’s version. - If the newer class belongs to a dependency, you must upgrade the runtime or pick an older version of that dependency.
Before you start
Have the exact failing command or deployment config at hand. You need to know which JVM runs the application and which JDK built it; these are often different machines (a developer laptop, a CI runner, a production container). The version table is the key:
| Class file major version | Java release |
|---|---|
| 52 | Java 8 |
| 55 | Java 11 |
| 61 | Java 17 |
| 65 | Java 21 |
| 69 | Java 25 |
The pattern is simple: major version equals the Java feature release plus 44. So 66 is Java 22, 67 is Java 23 and 68 is Java 24. The .0 after the number is the minor version, which is 0 for normal classes.
Why it happens
javac writes a major version into the header of each class file. A JVM supports every class file version up to its own and refuses anything newer, because a newer class may use bytecode features, constant pool entries or library APIs the older JVM does not have. The check happens when the class is loaded, so the failure can appear at startup (your main class) or minutes later (the first time a newer library class is used).
By default javac targets its own version. Compile with JDK 21 and you get version 65 classes, which a Java 17 runtime rejects. The mismatch usually comes from one of these:
- Your machine has several JDKs and the build used a different one from the runtime (
JAVA_HOMEpoints at 21, butjavaon thePATHis 17). - The CI pipeline builds on a newer image than the one production runs.
- An IDE project SDK differs from the command line toolchain.
- You upgraded a library whose new version requires a newer Java. Spring Boot 3 and Spring Framework 6 require Java 17, for example, which produces the second message above on a Java 11 runtime.
Step-by-step walkthrough
Step 1: Decode the numbers
Read the message from the right. “Recognizes class file versions up to 61.0” means the running JVM is Java 17. “Class file version 65.0” means the class targets Java 21. You now know you need either a Java 21 runtime or a Java 17 build target.
Step 2: Find the JVM that really runs the code
java -version
echo "$JAVA_HOME"
which javaRun these where the application actually starts. On a server, check the service definition: a systemd unit or startup script may hard-code /usr/lib/jvm/java-17-openjdk/bin/java. In a container:
docker run --rm --entrypoint java my-app:latest -versionFor a running process, jcmd <pid> VM.version reports the exact runtime.
Step 3: Find what the class was compiled for
javap reads the header of a compiled class. Point it at a class file or at a class inside a jar:
javap -v target/classes/com/example/App.class | grep major
javap -v -cp target/app.jar com.example.App | grep major major version: 65To check a dependency, run the same command with that library’s jar on -cp. On Linux and macOS, file App.class also prints the version.
Step 4: Align the build and the runtime
If you can, upgrade the runtime. Java versions are backward compatible for running older class files, so a Java 21 runtime runs Java 17 classes without changes.
If the runtime is fixed (a customer’s server, a platform image you do not control), target it explicitly when compiling. Use --release, not just -source and -target:
javac --release 17 -d out src/main/java/com/example/App.java--release 17 sets the class file version to 61 and also compiles against the Java 17 API. With only -target 17, a JDK 21 compiler would happily let you call a method added in Java 21 and produce a class that loads on Java 17 but then fails with NoSuchMethodError at runtime.
In Maven, set the release property (maven-compiler-plugin 3.6 or later):
<properties>
<maven.compiler.release>17</maven.compiler.release>
</properties>In Gradle, use a toolchain so the build no longer depends on whichever JDK launched Gradle:
java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(17))
}
}To build with a newer JDK but still target 17, keep the toolchain at the newer version and set options.release.set(17) on the JavaCompile tasks.
Step 5: Pin versions everywhere
Make the CI image, the Dockerfile base image and the build configuration agree, and put the Java version in one place. A runtime image such as eclipse-temurin:21-jre must match or exceed the release your build targets. Add mvn -v or ./gradlew -version to CI logs so the build JDK is visible in every run.
Worked scenario
A team upgrades its local JDK to 21. Builds pass. After deployment, the service crashes at startup with the class file version 65.0 versus 61.0 message.
Diagnosis: docker run --rm --entrypoint java orders-service:latest -version shows the production image is still Java 17. The Dockerfile:
FROM maven:3.9-eclipse-temurin-21 AS build
WORKDIR /src
COPY . .
RUN mvn -q package -DskipTests
FROM eclipse-temurin:17-jre
COPY --from=build /src/target/orders.jar /app/orders.jar
ENTRYPOINT ["java", "-jar", "/app/orders.jar"]The build stage was bumped to 21 when the developers upgraded, but the runtime stage was not. The pom.xml sets no release, so javac 21 targeted 21. javap -v -cp target/orders.jar com.example.orders.OrdersApplication | grep major confirms version 65.
There are two valid fixes. If the team wants Java 21, update the runtime stage to eclipse-temurin:21-jre. If production must stay on 17 for now, pin the target in the pom.xml:
<properties>
<maven.compiler.release>17</maven.compiler.release>
</properties>The team chose the second for this release, and opened a ticket to move both stages to 21 together. The build now produces version 61 classes, and --release would also fail the build if anyone used a Java 21 only API, which protects the Java 17 runtime.
Common mistake
The tempting fix is to set -source 17 -target 17 (or maven.compiler.source and maven.compiler.target) while compiling on JDK 21. The class files get version 61, so the startup error disappears, but the compiler still checks your code against the Java 21 library. A call like List.getFirst(), added in Java 21, compiles and then throws NoSuchMethodError on Java 17. javac even warns about this (“system modules path not set in conjunction with -source 17”). Use --release, which does both jobs.
Other mistakes:
- Changing
JAVA_HOMEin your shell and assuming the service uses it. Services, IDEs and Gradle daemons each choose their own JDK. - Downgrading a library at random to make the number match. If a framework’s new major version requires Java 17, the real decision is whether to upgrade the runtime.
- Editing the class file version with a tool. The newer bytecode may rely on features the old JVM lacks.
Verify the behavior
Prove the fix from the artifact, not from your IDE:
mvn -q clean package
javap -v -cp target/orders.jar com.example.orders.OrdersApplication | grep major
# expect: major version: 61
docker build -t orders-service:test .
docker run --rm orders-service:test --help # assumes the app accepts a harmless flagYou can also make the build enforce the runtime version so a mismatch fails in CI instead of production. The Maven Enforcer plugin’s requireJavaVersion rule checks the JDK running Maven, and a small test can assert the compiled target:
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.io.DataInputStream;
import java.io.IOException;
import org.junit.jupiter.api.Test;
class ClassFileVersionTest {
@Test
void mainClassTargetsJava17() throws IOException {
try (var in = new DataInputStream(
OrdersApplication.class.getResourceAsStream("OrdersApplication.class"))) {
assertEquals(0xCAFEBABE, in.readInt()); // magic number
in.readUnsignedShort(); // minor version
assertEquals(61, in.readUnsignedShort()); // major version for Java 17
}
}
}Interview exercise
A library you depend on releases a new version. After upgrading, your application fails on the production Java 11 runtime with “class file version 61.0 … up to 55.0”. Your own code is compiled with --release 11. Why did the setting not protect you, and what are your options?
Answer and reasoning
--release 11 controls only the classes your compiler produces and the API your code may use. The dependency arrives already compiled, as version 61 (Java 17) class files, and your compiler does not rewrite them. The JVM checks each class when it loads it, so the library’s classes fail on Java 11. The options are to upgrade the runtime to Java 17 or later (the usual answer, since the library has dropped Java 11), or to stay on the last library version that supports Java 11 and accept missing fixes. A strong answer adds prevention: check a dependency’s minimum Java version before upgrading, and run integration tests on the same Java version and image as production, so the mismatch fails in CI.