新しい JDK でビルドしたアプリを古い JVM で動かすと、次のエラーで落ちることがあります。
Exception in thread "main" java.lang.NoSuchMethodError: java.nio.ByteBuffer.flip()Ljava/nio/ByteBuffer;
at App.main(App.java:7)
コピペ検索用の表記ゆれです。メソッド名が違うだけで、原因はどれも同じです。
NoSuchMethodError: java.nio.ByteBuffer.flip()Ljava/nio/ByteBuffer;NoSuchMethodError: java.nio.ByteBuffer.clear()Ljava/nio/ByteBuffer;NoSuchMethodError: java.nio.ByteBuffer.position(I)Ljava/nio/ByteBuffer;NoSuchMethodError: java.nio.ByteBuffer.rewind()Ljava/nio/ByteBuffer;NoSuchMethodError: java.nio.CharBuffer.flip()Ljava/nio/CharBuffer;
flip() は Java 8 にもあります。 メソッドは実在するのに、実行時に見つかりません。
原因はビルドの指定です。-source 8 -target 8 でビルドしていると、クラスファイルは Java 8 と名乗るのに、中身は新しい JDK の API を呼びます。
直し方
-source / -target をやめて、--release にします。それだけです。
# Before(このエラーになる)
javac -source 8 -target 8 -d out App.java
# After
javac --release 8 -d out App.java
Maven:
<properties>
<!-- maven.compiler.source / maven.compiler.target ではなく release を使う -->
<maven.compiler.release>8</maven.compiler.release>
</properties>
Gradle:
// sourceCompatibility / targetCompatibility ではなく release を使う。
// compileJava だけに設定すると compileTestJava には効かず、
// テストだけが JDK 17 の API に対してコンパイルされ続ける。
tasks.withType(JavaCompile).configureEach {
options.release = 8
}
コードは変えません。ソースも実行する JVM もそのままで、javac の指定だけで直ります。
なぜ「あるはずのメソッドが無い」のか
Java 9 で、Buffer のメソッドが共変戻り値になりました。戻り値の型が変わっています。
flip() の戻り値 | |
|---|---|
| Java 8 | Buffer(ByteBuffer は flip() を持たず、親の Buffer から継承している) |
| Java 9 以降 | ByteBuffer(ByteBuffer が flip() を上書きした) |
JVM のメソッド解決は、戻り値の型まで含めた署名で行われます。 名前と引数が同じでも、戻り値が違えば別のメソッドです。
エラーメッセージの末尾を読むと、それが書いてあります。
java.nio.ByteBuffer.flip()Ljava/nio/ByteBuffer;
~~~~~~~~~~~~~~~~~~~~ 「ByteBuffer を返す flip」を呼ぼうとした
Java 8 の JVM が持っているのは flip()Ljava/nio/Buffer;(Buffer を返す flip)だけです。求められた署名のメソッドが無いので、NoSuchMethodError になります。
-target 8 は「Java 8 の API でビルドする」という意味ではない
-source… 使ってよい文法のレベル(ラムダやレコードを許すか)。-target… 出力するクラスファイルの版。- どちらも、コンパイル対象の API を制限しません。
つまり -source 8 -target 8 でビルドすると、javac は手元の JDK(たとえば 17)のクラスライブラリを見ながらコンパイルします。ByteBuffer.flip() は JDK 17 のものとして解決され、ByteBuffer を返す署名がクラスファイルに焼き込まれます。クラスファイルの版だけが 8 になっているので、Java 8 の JVM は読み込めてしまい、実行してその行に来た瞬間に落ちます。
--release 8 は違います。Java 8 当時の API 一覧に対してコンパイルします。だから flip() は Buffer を返すものとして解決され、Java 8 の JVM で正しく見つかります。
実際に javap でバイトコードを覗くと、呼び出し先が違うことが分かります。
# -source 8 -target 8 でビルド
invokevirtual // Method java/nio/ByteBuffer.flip:()Ljava/nio/ByteBuffer;
# --release 8 でビルド
invokevirtual // Method java/nio/ByteBuffer.flip:()Ljava/nio/Buffer;
同じソースから、違うメソッドを呼ぶクラスファイルが出ています。
なぜ古い JVM で出るのか
落ちるのは、古い JVM の上でだけです。
-source 8 -target 8 でビルドしたクラスを Java 17 で実行すると、正常終了します。ByteBuffer を返す flip() が実在するからです。落ちるのは、そのクラスファイルを Java 8 の JVM に持って行ったときだけになります。
- 開発機は JDK 17。ビルドも通る。動作確認も通る。
- 本番(または一部の顧客環境、古い CI ランナー)だけが Java 8。そこで初めて落ちる。
-source 8 のときだけ出る警告
-source 8 -target 8 でビルドすると、javac はこう言います。
warning: [options] bootstrap class path not set in conjunction with -source 8
意訳すると「-source 8 と指定されたが、参照するクラスライブラリは 8 のものが指定されていない」です。コンパイラは、手元の JDK のクラスライブラリを見てコンパイルしていることを申告しています。
--release 8 にすると、この警告は出ません。警告が消えることが、API まで 8 に揃ったことの目印になります。
切り分け
javacの指定を--releaseに直したのに、まだ落ちる → 依存ライブラリ側が-targetでビルドされている可能性があります。その jar は自分のビルド設定では直せないので、Java 8 に対応した版のライブラリを探すことになります。ただしat ...の行だけでは犯人を特定できません。 そこに出るのは「呼び出した場所」であって、「欠けたメソッドを要求しているバイナリの持ち主」ではないからです(自分のコードがライブラリの欠けたメソッドを呼んだ場合もat App.mainになります)。読むのはエラー本文の所有クラス.メソッド(シグネチャ)のほうで、そのクラスを含む jar を依存ツリーから探してください。NoSuchMethodErrorだが、クラスがjava.nioではない → 原因の型は同じです。新しい JDK でビルドし、古い JVM で実行しているなら、まず--releaseを疑ってください。List.of(Java 9 追加)のような新しい API を使っていれば、そもそも Java 8 では動きません。--release 8にするとcannot find symbolでコンパイル時点で弾かれるので、実行時ではなくビルド時に気づけます(確認しました)。- エラーが
UnsupportedClassVersionError: class file version ...→ 別のエラーです。そちらはクラスファイルの版そのものが新しすぎて、JVM が読み込めていません。-targetも付けずにビルドした場合がこれです。UnsupportedClassVersionError: class file version 61.0 の直し方 を参照してください。**NoSuchMethodErrorは「読み込めたが、中で呼んでいるメソッドが無い」**という一段先の失敗です。 - エラーが
NoClassDefFoundError: javax/xml/bind/...→ これも別件で、Java 11 が JDK から削除したパッケージの話です。NoClassDefFoundError: javax/xml/bind/JAXBException の直し方 を参照してください。 - そもそも Java 8 を捨てられないのか → 捨てられるなら、それがいちばん確実です。
--releaseは「古い JVM で動かし続ける」ための指定なので、実行環境を上げられるなら、そちらを先に検討してください。
検証環境
eclipse-temurin:17-jdk、ネットワーク有り。同じイメージにopenjdk-8-jre-headless(1.8.0_492)を入れ、ビルド用の JDK 17 と実行用の JVM 8 を同居させています- 再現:
javac -source 8 -target 8でビルドしたクラスを Java 8 で実行し、NoSuchMethodError: java.nio.ByteBuffer.flip()Ljava/nio/ByteBuffer;が出て終了コード 1 - 修正:同一のソースを
javac --release 8でビルドし直し、同じ Java 8 の JVM で実行して終了コード 0・シグネチャ消滅。reproduce/とfix/の.javaに差分はありません(変えたのはjavacの指定だけです) javapで確認した呼び出し先の違い(flip:()Ljava/nio/ByteBuffer;とflip:()Ljava/nio/Buffer;)、同じクラスが Java 17 では正常終了すること、clear/position/limit/rewind/CharBuffer.flipも同じ形で落ちること、-source 8のときだけbootstrap class path not setの警告が出ること、-target 8だけを指定するとコンパイル自体が通らないこと、--release 8ではList.ofがcannot find symbolで弾かれることは、いずれも個別に実行して確認しました
Maven の maven.compiler.release / Gradle の options.release は、機構が同じ(コンパイル対象の API を 8 に制限するか)ことを根拠にした対処であり、ビルドツール経由での再現は通していません。