Slide 1

Slide 1 text

Head toward Java 24 (JVM) KUBOTA Yuji LINEヤフー株式会社 Japan Java User Group Night Seminar 2025/03/26

Slide 2

Slide 2 text

KUBOTA Yuji (@sugarlife) Manager @ LINEヤフー株式会社, IMF team IMF team = Kafka infrastructure team エンジニアリングの究極的な挑戦の場はここにある Kafkaイン フラを全社に提供するIMFチームの仕事のやりがい: https://logmi.jp/tech/articles/325945 「膨大な量のトラフィックを扱えるLINEの環境は魅力的」 Kafkaスペシャリストが語る仕事の醍醐味: https://engineering.linecorp.com/en/interview/kafka-okada 2

Slide 3

Slide 3 text

We're hiring! 分散データベースエンジニア(メッセージングサービス)/ LINE Platform https://www.lycorp.co.jp/ja/recruit/career/job-categories/ly00093/ バックエンドエンジニア / LINE Platform https://www.lycorp.co.jp/ja/recruit/career/job-categories/ly00127/ Customer Reliability Engineer / LINE Platform https://www.lycorp.co.jp/ja/recruit/career/job-categories/ly00071/ DevOpsエンジニア(分散データベース) / LINE Platform https://www.lycorp.co.jp/ja/recruit/career/job-categories/ly00077/ and so on 3

Slide 4

Slide 4 text

Recent updates JDK JEP Non-Standard JEP Remark 17 14 3 LTS 18 9 3 - 19 7 6 - 20 7 7 - 21 15 7 LTS 22 12 8 - 23 12 9 - 24 24 10 - 1: Incubator or Preview or Experimental 4

Slide 5

Slide 5 text

5

Slide 6

Slide 6 text

JEPで導入される機能のフェーズ Incubator (JEP 11): Java APIの試験用モジュール( jdk.incubator )。標準化に向けてフィ ードバックを得て変更しやすいように特別なモジュールにしている。有効にするには --add- modules jdk.incubator.xxx の指定が必要 Preview (JEP 12): Java言語の試験機能。有効にするには --enable-preview の指定が必 要。コンパイル時には --source XX か --release XX の指定も必要( XX はバージョン) Experimental(試験機能): GCやコンパイラなどのランタイムの試験機能。有効にするには機 能ごとのオプション以外に、 -XX:+UnlockExperimentalVMOptions の指定が必要 Standard(標準機能): 上記の試験フェーズを通じて標準機能に昇格した機能。リリースサイク ルは6ヶ月だがフィードバック対応はおおよそ3ヶ月しかない(残り3ヶ月は安定化対応)ので、 おおよそ2回以上のバージョンアップを挟んで昇格することが多い 6

Slide 7

Slide 7 text

JVM JEPs in JDK 24 https://bugs.openjdk.org/issues/?jql=project %3D JDK AND issuetype %3D JEP AND status %3D Closed AND fixVersion %3D "24" AND component %3D hotspot JEP 490: ZGC: Remove the Non-Generational Mode JEP 404: Generational Shenandoah (Experimental) JEP 475: Late Barrier Expansion for G1 JEP 450: Compact Object Headers (Experimental) JEP 479: Remove the Windows 32-bit x86 Port JEP 501: Deprecate the 32-bit x86 Port for Removal JEP 483: Ahead-of-Time Class Loading & Linking JEP 491: Synchronize Virtual Threads without Pinning JEP 493: Linking Run-Time Images without JMODs (※ これは違う。tools) 7

Slide 8

Slide 8 text

JEP 490: ZGC: Remove the Non-Generational Mode ZGCはJDK 21から世代別GCとなり(JEP 439: Generational ZGC)、JDK 23からデフォルト となった(JEP 474: ZGC: Generational Mode by Default)。 そしてJDK 24で、メンテナンス性などのため、元々の非世代別の実装が削除された 非世代別を実行するためのオプション -XX:-ZGenerational を指定しても警告が出るだけで 世代別GCが実行される 8

Slide 9

Slide 9 text

JEP 404: Generational Shenandoah (Experimental) 殆どのオブジェクトは短命であるという仮説が実際そうであるケースが多く、集中的に若い 世代でサイクルを回して回収したほうが(CPU的にも電力的にも)効率が良い Red Hatが中心に開発しているShenandoah GCにも世代別GCがきた。以下のオプションで 有効にすることができる。今後のリリースでデフォルト化予定 -XX:+UnlockExperimentalVMOptions -XX:+UseShenandoahGC -XX:ShenandoahGCMode=generational ZGCのように非世代別が削除されるかは不明だが今後世代別がデフォルトになる予定 これで世代別ではないGCはEclipse GCのみ 2: OpenJDKではないがAzul SystemsのC4 GCは世代別を提供している。ただし、OSSではない 9

Slide 10

Slide 10 text

JEP 475: Late Barrier Expansion for G1 プログラムを正常に動作させつつ不要メモリを回収する(GC)には、オブジェクトの参照 関係の正確な追跡が必須 バリアはオブジェクトの参照操作時にメモリの整合性を保つ役割を果たす Read Barrier : オブジェクトを読み込む際に、その参照が最新の位置を示すよう に補正する Write Barrier :新しい参照を書き込む際に、GCが追跡すべき世代間参照等の参 照(のリスト)を記録し、参照を見逃さないようにする。G1ではカードテーブルと呼 ばれるデータ構造を更新することで効率的に管理している バリアは実行時のオーバーヘッドを伴う。しかし、バリアを完全になくそうとすると、 アプリを止めて(STW)全メモリスキャンが必要なので非現実的 バリア実装を簡素化し、オーバーヘッドの改善を図るのが本旨 10

Slide 11

Slide 11 text

(Cont.) JEP 475: Late Barrier Expansion for G1 世代間のヒープ領域への書き込みをサポートしているWrite Barrierの最適化を図る バリア遅延展開: バリアの展開をコンパイラの初期段階からコード生成の直前まで遅らせる ことで、C2コンパイラのオーバーヘッドを削減 (約1.2倍) 新規オブジェクト省略: 新規オブジェクトへの書き込みはバリアが不要なので省略 nullness: 書き込むオブジェクト参照がnullか非nullかが判明している場合、ポストバリア( ) を簡略化または削除。同時にクラスポインタの圧縮・解凍処理も簡略化 解凍操作の最適化: 圧縮クラスポインタが有効な場合に、書き込み操作とバリア処理で重複 する解凍操作を削除するため、圧縮と書き込みを一体化した(疑似)命令を導入 バリアコードのレイアウト最適化 : バリアが必要となる頻度は低いため、頻繁に実行される 部分とそうでない部分を分け、後者をアセンブリスタブにしてコードキャッシュ効率を向上 4: オブジェクトのフィールドに新しい参照を書き込んだ後に実行される、世代間の参照の追跡・記録などの処理 11

Slide 12

Slide 12 text

JEP 450: Compact Object Headers (Experimental) 全てのJavaのオブジェクトはオブジェクト状態やクラスメタデータへの参照などを持つ「ヘ ッダー」を持ち、従来は96~128bits だったものが64bitsに削減・固定された これによりメモリ使用量が最適化された。以下のオプションで有効にできる -XX:+UnlockExperimentalVMOptions -XX:+UseCompactObjectHeaders Project Liliputにより、レイアウトの変更、クラス参照の圧縮、ロック情報の管理方法の改良 (ヘッダサイズを増加させずに機能は維持)等によって削減・固定化が達成された 3: Compressed Class Pointerが有効かどうかで可変 12

Slide 13

Slide 13 text

(Cont.) JEP 450: Compact Object Headers (Experimental) ヘッダーの中身 Mark Word: オブジェクトの識別ハッシュコード、ロック状態、GCメタデータなどの情報 Class Word: クラスポインター。オブジェクトのクラスメタデータへの参照 Mark Word (normal): 64 39 8 3 0 [.......................HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH.AAAA.TT] (Unused) (Hash Code) (GC Age)(Tag) Class Word (uncompressed): 64 0 [cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc] (Class Pointer) Class Word (compressed): 32 0 [CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC] (Compressed Class Pointer) 13

Slide 14

Slide 14 text

(Cont.) JEP 450: Compact Object Headers (Experimental) 圧縮クラスポインタ(以下CCP)を先頭にしてWordを統合 CCPをエンコーディングを変更することで更に圧縮 (32 -> 22bits) ロックメカニズムの変更により、マークワードのロック情報の上書きを避け、CCPを保 持するように変更 オブジェクト移動時のGC処理の変更。古いオブジェクトのヘッダーを新しいオブジェク トの場所に上書きする代わりに、転送を示す新しいタグビットを使用( S ) Header (compact): 64 42 11 7 3 0 [CCCCCCCCCCCCCCCCCCCCCCHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHVVVVAAAASTT] (Compressed Class Pointer) (Hash Code) /(GC Age)^(Tag) (Valhalla-reserved(*) bits) (Self Forwarded Tag) *: 将来的な機能拡張(Value Class等)のための予約領域。議論中らしいが追えておらず、そもそも追えるのかも不明 14

Slide 15

Slide 15 text

JEP 479: Remove the Windows 32-bit x86 Port JDK 21 (JEP 449)で非推奨化されていたWindows 32-bit x86 Portを実装全て削除 32-bitをサポートしているWindows 10は2025-10でEoL Virtual ThreadのWindows 32-bit x86向け実装は(昔の)kernel threadsを利用する実装に なっており期待していた効果を得られなかった 15

Slide 16

Slide 16 text

JEP 501: Deprecate the 32-bit x86 Port for Removal Linux 32-bit x86 portの非推奨化。JDK 25で削除予定 16

Slide 17

Slide 17 text

JEP 483: Ahead-of-Time Class Loading & Linking JVM起動時にクラスローディングおよびそのリンクを行うが、大規模アプリ(クラスが多い)ほ どここに時間をかかる。キャッシュして高速化しましょう by Project Leyden # Record class load data $ java -XX:AOTMode=record -XX:AOTConfiguration=app.aotconf \ -cp app.jar com.example.App ... # Create cache file based on recording file $ java -XX:AOTMode=create -XX:AOTConfiguration=app.aotconf \ -XX:AOTCache=app.aot -cp app.jar # Execute $ java -XX:AOTCache=app.aot -cp app.jar com.example.App ... 5: Project LeydenはJVM起動時間改善プロジェクト。本JEPはその一環であり、これが全てではない 17

Slide 18

Slide 18 text

JEP 491: Synchronize Virtual Threads without Pinning Virtual Threadにおける synchronized のスケーラビリティ悪化問題の緩和を図る Virtual Threadとは Platform Thread(PT): OSのスレッドとして実装されており、スケジューリングはOSのス ケジューラに依存している Virtual Thread(VT): JDK 21で導入(JEP 444)された、JDKによって提供される軽量スレッ ド。JDKのスケジューラが、VTをプロセッサコアに直接割り当てるのではなくPTに割り当 て(マウント)、コードの実行後にアンマウントしてリリースする VTはI/Oなどのブロッキング処理を実行するときにアンマウントされる。処理が完了する準 備ができた際に、VTはJDKスケジューラに返る。その後、JDKスケジューラはコード実行 を再開するために、そのVTをマウントする VTはPTをブロックすることなく高頻度かつ透過的にマウント・アンマウントできるため、 限られたPTで大量のVTを効率的に実行できる 18

Slide 19

Slide 19 text

(Cont.) JEP 491: Synchronize Virtual Threads without Pinning が、 synchronized をVTで使うと以下の問題が発生する VTが synchronized を使うとアンマウントできずPTを占有してしまう(= Pinning) めっちゃ長いブロッキング処理してても、当該PTは他のVTを実行できない VTが synchronized の中でブロッキングすると実際に処理しているPTもブロックする 最悪の場合Deadlockを起こす これを避けるために synchronized ではなく ReentrantLock を使う必要がある 6: JVMがオブジェクトのモニターを保持しているスレッドを追跡する際、VTではなくPTを記録するため、 synchronized 内でアンマ ウントを許容すると別VTを誤ってモニターを保持してると認識し、相互排他性を失ってしまうためPinningしている 7: synchronized ブロック内(=pinned)と外(=unpinned)から ReentrantLock ロックを取得しようするスレッドがある場合、利用可 能なPTがPinningされたVTに占有され、unpinnedなスレッドがロックを取得できずにデッドロックが発生する 19

Slide 20

Slide 20 text

(Cont.) JEP 491: Synchronize Virtual Threads without Pinning https://gist.github.com/DanielThomas/0b099c5f208d7deed8a83bf5fc03179e // Fairness lock 。待機リストの先頭から順番に獲得する final ReentrantLock lock = new ReentrantLock(true); // ロック取得 lock.lock(); // ロック取得メソッド Runnable takeLock = () -> { try { lock.lock(); Thread.sleep(100); } catch (InterruptedException _) { } finally { lock.unlock(); } }; 20

Slide 21

Slide 21 text

(Cont.) JEP 491: Synchronize Virtual Threads without Pinning // `synchronized` ブロック * 外* から獲得 = Unpinned Thread 。待機リストの先頭に Thread unpinned = Thread.ofVirtual().name("unpinned").start(takeLock); // `synchronized` ブロック * 内* から獲得を繰り返し、利用可能なPT 全てでPinning List pinnedThreads = IntStream.range(0, Runtime.getRuntime().availableProcessors()) .mapToObj(i -> Thread.ofVirtual().name("pinning-" + i).start(() -> { synchronized (new Object()) { takeLock.run(); } })).toList(); // ロック解放 lock.unlock(); // しかしUnpinned Thread を動かせるPT が存在しないのでロックが獲得できずDeadLock 21

Slide 22

Slide 22 text

$ java VirtualThreadDeadlockExample.java import java.time.Duration; import java.util.List; import java.util.concurrent.locks.ReentrantLock; import java.util.stream.IntStream; import java.util.stream.Stream; public class VirtualThreadDeadlockExample { public static void main(String[] args) { // Fairness lock 。待機リストの先頭から順番に獲得する final ReentrantLock lock = new ReentrantLock(true); // メインスレッドがロック取得 System.out.println(Thread.currentThread() + " took lock"); lock.lock(); // ロック取得メソッド Runnable takeLock = () -> { try { System.out.println(Thread.currentThread() + " waiting for lock"); lock.lock(); System.out.println(Thread.currentThread() + " took lock"); Thread.sleep(100); } catch (InterruptedException _) { } finally { lock.unlock(); System.out.println(Thread.currentThread() + " released lock"); } }; // `synchronized` ブロック * 外* から獲得 = Unpinned Thread 。待機リストの先頭に Thread unpinnedThread = Thread.ofVirtual().name("unpinned").start(takeLock); // `synchronized` ブロック * 内* から獲得を繰り返し、利用可能なPlatform Thread 全てでPinning List pinnedThreads = IntStream.range(0, Runtime.getRuntime().availableProcessors()) .mapToObj(i -> Thread.ofVirtual().name("pinning-" + i).start(() -> { synchronized (new Object()) { takeLock.run(); } })).toList(); // メインスレッドがロック解放 try { Thread.sleep(500); } catch (InterruptedException _) {} System.out.println(Thread.currentThread() + " released lock"); lock.unlock(); // しかしUnpinned Thread を動かせるPT が存在しないのでロックが獲得できずDeadLock System.out.println("Waiting for taking lock"); Stream.concat(Stream.of(unpinnedThread), pinnedThreads.stream()).forEach(thread -> { try { if (!thread.join(Duration.ofSeconds(5))) { System.out.println("Deadlock detected"); } } catch (InterruptedException e) { throw new RuntimeException(e); } }); } } 22

Slide 23

Slide 23 text

(Cont.) JEP 491: Synchronize Virtual Threads without Pinning 今回の改善 VTが synchronized 内でブロックしてもPTをアンマウントできるようになり、ほかの VTを実行することが可能になった。結果、 synchonirzed を含めロック方法をユースケ ースに応じて柔軟に選べるようになった Object.wait() でブロック中も同様にアンマウント可能になった -Djdk.tracePinnedThreads が性能問題により廃止された 注意点 synchronized の相互排他制御は変わってないので、ロックの取り合いは起きる クラスローディング関連 でアンマウントできないケースが残っている 8: この基本動作が変わってたら既存コードが大惨事。参考サイト:https://b.chiroito.dev/entry/2025/03/22/124631 9: クラスやインターフェースへのシンボリック参照の解決中にクラスロードでブロックする場合などレアケース 23

Slide 24

Slide 24 text

JEP 493: Linking Run-Time Images without JMODs JDKの標準ライブラリはモジュール構成になっておりJMODで提供している jlinkでカスタムランタイムを作成する場合もJMODが使われる このJMODはJDKの総サイズの約25%を占めている カスタムランタイム作成をモジュールのJAR (modular JAR)でも可能にし、JMODを不 要にすることでランタイムイメージのサイズを約25%削減した JDKビルド時の設定で有効になるため、JDKベンダ次第では有効になっていない可能性 もある。 jlink --help で Linking from run-time image enabled が表示されれ ば有効になっている 24

Slide 25

Slide 25 text

Head toward Java 24 (JVM) KUBOTA Yuji LINEヤフー株式会社 Japan Java User Group Night Seminar 2025/03/26 25

Slide 26

Slide 26 text

EoP 26