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.0 や up to 55.0 という数字違いでも出ます。Gradle からは Unsupported class file major version 61 という短い形で出ます(こちらは別の機構です。後述)。
まず読むべきは 61 ではなく、末尾の 52.0
このメッセージには数字が2つあり、それぞれ別のものを指しています。
class file version 61.0… その.classをビルドした JDKup to 52.0… いま実行している JVM が読める上限
つまり「Java 17 でビルドしたものを、Java 8 の JVM で動かそうとした」という意味です。多くの人が 61.0 をそのまま検索しますが、直すための手がかりは末尾の 52.0 のほうにあります。そこに、動かしている JVM の正体が書いてあるからです。
クラスファイル版と Java のバージョンの対応は次のとおりです(実測値)。
| Java のバージョン | class file version |
|---|---|
| 8 | 52 |
| 11 | 55 |
| 17 | 61 |
| 21 | 65 |
up to 52.0 と言われたなら、実行しているのは Java 8。up to 55.0 なら Java 11 です。ここまで分かれば、あとは2つに1つになります。
直し方は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-java の java-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 で実行すると通る」は、いずれも個別に実行して確認しました
Maven / Gradle の設定と Docker のマルチステージ構成は、機構が同じ(クラスファイル版と JVM の対応)ことを根拠にした対処であり、ビルドツール経由での再現は通していません。