"The build succeeded — so why won't it run?" — Identify 90% of the cause from a single error line
Plenty of people land on this post after mvn package or gradle build finished green locally, only to hit this log the moment they run it on a server or in a container:
Exception in thread "main" java.lang.UnsupportedClassVersionError:
com/example/App 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.0Bottom line: this error occurs only when the JDK used to compile is newer than the JRE that is running. The reverse (compile with an older JDK, run on a newer one) is backward-compatible and is not a problem. The moment you see this error, the cause is already narrowed to one thing.
Decision rule: compile version > runtime version → this error, always.
The answer is already in the error message. class file version 61.0 means this class was compiled with Java 17, and up to 55.0 means the runtime currently executing it only understands up to Java 11. Look up those two numbers in the table and you are done. Follow the steps below in order and most cases recover in 3–5 minutes.
30-second diagnosis: reverse-map class file version numbers to JDK versions
Start by finding the two numbers from the error in the table below. The major version rule is Java 1.1 is 45.0, then +1 for each subsequent major version.
| class file version | JDK(Java) version | Release type |
|---|---|---|
| 52.0 | Java 8 | LTS |
| 53.0 | Java 9 | — |
| 54.0 | Java 10 | — |
| 55.0 | Java 11 | LTS |
| 56.0 | Java 12 | — |
| 57.0 | Java 13 | — |
| 58.0 | Java 14 | — |
| 59.0 | Java 15 | — |
| 60.0 | Java 16 | — |
| 61.0 | Java 17 | LTS |
| 62.0 | Java 18 | — |
| 63.0 | Java 19 | — |
| 64.0 | Java 20 | — |
| 65.0 | Java 21 | LTS |
Plug the example error (61.0 vs 55.0) into the table and you get code compiled with Java 17 running on a Java 11 runtime. This collision has surged especially as Spring Boot 3.x made Java 17 the minimum requirement.
Check the running runtime
java -versionExample of a healthy Java 11 runtime:
openjdk version "11.0.22" 2024-01-16
OpenJDK Runtime Environment Temurin-11.0.22+7
OpenJDK 64-Bit Server VM Temurin-11.0.22+7Here 11.0.22 is the runtime version. It matches the error's 55.0 (Java 11).
Check the compiled class file version
To confirm what a .class file was actually compiled with, use javap.
javap -verbose com/example/App.class | grep "major version"Example of a healthy output:
major version: 61If you see 61, Java 17 compilation is confirmed. Compare the two numbers from the table (compile 61 > runtime 55) and diagnosis is done.
Check what is inside the JAR
You can tell which JDK built a deployed JAR from the manifest.
unzip -p app.jar META-INF/MANIFEST.MFExample output:
Manifest-Version: 1.0
Build-Jdk-Spec: 17
Build-Jdk: 17.0.10+7
Created-By: Maven JAR Plugin 3.4.1If Build-Jdk is 17 and the server's java -version is 11, the answer is already decided. Now you only need to choose what to fix. There are two options:
- Raise the runtime (server/container JRE to at least the compile version)
- Lower the build target (compile to match the runtime)
If production is pinned to a specific version, lower the build target. If you are migrating to a newer version, raising the runtime is the better move.
Align the build target: copy-paste Maven / Gradle settings
Maven — use release instead of source/target
Add this single line under <properties> in pom.xml.
<properties>
<maven.compiler.release>17</maven.compiler.release>
</properties>The older style used two lines:
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
</properties>release is preferred because it also aligns the boot classpath. Specifying only source/target matches the language level and bytecode version, but the compiler can still reference the latest APIs of the build JDK, leaving a risk of NoSuchMethodError on older runtimes. release compiles with the javac --release flag so only that version's API signatures are exposed, cutting off this trap at the source.
Gradle — use toolchain (Kotlin DSL)
java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
}Groovy DSL:
java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
}The old style looks like this:
sourceCompatibility = '17'
targetCompatibility = '17'The decisive difference:
| Item | sourceCompatibility | toolchain |
|---|---|---|
| Meaning | Sets language/bytecode level only | Pins the JDK itself used to compile |
| Build JDK dependency | Tied to the JDK that runs Gradle | Auto-downloads/discovers if missing |
| Team reproducibility | Low (varies by each developer's JDK) | High (same JDK for everyone) |
sourceCompatibility cannot prevent "Gradle running on Java 21 with target 17." You can still incorrectly reference newer APIs in that case. toolchain pins the compiler JDK itself, so the whole team gets identical results. If local JDKs are mixed across developers during a Java 17→21 transition, toolchain is essentially mandatory.
Catch environment-specific traps: when multiple JDKs are installed
If you aligned the build settings and it still reproduces, the likely problem is which JDK is actually selected.
macOS — list installed JDKs and switch
/usr/libexec/java_home -VExample output:
Matching Java Virtual Machines (2):
21.0.2 (arm64) "Eclipse Adoptium" - "OpenJDK 21.0.2"
17.0.10 (arm64) "Eclipse Adoptium" - "OpenJDK 17.0.10"Pin JAVA_HOME to a specific version:
export JAVA_HOME=$(/usr/libexec/java_home -v 17)
java -version # confirm it switched to 17Linux — update-alternatives
sudo update-alternatives --config javaPick the number you want at the prompt. Note this only changes java (the runtime). javac used for compilation is separate, so align that too:
sudo update-alternatives --config javacTo change only the current shell session, setting JAVA_HOME directly is safer.
export JAVA_HOME=/usr/lib/jvm/temurin-17-jdk-amd64
export PATH=$JAVA_HOME/bin:$PATHWindows — check which java is picked up
where javaIf multiple paths appear, the top path is the java that actually runs. Clean up JAVA_HOME and the system Path to the JDK you want. This gets especially tangled on machines with temurin, Corretto, and Oracle JDK all installed.
Docker trap: build is 21, runtime is 17
This is the most common production reproduction. Local or CI builds with Java 21, but the runtime image is 17-jre.
# ❌ mismatch — build 21, runtime 17
FROM eclipse-temurin:21-jdk AS build
WORKDIR /app
COPY . .
RUN ./gradlew bootJar
FROM eclipse-temurin:17-jre # ← this is the problem
COPY --from=build /app/build/libs/app.jar app.jar
ENTRYPOINT ["java", "-jar", "app.jar"]Running this image produces a class file version 65.0 (Java 21) vs up to 61.0 (Java 17) error. Always match the major versions of the build stage and the runtime stage.
# ✅ match — build and runtime both 21
FROM eclipse-temurin:21-jdk AS build
WORKDIR /app
COPY . .
RUN ./gradlew bootJar
FROM eclipse-temurin:21-jre
COPY --from=build /app/build/libs/app.jar app.jar
ENTRYPOINT ["java", "-jar", "app.jar"]If you cannot raise the runtime image (e.g. ops policy pins 17-jre), lower the build stage and the Gradle toolchain to 17 instead. Unifying on one of the two is the key.
Align IntelliJ: all three places must match
If it only reproduces in the IDE, or IDE runs differ from terminal builds, check these three IntelliJ settings. A mismatch in any one of them will reproduce the symptom.
- Project Structure → Project → SDK / Language level The default JDK and language level the project uses to compile.
- Settings → Build, Execution, Deployment → Build Tools → Gradle → Gradle JVM The JVM used when running Gradle tasks. If this is 21 and Project SDK is 17, CLI builds will differ.
- Settings → Build Tools → Maven → Runner → JRE (Maven projects) The JDK used to run Maven.
The three exist separately because IntelliJ manages "IDE compile," "build-tool execution," and "project default" independently. When in doubt, unify all three to the same version — that is the safest option.
3-line checklist to keep it from breaking again + team standardization
Minimum checklist after recovery to prevent recurrence:
- Declare the build target: pin the JDK in code with Maven
maven.compiler.releaseor Gradletoolchain. - Match Docker build/runtime versions: same major version on the multi-stage build and runtime images.
- Verify the runtime environment: before deploy, compare
java -version(runtime) ↔Build-Jdk(JAR).
For team standardization, enable auto-download provisioning on the Gradle toolchain, or pin the JDK version in the repo with a file like .sdkmanrc (SDKMAN). Unifying mixed temurin/Corretto setups at the repository level cuts down "works on my machine" version collisions a lot. During a Java 17→21 LTS transition, also pin the build JDK in the CI pipeline explicitly.
For exact mapping values and current distribution policy, check the official sources (Oracle JVM Specification The class File Format, Eclipse Temurin Docker tag docs).
FAQ
Q. What Java version is class file version 61.0, exactly? A. Java 17. major version starts at 45 for Java 1.1 and increments by 1 each version, so 61 = 45 + 16 = Java 17. 65.0 is Java 21.
Q. I cannot raise the runtime. Is compiling to a lower version enough?
A. Usually yes. Set Maven maven.compiler.release or the Gradle toolchain to the runtime version or below. However, if you already use Java 17+ syntax (records, sealed classes, etc.) or newer APIs, compilation itself will fail and you will need to change the code.
Q. mvn compile works, but running fails. Why?
A. The JDK used to build and the JRE used to run are different. Compare java -version (runtime) with the JAR's Build-Jdk (compile side). If the compile number is larger, that is the cause.
Nodelog는 모든 콘텐츠의 내용과 출처를 공개 전에 검토합니다. 환경(OS·버전)에 따라 결과가 달라질 수 있는 기술 정보는 공식 문서와 함께 확인하며, 검토 기준과 정정 원칙은 편집 정책에서 안내합니다. 오류를 발견하시면 이메일로 제보해 주세요 — 확인 후 신속히 정정합니다.
Comments
Be the first to comment.