JVMメモリ構造の全体像
Javaアプリケーションのパフォーマンスを改善するには、まずJVMがどのようにメモリを管理しているかを理解する必要がある。JVMのメモリ領域は大きく分けて「ヒープ」「スタック」「メソッドエリア」「プログラムカウンタ」「ネイティブメソッドスタック」の5つに分かれる。特にチューニングの主戦場となるヒープ領域は、Young Generation(Eden + Survivor0/1)とOld Generationに分割され、デフォルトで1:2の比率でサイズが決まる。
クラスローディングの仕組み
ローダーの階層
クラスローダーは三層構造を取り、下位層から上位層へと委譲される。
// ローダーの実体を確認するサンプルコード
public class LoaderHierarchy {
public static void main(String[] args) {
ClassLoader app = LoaderHierarchy.class.getClassLoader();
ClassLoader ext = app.getParent();
ClassLoader boot = ext == null ? null : ext.getParent();
System.out.println("Application: " + app);
System.out.println("Extension : " + ext);
System.out.println("Bootstrap : " + boot);
}
}
委譲モデルの実装
loadClassメソッドは以下の流れで動作する。
- 既にロード済みかをfindLoadedClassでチェック
- 親ローダーに処理を委譲(parent.loadClass)
- 親が見つからなければ自身でfindClassで検索
- 見つかったらdefineClassでリンク
この仕組みにより、java.lang.Stringなどのコアクラスが意図せず書き換えられるリスクを回避できる。
ロード・リンク・初期化の3ステップ
- Loading:バイトコードをメモリに読み込み、Classオブジェクトを生成
- Linking:
- Verification:バイトコードの正当性検証
- Preparation:staticフィールドにデフォルト値を設定
- Resolution:シンボリック参照を直接参照に変換
- Initialization:staticブロックとstaticフィールドの明示的な値を実行
オブジェクトのライフサイクル
オブジェクトは以下のステップで生死を繰り返す。
- Eden領域に割り当てられる(Age=0)
- Minor GCで生存すればSurvivorへ移動しAgeをインクリメント
- Ageが閾値(多くは15)に達するとOld Generationへ昇格
- Old Generationが満杯になるとFull GCが発生
- 到達不能と判定されたオブジェクトが回収される
ガベージコレクションの判定アルゴリズム
根集合(GC Root)からの到達可能性
現在のJVMでは「Reference Counting」は採用されておらず、Root Tracing方式が使われている。Rootとなるオブジェクトは以下の通り。
- 各スレッドのスタックフレーム内のローカル変数
- staticフィールドに保持されている参照
- JNIハンドル
- 実行中のメソッドが保持する参照
主要なGCアルゴリズムと特徴
| アルゴリズム | 概要 | メリット | デメリット |
|---|---|---|---|
| Mark-Sweep | マーク→スイープの2段階 | 実装がシンプル | メモリ断片化が発生 |
| Copying | 使用領域を半分に分割し、生存オブジェクトをコピー | 断片化しない | メモリ効率が悪い |
| Mark-Compact | 生存オブジェクトを片側へ集約してからクリア | 断片化もメモリ効率も良好 | コストが高い |
代表的なガベージコレクタ
- Serial / Serial Old:シングルスレッドで動作。小規模アプリ向き。
- ParNew:Serialのマルチスレッド版。CMSと併用される。
- Parallel Scavenge / Parallel Old:スループット重視。バッチ処理に最適。
- CMS(Concurrent Mark-Sweep):低レイテンシを実現するが、フレージメント問題あり。
- G1(Garbage First):リージョン単位で管理し、予測可能な一時停止時間を提供。
Stop-The-World(STW)イベント
GC実行中にアプリケーションスレッドを一時停止させる現象。CMS/G1では以下のフェーズで発生する。
- Initial Mark:Root集合をスキャン
- Remark:並行マーク中の変更点を再スキャン
- Evacuation Pause(G1のみ):リージョンの再配置
三色マーキング
並行マークフェーズで使用される概念。
- White:未走査
- Gray:自身は走査済み、参照先は未走査
- Black:自身と参照先ともに走査済み
このモデルにより、マークフェーズとアプリケーションフェーズが同時に進行できる。
チューニング実践
JVM起動オプションの分類
- 標準オプション:-
option(例:-server) - 非標準オプション:-X
option(例:-Xmx2g) - 実験的オプション:-XX:
option(例:-XX:+UseG1GC)
# 実際に有効になっているフラグを確認
java -XX:+PrintFlagsFinal -version | grep UseG1GC
診断ツールの活用
本番環境でのボトルネック調査に役立つツールを以下に示す。
- Arthas:リアルタイムでメソッド実行時間やヒープダンプ取得が可能
- VisualVM:GUIベースでヒープやスレッドの可視化
- Java Flight Recorder:低オーバーヘッドで詳細なプロファイルを収集
# ArthasでCPU最上位メソッドを特定
dashboard -i 2000
profiler start --event cpu
profiler stop --file /tmp/cpu.html
パフォーマンス改善の指針
- GCログを収集し、一時停止時間と回収効率を把握
- ヒープダンプを取得し、メモリリーク箇所を特定
- オブジェクト割り当て率が高いメソッドを特定し、キャッシュやプールを導入
- CMSやG1のパラメータを調整し、フルGC頻度を削減
- アプリケーション要件に応じてGCアルゴリズムを選択(レスポンスタイム vs スループット)