Skip to content

Fix: Java UnsupportedClassVersionError: Unsupported major.minor version

FixDevs · (Updated: )

Part of:  Java & JVM Errors

Quick Answer

How to fix Java UnsupportedClassVersionError caused by compiling with a newer JDK than the runtime, JAVA_HOME misconfiguration, and Maven/Gradle target version settings.

The Error

You run a Java application and get:

Exception in thread "main" java.lang.UnsupportedClassVersionError:
com/example/Main 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

Or variations:

java.lang.UnsupportedClassVersionError: Unsupported major.minor version 52.0
Error: LinkageError occurred while loading main class Main
java.lang.UnsupportedClassVersionError: Main (class file version 61.0), this version of the Java Runtime only recognizes class file versions up to 55.0

The Java class file was compiled with a newer JDK than the JRE running it. The runtime does not understand the bytecode format.

Why This Happens

Each JDK version produces bytecode with a specific class file version number. A class compiled with JDK 21 (version 65.0) cannot run on JRE 17 (which only understands up to version 61.0). Java is forward-compatible (old code runs on new runtimes) but not backward-compatible (new code does not run on old runtimes).

The class file format stores the version in the first eight bytes of every .class file. The classloader reads those bytes before doing anything else; if the major version is higher than what the JVM was built for, the loader refuses with this error before any of your code runs. There is no way to “downgrade” a compiled class file at runtime — the only real fix is to either compile to a lower target or run on a newer JVM.

A second important detail: the class file version reflects the target the compiler was asked for, not the JDK that hosted the compiler. JDK 21 can produce class files at version 52 (Java 8 bytecode) if you set the right --release flag. That is the entire point of the --release option, and it is the cleanest way to keep one compiler installation while shipping for several runtimes.

Version History That Changes the Failure Mode

The mapping between class file major versions and Java releases is fixed, but the practical landscape — which version your shop is most likely to ship for — has shifted noticeably with each LTS.

Class VersionJDK VersionReleasedStatus
52.0Java 8Mar 2014Long-term LTS, still widespread
53.0Java 9Sep 2017Module system introduced
55.0Java 11Sep 2018LTS
56.0Java 12Mar 2019Non-LTS
57.0Java 13Sep 2019Non-LTS
58.0Java 14Mar 2020Non-LTS
59.0Java 15Sep 2020Non-LTS
60.0Java 16Mar 2021Non-LTS
61.0Java 17Sep 2021LTS
62.0Java 18Mar 2022Non-LTS
63.0Java 19Sep 2022Non-LTS
64.0Java 20Mar 2023Non-LTS
65.0Java 21Sep 2023LTS
66.0Java 22Mar 2024Non-LTS
67.0Java 23Sep 2024Non-LTS
68.0Java 24Mar 2025Non-LTS
69.0Java 25Sep 2025LTS

Practical pivots worth knowing:

  • Java 8 (Mar 2014). Still the most common version in legacy enterprise deployments. Many third-party JARs in Maven Central continue to target Java 8 as the baseline.
  • Java 11 (Sep 2018). The first LTS after Oracle’s six-month cadence began. A lot of “modern but conservative” stacks live here.
  • Java 17 (Sep 2021). Required by Spring Boot 3 and Spring Framework 6. Adopting Spring Boot 3 forces a jump to at least 17, and shops still on Java 8 or 11 hit this error frequently when bumping dependencies.
  • Java 21 (Sep 2023). Required by some newer Spring Boot 3.2+ features and a growing number of libraries that use virtual threads. JARs published in late 2024 and 2025 increasingly target 21.
  • Java 25 (Sep 2025). The current LTS at time of writing. New library releases that say “JDK 25+” cannot be loaded by an older JVM, which is the same root cause behind most fresh appearances of this error.

JDK distributions matter too. Oracle JDK, Eclipse Temurin (Adoptium), Amazon Corretto, Azul Zulu, and Microsoft’s build of OpenJDK all produce class files with the same version numbers, but they differ in packaging, support windows, and free-use terms. The error message does not say which distribution compiled the class — only the major version. If a teammate’s CI uses Temurin 21 and yours uses Corretto 17, you will see this error even though both are “OpenJDK.” Pin distribution and version explicitly in your CI configuration.

Common scenarios:

  • JAVA_HOME points to an old JDK. Your IDE or build tool compiled with a newer JDK than the one on your PATH.
  • CI/CD uses a different JDK. The build server compiles with JDK 21 but the production server runs JDK 17.
  • A dependency was compiled with a newer JDK. A third-party JAR targets a higher bytecode version.
  • Docker base image has the wrong JDK. Your Dockerfile uses a JDK image for building but a different JRE image for running.

Fix 1: Check Your Java Versions

First, see which Java version is running your code:

java -version

Then see which Java version compiled the code:

javac -version

If javac -version shows 21 but java -version shows 17, that is the problem. The compiler and runtime are different versions.

Check JAVA_HOME:

echo $JAVA_HOME

On Windows:

echo %JAVA_HOME%

Fix JAVA_HOME to point to the correct JDK:

export JAVA_HOME=/usr/lib/jvm/java-21-openjdk
export PATH="$JAVA_HOME/bin:$PATH"

Add this to your ~/.bashrc or ~/.zshrc to make it permanent.

Pro Tip: Use a Java version manager like sdkman to switch between JDK versions easily:

sdk install java 21.0.2-open
sdk use java 21.0.2-open

This sets JAVA_HOME and PATH automatically without manual configuration.

Fix 2: Set the Target Version in Maven

Tell Maven to compile for a specific Java version, regardless of which JDK you use to compile:

Using the maven-compiler-plugin:

<properties>
    <maven.compiler.source>17</maven.compiler.source>
    <maven.compiler.target>17</maven.compiler.target>
</properties>

Or with the release flag (Java 9+, preferred):

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

Full plugin configuration:

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.13.0</version>
            <configuration>
                <release>17</release>
            </configuration>
        </plugin>
    </plugins>
</build>

The release flag is better than source/target because it also restricts the API to what is available in the target version. With source/target, you can accidentally use JDK 21 APIs that do not exist in JDK 17, and the code compiles but fails at runtime with NoSuchMethodError.

If Maven itself fails to resolve dependencies during the build, see Fix: Maven could not resolve dependencies.

Fix 3: Set the Target Version in Gradle

Kotlin DSL (build.gradle.kts):

java {
    sourceCompatibility = JavaVersion.VERSION_17
    targetCompatibility = JavaVersion.VERSION_17
}

tasks.withType<JavaCompile> {
    options.release.set(17)
}

Groovy DSL (build.gradle):

java {
    sourceCompatibility = JavaVersion.VERSION_17
    targetCompatibility = JavaVersion.VERSION_17
}

compileJava {
    options.release = 17
}

Rebuild after changing:

./gradlew clean build

If the Gradle build itself fails after this change, run ./gradlew --stacktrace clean build to surface the underlying cause before re-checking the JDK setting.

Fix 4: Fix the Runtime JDK

Instead of changing the compiler target, upgrade the runtime to match the compiler:

Check what is installed:

# Linux
update-alternatives --list java

# macOS
/usr/libexec/java_home -V

Install the required JDK:

# Ubuntu/Debian
sudo apt install openjdk-21-jdk

# macOS (Homebrew)
brew install openjdk@21

# Windows (Chocolatey)
choco install openjdk21

Set the new JDK as default:

# Linux
sudo update-alternatives --set java /usr/lib/jvm/java-21-openjdk-amd64/bin/java

# macOS
export JAVA_HOME=$(/usr/libexec/java_home -v 21)

Fix 5: Fix Docker Multi-Stage Builds

A common mistake in Dockerfiles: compiling with one JDK version and running with another:

Broken:

# Build stage — JDK 21
FROM eclipse-temurin:21-jdk AS builder
WORKDIR /app
COPY . .
RUN ./gradlew build

# Run stage — JDK 17 (version mismatch!)
FROM eclipse-temurin:17-jre
COPY --from=builder /app/build/libs/app.jar /app.jar
CMD ["java", "-jar", "/app.jar"]

Fixed — use the same major version:

# Build stage — JDK 21
FROM eclipse-temurin:21-jdk AS builder
WORKDIR /app
COPY . .
RUN ./gradlew build

# Run stage — JRE 21 (matches!)
FROM eclipse-temurin:21-jre
COPY --from=builder /app/build/libs/app.jar /app.jar
CMD ["java", "-jar", "/app.jar"]

Always use the same major JDK version in both stages. Use the -jre variant for the runtime stage to keep the image small.

Fix 6: Fix Third-Party JAR Version Issues

If the error points to a third-party library class rather than your own code, the library was compiled for a newer JDK than your runtime.

Check a JAR’s target version:

javap -verbose -cp library.jar com.example.SomeClass | grep "major version"

Or unzip the JAR and inspect the class files:

unzip -p library.jar META-INF/MANIFEST.MF

Fix options:

  1. Upgrade your runtime JDK to match the library’s requirement.
  2. Use an older version of the library that targets your JDK version. Check the library’s release notes for Java version requirements.
  3. Use the multi-release JAR if the library provides one. Multi-release JARs contain bytecode for multiple Java versions.

If a class is not found at all (rather than being the wrong version), see Fix: Java ClassNotFoundException.

Fix 7: Fix IDE-Specific Issues

IntelliJ IDEA:

  1. Go to File → Project Structure → Project
  2. Set Project SDK to the correct JDK version
  3. Set Project language level to match your target
  4. Go to File → Project Structure → Modules → Sources
  5. Set Language level for each module
  6. Go to Settings → Build → Compiler → Java Compiler
  7. Set Target bytecode version per module

Eclipse:

  1. Window → Preferences → Java → Compiler
  2. Set Compiler compliance level to your target
  3. Right-click project → Properties → Java Compiler
  4. Set project-specific compliance level

VS Code:

In settings.json:

{
    "java.configuration.runtimes": [
        {
            "name": "JavaSE-17",
            "path": "/usr/lib/jvm/java-17-openjdk",
            "default": true
        }
    ]
}

Fix 8: Fix CI/CD Pipeline Versions

GitHub Actions:

steps:
  - uses: actions/setup-java@v4
    with:
      distribution: 'temurin'
      java-version: '21'  # Match your project's required version

Make sure the same version is used for both build and test steps. Pin both distribution and java-version; the distribution field is what avoids accidentally mixing Temurin and Corretto across jobs.

Jenkins:

pipeline {
    tools {
        jdk 'JDK21'
    }
}

GitLab CI:

image: eclipse-temurin:21-jdk

build:
  script:
    - ./gradlew build

Common Mistake: Setting the JDK version in the build step but forgetting to set it in the test and deploy steps. Each step might use a different default JDK. Always explicitly set the JDK version in every step that runs Java code.

Still Not Working?

If you have verified that the compiler and runtime are the same version:

Check for mixed dependencies. Your project might pull in a transitive dependency compiled for a newer JDK. Use mvn dependency:tree or gradle dependencies to inspect the full dependency tree.

Check for cached class files. Clean the build output completely:

# Maven
mvn clean

# Gradle
./gradlew clean

# Manual
rm -rf target/ build/ out/ bin/

Old class files from a previous compilation might be lingering.

Check for fat JAR issues. If you use a shade plugin or shadow JAR, dependencies might include class files compiled for a different version. Inspect the JAR contents:

jar tf app.jar | head -20

Check annotation processors. Annotation processors (Lombok, MapStruct, Dagger) run during compilation and might produce bytecode at a different level than your source code. Update them to versions compatible with your target JDK.

Check for bytecode manipulation libraries. Libraries like ASM, ByteBuddy, or cglib generate bytecode at runtime. If they target a version higher than the running JVM, you get this error. Update these libraries to versions that support your JDK.

Check for Spring Boot 3 or Jakarta EE 9+ in the dependency tree. Pulling in Spring Boot 3, Spring Framework 6, Jakarta Servlet 5+, or Hibernate 6+ raises the runtime floor to Java 17. If anything in your transitive graph requires those, your “Java 11 runtime” cannot load it. Run mvn dependency:tree -Dverbose or ./gradlew dependencyInsight to find the offending coordinate. For the related Spring DI failure you may then see, check Fix: Spring BeanCreationException: Error creating bean with name.

Check for --enable-preview features baked into the bytecode. Preview features compiled on JDK 21 cannot run on a different JDK 21 build, let alone a different major version. The class file carries a flag marking it as preview, and the JVM refuses to load it unless --enable-preview is passed on the matching version. Remove --enable-preview from your build, or pin the runtime to the exact same minor version.

Check the JDK distribution actually installed in containers. A Dockerfile that says FROM openjdk:17 is pulling from an image that has not been maintained on Docker Hub for years. Use a current image such as eclipse-temurin:21-jre or amazoncorretto:21. An “old image” silently giving you Java 11 instead of the 17 you assumed is a common source of this error.

If the error occurs as an OutOfMemoryError during compilation instead, see Fix: Java OutOfMemoryError for heap configuration.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles