- JAVA
- 技術 💻
JVMの仕組み8選|JIT・エスケープ解析・仮想スレッド
Javaを何年も書いていると、言語よりもJVMの振る舞いで悩む場面が増えてきますよね。
なぜか速い、なぜか遅い、なぜかキャストで落ちる。原因はコードではなく、その下で動いている実行環境にあることが多いんです。今回は、コードの下でJVMが何をしているか——JVMの「気持ち」が分かるものを8つ集めました。1つでも知らなかったら儲けものです。
① メソッドは、小さいほど速くなることがあります
JITコンパイラの最重要の最適化がインライン化——メソッド呼び出しを、呼び出し先の中身で置き換える処理です。
これが効くと、呼び出しコストが消えるだけでなく、置き換えた先でさらに最適化が連鎖します。ただし条件があり、HotSpotはバイトコードが一定サイズ(既定で35バイト)以下の小さなメソッドを積極的にインライン化します。頻繁に呼ばれるメソッドはもう少し大きくても対象になりますが、巨大なメソッドは外れやすい。
つまり「1つの巨大メソッド」より「小さなメソッドの組み合わせ」の方が、読みやすいだけでなく、速いことがある。可読性と性能が同じ方向を向く、珍しい例なんです。
② newと書いても、オブジェクトが作られないことがあります
もう1つ、JITの魔法です。
メソッド内で作ったオブジェクトが外に漏れない(戻り値にもフィールドにも入らない)とJITが証明できると——エスケープ解析——そのオブジェクトはヒープに確保されず、フィールドが単なるローカル変数に分解されます(スカラー置換)。
「ループ内で毎回newしている、遅そう」というコードが、実測では速い理由がこれです。
「newは遅い」という古い常識は、JITが半分書き換えています。だから性能の話は、計測が先なんです。
③ 同じクラスなのに、ClassCastExceptionが出ます
「com.example.Userをcom.example.Userにキャストできない」——このエラー、実在します。
JVMにとってクラスの同一性は、「クラス名」ではなく「クラス名+読み込んだClassLoader」で決まります。同じjarを別々のClassLoaderが読めば、JVM内には同名の別クラスが2つ存在します。
アプリケーションサーバー、プラグイン機構、ホットリロード——複数のClassLoaderが絡む環境で必ず出会う怪現象です。
クラスは名前で決まらない。これを知っていると、調査の方向が一瞬で定まります。
④ 仮想スレッドは、100万本立てられます
Java 21で正式化された仮想スレッドは、OSスレッドと1対1ではない軽量スレッドです。
従来のスレッドは1本ごとに大きなスタックを持ち、数千本で苦しくなる。仮想スレッドはヒープ上の小さなオブジェクトなので、100万本規模でも動きます。I/O待ちの多いサーバー処理(1リクエスト1スレッドの素朴なモデル)が、そのまま高スループットになる。
ただし注意点もあり、Java 21ではsynchronizedブロック内で待つと、下のOSスレッドが固定されて(ピン留め)恩恵が薄れました。この点はJava 24で改善されています。
「非同期に書き直す」以外の選択肢が、Javaに戻ってきたんです。
⑤ Stringは、1文字1バイトで持っていることがあります
Java 9以降、Stringは内部表現を自動で切り替えます(Compact Strings)。
文字列がLatin-1の範囲(英数字など)に収まる場合は1文字1バイト、日本語などが含まれる場合は1文字2バイトのUTF-16で保持。以前は常に2バイトだったので、英数字中心のシステムではヒープの文字列領域が大きく減りました。
つまり同じ「10文字の文字列」でも、中身が英語か日本語かでメモリ使用量が違う。ヒープダンプで文字列が思ったより小さい/大きい理由は、これなんです。
⑥ GCの停止は、「止まれる場所」まで待ちます
Stop the Worldは、全スレッドがセーフポイントという「止まってよい地点」に到達して初めて始まります。
ここに罠があり、intカウンタの単純なループはJITの最適化でセーフポイントのチェックが省かれることがあります。すると、そのループが終わるまで他のスレッドは止まれず、GCが待たされる。最近のJVMでは対策が入って緩和されていますが、考え方として押さえておきたいところです。
「GCの停止時間は短いのに、実際のレスポンスの棘が長い」——このズレの正体は、停止そのものではなく「止まるまでの時間(Time To Safepoint)」だったりします。
GCログの数字と、ユーザーの体感がズレる理由の1つです。
⑦ コンテナの中のJVMは、メモリ上限を自分で読んでいます
コンテナ時代の必修知識です。
現代のJVMは、コンテナのメモリ制限(cgroup)を認識して、ヒープの最大値を自動で決めます。既定はコンテナメモリの25%(MaxRAMPercentage)。
つまり2GBのコンテナで何も指定しなければ、ヒープは512MB。「メモリを2GBも渡したのにOOMになる」の答えがこれです。逆に、-Xmxをコンテナ上限ぎりぎりに設定すると、ヒープ外の領域が確保できずコンテナごと強制終了されます。
JVMのメモリ設定は、ヒープ+ヒープ外+OS分の「合計」で考える。コンテナに入れた瞬間、この計算が必須になるんです。
⑧ ラムダは、匿名クラスではありません
「ラムダは匿名内部クラスの糖衣構文」——違います。
匿名クラスはコンパイル時に .classファイルが生成されますが、ラムダはinvokedynamicという仕組みで、初回実行時に生成されます。クラスファイルは増えず、キャプチャする変数がなければインスタンスも使い回される。
同じinvokedynamicは、Java 9以降の文字列連結(+)にも使われています。「StringBuilderに置き換わる」という昔の説明は、もう正確ではありません。
バイトコードの下で、Javaは静かに設計を変え続けているんです。
番外編:手元で確かめられる小ネタ
JITの判断を見る
java -XX:+PrintCompilation MyApp
# ①のインライン化の様子(診断オプション)
java -XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining MyApp
どのメソッドがいつコンパイルされ、何がインライン化されたかが流れます。
Java Flight Recorder(JFR)は標準装備
本番でも使える低オーバーヘッドのプロファイラが、JDKに最初から入っています。⑥のセーフポイント待ちも、ここで見えます。
ZGCは、停止時間ミリ秒未満の世界
Java 15で正式化されたZGCは、ヒープが数百GBでも停止時間をミリ秒未満に抑える設計です。Java 21で世代別ZGCが入り、その後は世代別が標準になりました。GCの選択肢は、この10年で様変わりしているんです。
RJCでも、JVMと向き合っています
RJCが携わる金融業界向けのJava 21プロジェクトのような世界では、長時間稼働・ピーク性能・コンテナ運用が当たり前の前提です。⑥⑦のような「コードの外側」の知識が、そのまま安定稼働に効くんですよね。
社内のJava探求グループでも、こういう深いところを掘り下げています。Techチームで共有しながら開発しています。
よくいただくご質問
Q. まずどれから押さえるべきですか?
A. ⑦かなと思います。コンテナ運用は避けて通れず、設定ひとつで本番の生死が決まる話なんですよね。
Q. ここまでJVMを知る必要はありますか?
A. 日常のコードには不要ですが、「なぜか遅い」「なぜか落ちる」の調査で一段深く潜れる人は、チームに1人いるだけで価値が全然違うんです。
Q. 仮想スレッドは、もう使っていいですか?
A. Java 21以上なら実用段階です。④の注意点(synchronized)を理解したうえで、I/O待ちが多い処理から試すのがおすすめです。Java 24以降なら、その注意点も大きく緩和されています。
Q. 未経験からでも、ここまで到達できますか?
A. できます。RJCの研修はJavaの基礎から始まり、ここは実務で数年書いた先の景色です。段階を踏めば必ず辿り着く場所なんです。
Javaの速さは、コードではなくJVMが決めています
今日の8つに共通していたのは、これでした。
インライン化、エスケープ解析、セーフポイント、コンテナ認識——Javaの性能と安定性は、書いたコードの下で走るJVMの判断で決まっています。
その判断を読める人は、勘ではなく観察で最適化できる。上級の入口は、言語の上ではなく下にあるんですよね。
みなさんがJVMで出会った怪現象、よかったら教えてください。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。