Fix: Java UnsupportedClassVersionError: Unsupported major.minor version
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.0Or variations:
java.lang.UnsupportedClassVersionError: Unsupported major.minor version 52.0Error: 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.0The 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 Version | JDK Version | Released | Status |
|---|---|---|---|
| 52.0 | Java 8 | Mar 2014 | Long-term LTS, still widespread |
| 53.0 | Java 9 | Sep 2017 | Module system introduced |
| 55.0 | Java 11 | Sep 2018 | LTS |
| 56.0 | Java 12 | Mar 2019 | Non-LTS |
| 57.0 | Java 13 | Sep 2019 | Non-LTS |
| 58.0 | Java 14 | Mar 2020 | Non-LTS |
| 59.0 | Java 15 | Sep 2020 | Non-LTS |
| 60.0 | Java 16 | Mar 2021 | Non-LTS |
| 61.0 | Java 17 | Sep 2021 | LTS |
| 62.0 | Java 18 | Mar 2022 | Non-LTS |
| 63.0 | Java 19 | Sep 2022 | Non-LTS |
| 64.0 | Java 20 | Mar 2023 | Non-LTS |
| 65.0 | Java 21 | Sep 2023 | LTS |
| 66.0 | Java 22 | Mar 2024 | Non-LTS |
| 67.0 | Java 23 | Sep 2024 | Non-LTS |
| 68.0 | Java 24 | Mar 2025 | Non-LTS |
| 69.0 | Java 25 | Sep 2025 | LTS |
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 -versionThen see which Java version compiled the code:
javac -versionIf 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_HOMEOn 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
sdkmanto switch between JDK versions easily:sdk install java 21.0.2-open sdk use java 21.0.2-openThis sets
JAVA_HOMEandPATHautomatically 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 buildIf 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 -VInstall the required JDK:
# Ubuntu/Debian
sudo apt install openjdk-21-jdk
# macOS (Homebrew)
brew install openjdk@21
# Windows (Chocolatey)
choco install openjdk21Set 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.MFFix options:
- Upgrade your runtime JDK to match the library’s requirement.
- Use an older version of the library that targets your JDK version. Check the library’s release notes for Java version requirements.
- 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:
- Go to File → Project Structure → Project
- Set Project SDK to the correct JDK version
- Set Project language level to match your target
- Go to File → Project Structure → Modules → Sources
- Set Language level for each module
- Go to Settings → Build → Compiler → Java Compiler
- Set Target bytecode version per module
Eclipse:
- Window → Preferences → Java → Compiler
- Set Compiler compliance level to your target
- Right-click project → Properties → Java Compiler
- 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 versionMake 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 buildCommon 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 -20Check 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.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: AWS Lambda SnapStart Not Working — Version vs Alias, Restore Hooks, and Uniqueness Bugs
How to fix Lambda SnapStart errors — feature requires published version, $LATEST not supported, restore hook for stale connections, UUID collisions after snapshot, time-based state staleness, and pricing surprises.
Fix: Java Record Not Working — Compact Constructor Error, Serialization Fails, or Cannot Extend
How to fix Java record issues — compact constructor validation, custom accessor methods, Jackson serialization, inheritance restrictions, and when to use records vs regular classes.
Fix: OpenTelemetry Not Working — Traces Not Appearing, Spans Missing, or Exporter Connection Refused
How to fix OpenTelemetry issues — SDK initialization order, auto-instrumentation setup, OTLP exporter configuration, context propagation, and missing spans in Node.js, Python, and Java.
Fix: Spring Boot Test Not Working — ApplicationContext Fails to Load, MockMvc Returns 404, or @MockBean Not Injected
How to fix Spring Boot test issues — @SpringBootTest vs test slices, MockMvc setup, @MockBean vs @Mock, test context caching, and common test configuration mistakes.