JDK を 21 に上げたあと、Gradle のビルドが次のエラーで落ちることがあります。
FAILURE: Build failed with an exception.
* Where:
Build file '/app/build.gradle'
* What went wrong:
BUG! exception in phase 'semantic analysis' in source unit '_BuildScript_'
Unsupported class file major version 65
コピペ検索用の表記ゆれです。数字が違うだけで、原因は同じです。
Unsupported class file major version 65(JDK 21)Unsupported class file major version 61(JDK 17)Unsupported class file major version 67(JDK 23)BUG! exception in phase 'semantic analysis' in source unit '_BuildScript_'
数字が違えば、必要な Gradle の版も違います。「8.5 にすれば全部直る」ではありません(下の切り分けを見てください)。
あなたのコードは 1 行も読まれていません。 落ちているのは Gradle 自身です。
直し方
Gradle を上げます。 JDK を下げる必要も、コードを直す必要もありません。
gradle-wrapper.properties を直接書き換えてください。
# gradle/wrapper/gradle-wrapper.properties
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
./gradlew wrapper --gradle-version 8.5 は使えません。 よく勧められる手ですが、このエラーが起きている状態では、そのコマンド自体が同じエラーで落ちます(確認しました)。理由は次の節のとおりで、Gradle はどのタスクを実行するときも先にビルドスクリプトを解析するので、wrapper タスクに到達する前に止まります。ファイルを直接書き換えるのが、確実に通る手です。
JDK 21 で Gradle 自体を動かすなら 8.5 以降にしてください(実測)。
ただし 7.6.4 が JDK 21 で通る条件も実在します——~/.gradle にビルドスクリプトの解析結果が残っている場合です(下の節)。その場合でも直し方は変わりません。通っているのはキャッシュが解析を省いているからで、キャッシュが消えれば同じエラーで落ちます。
なお、Gradle の公式互換性マトリクスでは「Gradle 自体をその JDK で実行できる版」と「toolchain としてその JDK でコンパイルできる版」が別に定義されています。JDK 21 の場合、前者が 8.5 以降、後者は 8.4 以降です。この記事が扱っているのは前者(Gradle 自体が JDK 21 の上で動くか)です。
sourceCompatibility を下げても直りません
このエラーで最も多い空振りがこれです。「Unsupported class file と言われたのだから、Java の版が高すぎるのだろう」と考えて、こう書きます。
java {
sourceCompatibility = JavaVersion.VERSION_17 // これでは直らない
targetCompatibility = JavaVersion.VERSION_17
}
エラーは 1 文字も変わりません。 理由はエラーメッセージの中にあります。
BUG! exception in phase 'semantic analysis' in source unit '_BuildScript_'
~~~~~~~~~~~~~ ここ
source unit が _BuildScript_ です。つまり Gradle は、自分のビルドスクリプト(build.gradle)を解析している最中に落ちています。あなたの src/main/java には、まだ到達していません。
sourceCompatibility は「あなたのコードを何の版としてコンパイルするか」の設定です。そのコンパイルが始まる前に落ちているので、何を書いても効きません。
なぜ Gradle が JDK のクラスを読めないのか
Gradle は build.gradle(Groovy)を解析するときに、クラスパス上のクラスファイルを読みます。そのために、Gradle が同梱している Groovy が、ASM というバイトコード読み取りライブラリを内蔵しています(スタックトレースに groovyjarjarasm.asm.ClassReader として現れます)。
クラスファイルには「どの JDK でビルドされたか」を示す番号(major version)が入っています。
| JDK | major version |
|---|---|
| 8 | 52 |
| 11 | 55 |
| 17 | 61 |
| 21 | 65 |
Gradle 7.6.4 が同梱する Groovy の ASM は、65 という番号を知りません。 リリース時点でまだ Java 21 が存在しなかったからです。知らない番号のクラスファイルを渡されて、読めずに例外を投げています。
BUG! という文言はここから来ています。Gradle の内部エラーとして報告されているだけで、あなたのビルドスクリプトにバグがあるという意味ではありません。
どの JDK とどの Gradle なら通るのか
Gradle を起動する JDK と Gradle の版を、Gradle User Home(~/.gradle)を毎回まっさらにして組み合わせた実測です。
| 実行する JDK | major version | Gradle 7.6.4 | Gradle 8.5 | Gradle 9.1.0 |
|---|---|---|---|---|
| 17 | 61 | 通る | 通る | 通る |
| 21 | 65 | 落ちる(65) | 通る | 通る |
| 25 | 69 | 落ちる(69) | 落ちる(69) | 通る |
Gradle 7.6.4 は壊れていません(JDK 17 では問題なく動きます)。JDK 21 も壊れていません(Gradle 8.5 では問題なく動きます)。落ちるのは、この 2 つを組み合わせたときだけです。
そして**「8.5 にすれば済む」ではない**ことが、この表の 3 行目に出ています。JDK 25 では 8.5 も落ちます(今度は major 69)。JDK を上げるたびに、Gradle も追いかける必要があります。JDK 25 を通したのは 9.1.0 だけでした(起動時に Full Java 25 support と表示します)。
だから「Gradle を下げる」も「JDK を下げる」も、片方だけ動かせば止まります。ただし下げる方向は将来また同じ問題に当たるので、Gradle を上げるほうを選んでください。
(実測したのは 7.6.4 / 8.5 / 9.1.0 の 3 点だけです。その間のリリースは測っていません。JDK 23=major 67 については、8.5 が落ちることだけ実測しました——公式互換性マトリクスでは JDK 23 は Gradle 8.10 以降です。)
なぜ CI で落ちるのか(同じ Gradle・同じ JDK なのに)
上の表は「まっさらな ~/.gradle」での話です。キャッシュが残っていると、同じ組み合わせが通ります。
同じプロジェクト・同じパス・Gradle 7.6.4・JDK 21 で、~/.gradle の状態だけを変えた実測です。
~/.gradle の状態 | 結果 |
|---|---|
| JDK 17 でビルドした履歴がある | 成功する(終了コード 0) |
| まっさら | Unsupported class file major version 65 で落ちる |
履歴はあるが caches/7.6.4/scripts/ だけ消した | Unsupported class file major version 65 で落ちる |
表の 3 行はすべて Gradle 7.6.4 と JDK 21 の組み合わせです。 版はどこも動かしていません。変えたのはキャッシュだけです。
分岐点は ~/.gradle/caches/<Gradle の版>/scripts/ です。ここにはビルドスクリプトをコンパイルした結果が入っていて、このディレクトリを消したときだけ落ちます(3 行目)。エラーメッセージも Could not open settings generic class cache と、この scripts/ 配下を名指しします。
キャッシュが生きているときに Gradle が build.gradle の解析を省いている、というのが実測と整合する説明です(解析しなければ JDK 21 のクラスファイルを読む必要がなく、major version 65 に当たりません)。ただし Gradle の内部でキャッシュの有効性をどう判定しているかまでは追っていません。言えるのは「scripts/ が成否を決める」までです。
開発マシンと CI で結果が分かれるのは、この差です。
- 開発マシン:JDK 17 の時代にビルドした
~/.gradleが残っている。JDK だけ 21 に上げても通る。 - CI:コンテナは毎回まっさらな Gradle User Home で走る。落ちる。
JDK も Gradle も、両方で同じ版なのに結果が違います。 java -version と distributionUrl を見比べても差は出ません。
手元で再現する
デーモンを先に止めてください。 Gradle デーモンはコンパイル済みのビルドスクリプトをプロセス内に保持するので、デーモンが生きたままディスクの scripts/ を消しても、再解析されずに成功します(実測:デーモンを残したまま scripts/ を削除しても BUILD SUCCESSFUL)。
GRADLE_VER=7.6.4 # 自分の Gradle の版に読み替える
GUH="${GRADLE_USER_HOME:-$HOME/.gradle}" # 既定は ~/.gradle
./gradlew --stop # デーモンを止める
rm -rf "$GUH/caches/$GRADLE_VER/scripts" # スクリプトキャッシュだけを消す
./gradlew --no-daemon build
消すのは scripts だけです。 ~/.gradle や caches ごと消すと、ダウンロード済みの依存も失われて次のビルドが長くかかります。CI などで GRADLE_USER_HOME を設定している場合は場所が変わるので、上のように変数から取ってください。
手元で通っていても、7.6.4 が JDK 21 を読めるようになったわけではありません。 通っているのは古い解析結果を使い回しているからです。まっさらな Gradle User Home(CI のコンテナ、新しいマシン)では必ず落ちます。scripts/ がどの操作で無効化されるかは追っていませんが、環境が変われば無くなるものに依存している状態であることは変わりません。
Gradle を上げられないとき
依存しているプラグイン(Android Gradle Plugin など)が古い Gradle を要求していて、すぐには上げられないことがあります。その場合は、Gradle 自体を古い JDK で動かし、コンパイルだけ新しい JDK にやらせるという分離ができます(Gradle の toolchain)。
java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}
この場合、Gradle を起動する JDK は 17 のままにします(JAVA_HOME や CI の setup-java で指定します)。Gradle 自身は 17 で動くので major version 65 を読む必要がなく、コンパイルだけが 21 で行われます。
この構成が実際に動くことは確認しました:Gradle 7.6.4 を JDK 17 で起動し、toolchain に 21 を指定すると、ビルドは成功し、生成されたクラスファイルは major version 65(=Java 21)になります。Gradle 自身は JDK 21 のクラスを読まないので、major version 65 の問題に当たりません。
ただしこれはプラグインの都合で Gradle を上げられないときの手です。可能なら Gradle を上げるほうが、次の JDK 移行で同じ回避策を積み増さずに済みます。
切り分け
sourceCompatibilityを下げたのに何も変わらない → 正常です。落ちているのはビルドスクリプトの解析段階で、その設定はまだ読まれていません。Gradle を上げてください。- 数字が 65 ではなく 69 → JDK 25 です。8.5 でも落ちます(実測)。9.1.0 まで上げてください。
- 数字が 67 → JDK 23 です。8.5 では直りません(実測)。Gradle の公式互換性マトリクスでは、JDK 23 で Gradle 自体を動かせる最小の版は 8.10 です。
- 数字が 61 → JDK 17 です。Gradle 7.3 以降で動きます(公式互換性マトリクス)。8.5 や 9.1.0 まで上げる必要はありません。過剰な版上げは、プラグイン側の互換性を巻き込みます。
- この記事が失敗を実測したのは 65(JDK 21)と 69(JDK 25)です。 61 と 67 で「必要な最小の Gradle 版」は公式互換性マトリクスに拠り、失敗そのものは測っていません(67 で 8.5 が落ちることだけは実測しました)。
source unitが_BuildScript_ではない → ビルドスクリプトではなく、あなたのコードのコンパイルで落ちています。別の原因を疑ってください。- Gradle ではなく、実行時に
UnsupportedClassVersionErrorが出る → 別のエラーです。そちらはビルドは成功していて、実行する JVM が古いという話です。UnsupportedClassVersionError: class file version 61.0 の直し方 を参照してください。このエラー(Gradle 側)はコンパイルが始まる前(Gradle がビルドスクリプトを解析している段階)、あちら(実行時)はビルドが終わってアプリを動かしている段階、という違いがあります。 ./gradlewを実行しているのに、Gradle の版が変わらない →gradle-wrapper.propertiesのdistributionUrlを見てください。wrapper はそこに書かれた版を使います。ローカルにインストールした Gradle の版は関係ありません。- 手元では通るのに CI でだけ落ちる → 疑う順は 2 つあります。まず版:CI の JDK が新しいのに、
gradle-wrapper.propertiesが古い版を指していないか(java -versionとdistributionUrlを両方で見比べる)。それで差が出ないなら、キャッシュ:JDK も Gradle も両方で同じなのに落ちる場合があります(上の節)。後者は版を見比べても差が出ないので、前者で空振りしたらこちらを見てください。
検証環境
eclipse-temurin:21-jdk、ネットワーク有り(Gradle の公式配布物を取得)- 再現:
plugins { id 'java' }だけの最小プロジェクトを Gradle 7.6.4 でビルドし、Unsupported class file major version 65が出て終了コード 1 - 修正:同一のプロジェクト(
build.gradle/settings.gradle/App.javaすべて差分なし)を Gradle 8.5 でビルドし、同じ JDK 21 のまま終了コード 0・シグネチャ消滅 - 「Gradle 7.6.4 は JDK 17 なら通る」「
sourceCompatibility = 17を足しても同じエラーのまま落ちる」「gradle wrapper --gradle-version 8.5も同じエラーで落ちる」「Gradle 7.6.4 を JDK 17 で起動し toolchain 21 を指定するとビルドが成功し major version 65 が出る」も、それぞれ実行して確認しました - 版の表は、JDK 17 / 21 / 25(
eclipse-temurin)と Gradle 7.6.4 / 8.5 / 9.1.0 の 9 通りを、JDK ごとに新しい Gradle User Home を割り当てて測ったものです。7.6.4 と 8.5 の間、8.5 と 9.1.0 の間のリリースは測っていません。JDK 23(major 67)は、8.5 が落ちることだけ実測しました(Unsupported class file major version 67)。61 と 67 で必要な最小の Gradle 版(7.3 / 8.10)は Gradle の公式互換性マトリクスに拠ります - キャッシュの表は、同じプロジェクト・同じパス・Gradle 7.6.4・JDK 21・
--no-daemonで、~/.gradleの状態だけを変えて測りました(JDK 17 で温める → JDK 21 で成功/まっさら → 失敗/温めたうえでcaches/7.6.4/scripts/だけ削除 → 失敗)。デーモンを有効にしたままscripts/を削除すると、再解析されずに成功します(実測)。だから再現手順では先にデーモンを止めています。scripts/が無効化される正確な条件(Gradle の版更新とスクリプト変更以外)までは追っていません - Gradle wrapper 経由、Kotlin DSL、Android Gradle Plugin や Kotlin プラグインが絡む場合は確認していません