JVM — アーキテクチャ・GC・String Pool
JVM — アーキテクチャ・GC・String Pool
🎯 このlessonを読み終えたら
このlessonを読み終えると、以下の3つを自信を持って説明できるようになります。
- ▸✅ JVMメモリ構造 (Heap / Stack / Metaspace) + GCアルゴリズム
- ▸✅ 運用JVMオプション (-Xms/-Xmx/-XX:+UseG1GC) 5種の推奨設定
- ▸✅ OOM発生時のHeapDump分析ワークフローの説明
学習目標をチェックリストとして手元に置き、すべてに答えられるようになったらlessonを閉じてください。
JVMとは何か — *一度書けばどこでも動く*
一言でまとめると
JVM (Java Virtual Machine) = Javaコードを実行する仮想マシン。"Write Once, Run Anywhere" — 一度書いたJavaコードがWindows・macOS・Linuxどこでもまったく同じように動きます。
なぜ可能なのか
Javaコードは直接実行されません。2つのステップを経ます:
1. コンパイル: .javaファイル → .classファイル (バイトコード)。OSに依存しない中間言語。
2. 実行: JVMが.classファイルを読み込み、各OSのネイティブ命令に変換して実行。
このバイトコード + JVMの組み合わせがJavaの核心です。書く時点でWindowsやMacを意識する必要はなく、実行時点でそのOS用JVMがすべて処理します。
JVMメモリ — 何がどこに格納されるか
JVMはメモリを複数の領域に分けて管理します。重要な4つ:
- ▸ヒープ — すべてのオブジェクトと配列。最大の領域。GCの舞台。
- ▸スタック — メソッド呼び出しフレームとローカル変数。スレッドごとに独立。
- ▸Method Area — クラスメタデータ (フィールド・メソッド情報)。全スレッド共有。
- ▸PCレジスタ — 現在実行中の命令位置。スレッドごと。
xのようなプリミティブ値はスタックへ。newで生成したオブジェクトはヒープへ。そのオブジェクトを指す参照だけがスタックに置かれます。この区別がJavaメモリの基本の図です。
JITコンパイラ — よく使うコードをさらに速く
最初、JVMはバイトコードをインタープリットします — 一行ずつ読んで実行。遅いです。
しかし、どのメソッドが頻繁に呼ばれるかを追跡し、閾値を超えるとそのメソッドをネイティブコードにコンパイルします。これをJIT (Just-In-Time)と呼びます。
JITがプロファイリングに基づいて最適化するため、長く動いたJVMほど速くなります (ウォームアップ)。Javaサーバーが最初は遅くて徐々に速くなる理由です。
C++のように事前コンパイルされたコードも速いですが、JITは実際の実行パターンを見て最適化するため、場合によってはC++より速くなることもあります。
まとめ
JVMは単なる翻訳機ではありません。メモリ管理・最適化・GCまですべて自動で処理します。Java開発者がメモリの確保・解放を手動で行わなくてよい理由です。
Garbage Collection — *自動メモリクリーンアップ*
GCが行うこと
C・C++ではfree()で手動でメモリを解放する必要があります。忘れるとメモリリーク、2回解放するとプログラムクラッシュ。
JavaのGarbage Collectorが自動で処理します。もう参照されていないオブジェクトを見つけてメモリを回収します。開発者は生成するだけで、あとは忘れてかまいません。
世代仮説 — ほとんどのオブジェクトはすぐに死ぬ
JVMのGCが速い理由は世代仮説です。
> ほとんどのオブジェクトは短命で死ぬ。メソッド内で作られたnewやString.split()の結果など — メソッドが終われば使われなくなります。
この仮説を利用してヒープを2つの領域に分けます:
- ▸Young Generation — 新しく生成されたオブジェクト。ほとんどはここで死ぬ。
- ▸Old Generation — Youngから生き残ったオブジェクト。長生きする可能性が高い。
新しいオブジェクトはEdenに入ります。Minor GCを生き残るとSurvivor (S0)へ、さらに生き残ればS1へ — 複数回生き残るとOld Generationへ昇格します。
Minor GC vs Full GC
- ▸Minor GC: Young領域のみ清掃。頻繁・高速 (数十ミリ秒)。
- ▸Full GC: Oldまで清掃。まれだが低速 (数百ミリ秒〜数秒)。全スレッド停止 (Stop-The-World)。
本番環境でFull GCが頻繁に発生すると大きな問題です。レスポンスタイムが突然跳ね上がり、ユーザーは遅くなったと感じます。Old Genがよく埋まったり、メモリリークがあるサインかもしれません。
GCアルゴリズム — 選択可能
JVMは複数のGCアルゴリズムを提供します。どれを使うかはチューニングで決めます。
- ▸Serial GC — シングルスレッド。小規模アプリ・組み込み。
- ▸Parallel GC — マルチスレッド。スループット優先 (バッチ処理)。
- ▸G1 GC — Java 9+のデフォルト。予測可能な一時停止。ほとんどの場合に適切。
- ▸ZGC — Java 11+。一時停止1ミリ秒未満。大規模・低レイテンシ。
- ▸Shenandoah — ZGCと類似。RedHat主導。
-XX:+UseG1GCのようなJVMオプションで選択します。基本ガイドライン:小規模アプリはG1、大規模・低レイテンシはZGC。
まとめ
GCは自動メモリ管理という大きな利便性をもたらします。しかしタダではありません — Full GCが頻発するとレスポンス遅延が発生します。メモリ使用パターンを適切に設計し、JVMオプションをチューニングすることが実務の核心スキルです。
String — *不変オブジェクトの代表例*
Stringは変わらない
JavaのStringは不変 (Immutable)オブジェクトです。一度作られると、その内容を変えることができません。
concat・replace・toUpperCaseなどすべてのメソッドは新しいStringを生成して返します。元のオブジェクトには触れません。
なぜ不変なのか
不変であることには大きなメリットがあります:
- ▸スレッドセーフ: 複数のスレッドが同時アクセスしても変更されることがない
- ▸HashMapキーとして安全: キーとして使用可能 (ハッシュ値が変わらない)
- ▸セキュリティ: ファイルパス・URLを勝手に変更できない
- ▸String Pool最適化: 同じ値は共有可能
String Pool — 同じ文字列は共有
JVMはStringのための特別な空間 — String Pool — を持ちます。同じ内容のStringは1つだけ保存して共有します。
ここでJavaで最も混乱しやすいことが登場します。
== vs equals():
- ▸
==は参照比較 (同じオブジェクトか?) - ▸
equals()は値比較 (同じ内容か?)
Stringの比較は必ずequals()を使ってください。==で比較すると偶然合ったり外れたりします。
StringBuilder — 文字列連結の落とし穴
result += iは新しいStringを生成してresultに再代入します。1000回繰り返すと1000個の一時オブジェクトが生成されます。GCがフル稼働します。
StringBuilderを使うと内部バッファに積み重ねて、最後に一度だけStringに変換します。
> 💡 単純なa + b + cはコンパイラが自動的にStringBuilderに変換します。ループ内でのみ明示的に使用してください。
Text Block (Java 15+) — 複数行文字列
インデントや改行をそのまま維持しながらきれいに書けます。JSON・SQL・HTMLの記述に劇的に便利です。
まとめ
Stringは不変です。比較はequals()、繰り返し連結はStringBuilder、複数行はText Block。この3つを知るだけで日常コードの90%は安全です。
Java 17〜21 最新機能 — *コードが短くなる*
record (Java 14・16正式) — 不変データクラス
データだけを保持するシンプルなクラスを毎回コンストラクタ・getter・equals・hashCode・toStringすべて書くと50行になります。recordなら1行で済みます。
この1行が以下を自動生成します:
- ▸コンストラクタ
new User(1L, "ホン", "[email protected]") - ▸getter
u.id()・u.name()(getId()ではない — 少し異なる) - ▸
equals()・hashCode()・toString()
DTO・イベント・VOのようなシンプルなデータオブジェクトに最適です。Lombokの@Valueと同じ役割を言語レベルでサポートするものです。
sealed class (Java 17) — 許可されたサブクラスのみ
Shapeを実装できるクラスをCircle・Square・Triangleに制限します。他の場所でShapeを実装しようとするとコンパイルエラー。
なぜ便利なのか? switchパターンマッチングと組み合わせるとすべてのケースをコンパイラが検証してくれます。新しい図形が追加された場合、すべてのswitchを更新しないとコンパイルできない — 見落としミスがなくなります。
switch式 (Java 14) — 値を返す
旧switchは文だったため値を返せませんでした。モダンなswitchは式なので可能です:
->アロー構文 + break不要 + 値を返す + 複数caseのまとめ。旧switchのフォールスルーの罠もなくなりました。
パターンマッチング (Java 21) — if-instanceofにさようなら
instanceofと変数宣言を1行に。switchと組み合わせるとさらに強力です:
sealed classと組み合わせるとすべてのケースの強制も加わります。
Optional (Java 8) — nullの明示的な表現
User getUser()を呼んでnullの可能性があることを誰が知るでしょうか?誰も知りません。そしてNullPointerExceptionが爆発します。
戻り値の型にOptionalがあれば必ず空の可能性があることを認識させます。
> 💡 Optionalは戻り値型にのみ使ってください。フィールドやパラメータに使うのはアンチパターンです。
Virtual Thread (Java 21) — 並行性の革命
Collections + Functionalのlessonで扱ったその機能です。1万以上の並行タスクをOSスレッドの負担なく処理します。I/O待機中に自動でyield。JavaがLGo・Kotlinレベルの並行性を手に入れた出来事です。
まとめ
Java 17〜21の新機能はコードを短くし、コンパイラがより多くを検証できるようにします。record・sealed・switchパターン・Optional・Virtual Thread — 新規プロジェクトなら積極的に使用を推奨。既存プロジェクトは段階的に導入してください。
🎮 JVMメモリ・GC可視化
public class Hello {
public static void main(String[] args) {
System.out.println("안녕, Java!");
}
}
GCチューニングオプション — 実務でよく使う5つ
GCオプションが必要な理由
デフォルト設定のJVMは小メモリ + 一般ワークロード基準です。実際のサービスでは:
- ▸メモリを1GBから8GBに増やす必要があったり
- ▸レスポンス遅延が重要な場合にStop-the-world時間を短縮する必要があります
これをJVMオプション (-X, -XX)で調整します。
よく使う5つ
1. -Xms / -Xmx — ヒープメモリサイズ
- ▸
-Xms= 初期ヒープサイズ - ▸
-Xmx= 最大ヒープサイズ
実務のヒント:-Xms == -Xmxで同じ値に設定してください。動的拡張のコストを避け予測可能なパフォーマンスを実現します。AWS・k8s環境では事実上の標準です。
2. -XX:+UseG1GC — G1ガベージコレクター
Java 9+のデフォルト値 (Java 17もG1)。4GB以上のヒープでレスポンス遅延が短い。明示的に記載しておくと明確さの観点で良いです。
Java 11+ ZGC、Java 15+ Shenandoahがより短いpause timeを提供しますがHeap 16GB以上でなければG1で十分です。
3. -XX:MaxGCPauseMillis=200 — 最大GC停止時間の目標
JVMへの「GC1回で200ミリ秒を超えるな」というヒント。保証ではなく目標値です。レスポンス遅延SLAがあるサービスには必須。
4. -XX:+HeapDumpOnOutOfMemoryError — OOM時のダンプ自動生成
OOMが発生すると自動でヒープダンプを出力します。事後分析用 — 本番環境の必須オプション。
5. -XX:+PrintGCDetails (Java 8) / -Xlog:gc* (Java 9+) — GCログ
GCがいつ・どのくらい・なぜ発生したかを記録します。負荷テスト時に必須。
実務標準の組み合わせ (Spring Boot運用)
これらのオプションをすべて暗記する必要はありません。ただしオプションが存在するという事実となぜ使うかだけ知っていれば十分です — 面接で「GCチューニングをしたことはありますか?」と聞かれたときにこの5つを挙げられるようにしておくことが大切です。
☕ 自分で試す — String Pool・== vs equals
🤖 AIへのリクエスト例
このlessonの概念を知っていれば、AIに具体的に指示できます。漠然とした「直して」ではなく語彙を持ったリクエスト — それがトークン節約の出発点です。
- ▸「このSpring Bootアプリの運用JVMオプション (Xms/Xmx/G1GC/HeapDump) を推薦してください」
- ▸「このコードのOutOfMemoryErrorの原因をヒープダンプ分析の観点から診断してください」
- ▸「GCログオプションをJava 17形式で追加してください」
なぜこれがトークンを減らすのか
概念を知らないとAIの回答を受け取っても「それは何ですか?」とまた聞き直す必要があります。その「聞き直し」がトークンを消費します。概念を一度習得しておけば会話が一度で終わります。