/개발/UnsupportedClassVersionError 30-Second Diagnosis & Recovery Runbook (class file 61.0)
DevelopmentUnsupportedClassVersionErrorclass file version

UnsupportedClassVersionError 30-Second Diagnosis & Recovery Runbook (class file 61.0)

Pinpoint the cause of UnsupportedClassVersionError in 30 seconds from the class file version 61.0/65.0 numbers alone. A hands-on runbook with copy-paste Maven, Gradle, Docker, and IntelliJ settings for immediate recovery.

UnsupportedClassVersionError 30-Second Diagnosis & Recovery Runbook (class file 61.0)

"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:

TEXT
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.0

Bottom 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 versionJDK(Java) versionRelease type
52.0Java 8LTS
53.0Java 9
54.0Java 10
55.0Java 11LTS
56.0Java 12
57.0Java 13
58.0Java 14
59.0Java 15
60.0Java 16
61.0Java 17LTS
62.0Java 18
63.0Java 19
64.0Java 20
65.0Java 21LTS

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

Bash
java -version

Example of a healthy Java 11 runtime:

TEXT
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+7

Here 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.

Bash
javap -verbose com/example/App.class | grep "major version"

Example of a healthy output:

TEXT
  major version: 61

If 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.

Bash
unzip -p app.jar META-INF/MANIFEST.MF

Example output:

TEXT
Manifest-Version: 1.0
Build-Jdk-Spec: 17
Build-Jdk: 17.0.10+7
Created-By: Maven JAR Plugin 3.4.1

If 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.

XML
<properties>
    <maven.compiler.release>17</maven.compiler.release>
</properties>

The older style used two lines:

XML
<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)

KOTLIN
java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(17)
    }
}

Groovy DSL:

GROOVY
java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(17)
    }
}

The old style looks like this:

GROOVY
sourceCompatibility = '17'
targetCompatibility = '17'

The decisive difference:

ItemsourceCompatibilitytoolchain
MeaningSets language/bytecode level onlyPins the JDK itself used to compile
Build JDK dependencyTied to the JDK that runs GradleAuto-downloads/discovers if missing
Team reproducibilityLow (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

Bash
/usr/libexec/java_home -V

Example output:

TEXT
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:

Bash
export JAVA_HOME=$(/usr/libexec/java_home -v 17)
java -version   # confirm it switched to 17

Linux — update-alternatives

Bash
sudo update-alternatives --config java

Pick the number you want at the prompt. Note this only changes java (the runtime). javac used for compilation is separate, so align that too:

Bash
sudo update-alternatives --config javac

To change only the current shell session, setting JAVA_HOME directly is safer.

Bash
export JAVA_HOME=/usr/lib/jvm/temurin-17-jdk-amd64
export PATH=$JAVA_HOME/bin:$PATH

Windows — check which java is picked up

POWERSHELL
where java

If 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.

Dockerfile
# ❌ 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.

Dockerfile
# ✅ 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.

  1. Project Structure → Project → SDK / Language level The default JDK and language level the project uses to compile.
  2. 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.
  3. 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:

  1. Declare the build target: pin the JDK in code with Maven maven.compiler.release or Gradle toolchain.
  2. Match Docker build/runtime versions: same major version on the multi-stage build and runtime images.
  3. 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.

확인 정보
✦ ✦ ✦
편집 검토 · Editorial Review

Nodelog는 모든 콘텐츠의 내용과 출처를 공개 전에 검토합니다. 환경(OS·버전)에 따라 결과가 달라질 수 있는 기술 정보는 공식 문서와 함께 확인하며, 검토 기준과 정정 원칙은 편집 정책에서 안내합니다. 오류를 발견하시면 이메일로 제보해 주세요 — 확인 후 신속히 정정합니다.

편집 책임 · Nodelog 기술 편집팀·발행 · ·업데이트 ·

Comments

Be the first to comment.