Error [has been compiled by a more recent version of the Java Runtime (class file version 61.0)]

UnsupportedClassVersionError: class file version 61.0 の直し方(Java)

FIX SUMMARY verified
Applies when
eclipse-temurin:17-jdkclass compiled with a newer JDK executed on an older JVM (release 17 -> Java 8)

Verified: reproduced in eclipse-temurin:17-jdk, then the has been compiled by a more recent version of the Java Runtime (class file version 61.0) signature was gone after the fix (exit 0).

Java 8 のまま動いていたアプリを、新しい JDK でビルドし直した。あるいは CI の JDK だけが上がった。すると実行時にこれが出ます。

Exception in thread "main" java.lang.UnsupportedClassVersionError: 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 52.0

同じ原因が、class file version 65.0up to 55.0 という数字違いでも出ます。実行側が古いと Unsupported major.minor version 65.0 という別の文言になり(後述)、Gradle からは Unsupported class file major version 61 という短い形で出ます(こちらは別の機構です。後述)。

まず読むべきは 61 ではなく、末尾の 52.0

このメッセージには数字が2つあり、それぞれ別のものを指しています

  • class file version 61.0 … その .classビルドした JDK
  • up to 52.0 … いま実行している JVM が読める上限

つまり「Java 17 でビルドしたものを、Java 8 の JVM で動かそうとした」という意味です。多くの人が 61.0 をそのまま検索しますが、直すための手がかりは末尾の 52.0 のほうにあります。そこに、動かしている JVM の正体が書いてあるからです。

クラスファイル版と Java のバージョンの対応は次のとおりです(実測値)。

Java のバージョンclass file version
852
1155
1761
2165

up to 52.0 と言われたなら、実行しているのは Java 8up to 55.0 なら Java 11 です。ここまで分かれば、あとは2つに1つになります。

major.minor と出たなら、数字はひとつしかない

実行側が古いと、同じ原因でもメッセージの形が変わります。

Exception in thread "main" java.lang.UnsupportedClassVersionError: App65 : Unsupported major.minor version 65.0

こちらの数字はひとつだけで、それはビルドした側の版です65.0 なら Java 21 でビルドされたクラス)。上の節で「末尾の 52.0 を読め」と書きましたが、この形には up to が無いので、実行している JVM の版はメッセージから読み取れませんjava -version で確かめてください。

文言を決めているのは実行する JVM の側です。実測では 7u181 がこの短い形Java 8 は 8u102 / 8u111 / 8u181 / 8u492 のいずれも長い形has been compiled by a more recent version ...)でした。つまり major.minor と出た時点で、動かしているのは Java 8 より前だと分かります。数字の読み替えは上の対応表がそのまま使えます。

直し方は2つ。ビルド側を下げるか、実行側を上げるか

1. 実行側の JVM を上げる(原則こちら)

新しい JDK でビルドしたのには理由があるはずで、その場合に直すのは実行環境のほうです。

# 動かしている JVM を確認する(up to 52.0 なら 1.8.x が出るはず)
java -version

Docker なら、実行イメージのタグを揃えます。ビルドは 17 なのに実行ベースが 8 のまま、という食い違いがよくある形です。

# 実行ステージのベースを、ビルドに使った JDK と揃える
FROM eclipse-temurin:17-jre

CI なら、テストや実行を行うジョブの JDK も上げてください(actions/setup-javajava-version など)。ビルドのジョブだけ上げて、実行のジョブが古いまま、という取りこぼしがこのエラーの典型的な出方です。

2. ビルド側を古い JVM に合わせる(実行環境を動かせないとき)

本番の JVM が 8 から動かせないなら、成果物のほうを 8 向けに出します。--release を使うと、バイトコードの版だけでなく API も 8 のものに制限されるので、「ビルドは通ったのに実行時に NoSuchMethodError」という次の事故を防げます。

<!-- pom.xml -->
<properties>
  <maven.compiler.release>8</maven.compiler.release>
</properties>

Gradle なら次の形です。

// build.gradle.kts
java {
    toolchain {
        languageVersion.set(JavaLanguageVersion.of(8))
    }
}

javac を直接叩くなら javac --release 8 です。古い -source / -target だけの指定は、新しい JDK の API を参照したままバイトコードの版だけ下げてしまうので避けてください。--release はまさにその穴を塞ぐために用意されました。

なぜ JVM は新しいクラスファイルを読まないのか

JVM は .class の先頭にあるメジャー版を読み、自分が知っている上限を超えていたら、そこで読み込みを止めます。上位互換はありません(新しい JVM が古い .class を読むのは可能ですが、逆はできません)。

このため、エラーはコンパイル時ではなくクラスをロードした瞬間に出ます。ビルドは緑、テストも(同じ新しい JDK で走っていれば)緑、デプロイして初めて落ちる。手元と CI とサーバで JVM が違うときに、最後の段だけが赤くなるのはこのためです。

依存ライブラリが原因のこともある

自分のコードを 8 向けにビルドしていても、依存している jar が新しい JDK でビルドされていれば同じことが起きます。この場合、エラーに出るクラス名が自分のパッケージではなく、ライブラリのクラス名になります。

java.lang.UnsupportedClassVersionError: com/example/lib/Foo has been compiled by a more recent
version of the Java Runtime (class file version 61.0) ...

クラス名が自分のものでないなら、上げるべきは自分のビルド設定ではなく、そのライブラリの版(新しい JDK を要求しない版へ下げる)か、実行 JVM のほうです。ライブラリは「Java 17 以上が必要」と宣言していることが多いので、リリースノートの必要 JDK 版を確認してください。

切り分け

  • Unsupported class file major version 61 という短い文言で、Gradle から出る別の機構です。 落ちているのはあなたのアプリではなく Gradle 自身で、しかも JVM のクラスロードではなく、Gradle がビルドスクリプトを解析する段階で使うバイトコード読み取りが失敗しています。直し方も違い(Gradle を上げる)、sourceCompatibility を下げても変わりません。Unsupported class file major version 65 の直し方(Gradle が新しい JDK を読めない) を参照してください。
  • class file version の数字が 61 でなく 65 → ビルドしたのは Java 21 なので、上の表で読み替えてください。原因も対処も変わりません。
  • エラーが NoSuchMethodError → 別件です。バイトコードは読めているので、クラスファイル版の不一致ではありません。--release を使わず -source 8 -target 8 でビルドすると、クラスファイルの版は 8 なのに中身が新しい JDK の API を呼ぶ、という形になります(-target だけを指定してもコンパイル自体が通りません)。NoSuchMethodError: java.nio.ByteBuffer.flip()Ljava/nio/ByteBuffer; の直し方 を参照してください。
  • エラーが NoClassDefFoundError: javax/xml/bind/... → これも別件で、Java 11 が JDK からパッケージごと削除したためです。NoClassDefFoundError: javax/xml/bind/JAXBException の直し方 を参照してください。

検証環境

  • eclipse-temurin:17-jdk、ネットワーク有り。同じイメージに openjdk-8-jre-headless を入れ、ビルド用の JDK 17 と実行用の JVM 8 を同居させています
  • 再現:javac --release 17 でビルドしたクラスを Java 8 の JVM で実行し、class file version 61.0 / up to 52.0 が出て終了コード 1
  • 修正:同じソースを javac --release 8 でビルドし直し、同じ Java 8 の JVM で終了コード 0
  • 対応表(8=52 / 11=55 / 17=61 / 21=65)と「Java 11 で実行すると up to 55.0 になる」「Java 17 で実行すると通る」は、いずれも個別に実行して確認しました
  • Unsupported major.minor version の文言は、同じ .class(版 61 と版 65 の 2 種)を openjdk:7u181-jre-alpine / openjdk:8u102-jre / openjdk:8u111-jre / openjdk:8u181-jre-slim / eclipse-temurin:8-jre(8u492)でそれぞれ実行して確認しました。短い形が出たのは 7u181 だけで、Java 8 の 4 つはすべて長い形です。Java 7 より前の版は実行していません

Maven / Gradle の設定と Docker のマルチステージ構成は、機構が同じ(クラスファイル版と JVM の対応)ことを根拠にした対処であり、ビルドツール経由での再現は通していません。

検証(machine-verified)

この修正は eclipse-temurin:17-jdk のバージョン固定コンテナ内で再現し、修正後に has been compiled by a more recent version of the Java Runtime (class file version 61.0) のシグネチャが消えることを機械で確認しています。

verify — run-case.mjs
$ node run-case.mjs jvm/unsupported-class-version
● reproduce has been compiled by a more recent version of the Java Runtime (class file version 61.0) present ✓
● apply fix exit 0
● re-run has been compiled by a more recent version of the Java Runtime (class file version 61.0) gone ✓
PASS verified · eclipse-temurin:17-jdk · signature gone

確認したのは上のイメージの中だけです。別の環境で直らなかった、記述が違う、という場合は 報告してください(対象と検証イメージは件名・本文に入ります)。