業務でもJavaアプリの性能測定や負荷試験などを実施したり、メモリを増やしたりすることがあると思います。
でもそもそも「メモリを増やす」という結論へと至る前に、メモリ不足エラー(Out-of-Memory:OOM)が発生する流れを理解しておくべきと思い、本記事に改めてまとめさせていただきます。
全体構造
OSメモリ
└─ JVMプロセス
├─ Heap
│ ├─ Young
│ └─ Old
├─ Metaspace
├─ Thread Stack
├─ Direct Memory
└─ Code Cache
重要ポイント:
・-XmxはHeapのみ
・GC対象は基本的にHeap
・OOMはHeap以外でも発生する
| 領域 | 主な役割 | 確保タイミング | 解放タイミング | GC対象 | 補足ポイント |
|---|---|---|---|---|---|
| Heap(Young/Old) | newしたオブジェクト格納 | JVM起動時に-Xms分確保、必要に応じて-Xmxまで拡張 | GCで不要オブジェクト回収(ただしOSへ即返却とは限らない) | ○ | GCの主戦場。Old使用率が重要指標 |
| Metaspace | クラス情報・静的変数・メソッド情報 | クラスロード時に拡張 | クラスローダがGCされると解放 | × | 再デプロイ多いと肥大化 |
| Thread Stack | ローカル変数・メソッド呼び出し履歴 | スレッド生成時(-Xss分) | スレッド終了時 | × | スレッド数×Xssで計算される |
| Direct Memory | NIOバッファなど | allocateDirect使用時 | 対応オブジェクトGC後に解放(遅延あり) | ×(ヒープ外) | Heap空いていてもOOMあり |
| Code Cache | JITコンパイル済コード | 実行中にJITが生成 | 基本解放されない(JVM終了時) | × | 枯渇すると性能低下 |
フロー①:アプリ起動時
1.OSがJVMプロセス生成
2.Heapを-Xms分確保
3.Metaspace初期確保
4.メインスレッド生成(Thread Stack確保)
5.クラスロード(Metaspace使用)
6.JIT開始(Code Cache使用)
この時点で、Heap以外もすでに消費している。
フロー②:通常処理
new → Young(Eden)へ配置
Edenが満杯になるとMinor GC発生。
フロー③:Minor GC
対象:Youngのみ
流れ:
1.参照可能オブジェクト探索
2.生存オブジェクトをSurvivorへコピー
3.一定回数生存でOldへ昇格
4.不要オブジェクト破棄
ここで発生するのがStop The World(STW)
→日本語で言うと:「全アプリケーションスレッド一時停止」
GC中はユーザ処理が止まる。
フロー④:Old増加
長寿命オブジェクトが溜まる。
・キャッシュ
・セッション
・静的保持
フロー⑤:Mixed GC(G1の場合)
Oldの一部も掃除。それでも減らないとフロー⑥へ移行
フロー⑥:Full GC
対象:Young + Old全体
処理:
1.生存確認(Mark)
2.削除(Sweep)
3.圧縮(Compact)
特徴:
✔ STW長時間
✔ CPU高負荷
✔ サーバレスポンス低下
フロー⑦:減らない場合、OOM発生
Full GC後もOld使用率が高い場合、空き確保不能。
OutOfMemoryError発生
全体まとめフロー
起動
↓
Heap/Metaspace確保
↓
オブジェクト生成(Young)
↓
Minor GC
↓
Old蓄積
↓
Mixed GC
↓
Full GC
↓
減らない
↓
OOM
SEが本当に気をつけるべきこと
本質は「どのオブジェクトが、なぜ生き続けているか」
これを構造で理解できるかどうかが、OOM対応力の差になります。
しかしJavaアプリケーションでメモリ不足やGC多発が発生したとき、
まず検討されがちなのが次の対応です。
・Xmxを増やす
・サーバのメモリを増設する
・インスタンスサイズを上げる
確かに一時的な延命にはなります。
しかし多くの場合、それは根本解決ではありません。
以降に「不要なオブジェクトを長生きさせない」ためにはコーディングで何を気をつけるべきかを併せて列挙します。
①キャッシュの無制限保持
static Map cache = new HashMap<>();
・上限なし
・削除ロジックなし
・有効期限なし
これは Old世代肥大化確定コースの利用につながります。
対策:
・サイズ上限
・TTL設定
・LRU
・Caffeineなど利用
② セッションに巨大オブジェクト保持
session.setAttribute("userInfo", hugeObject);
セッションは基本長寿命。
・ファイル
・大量データList
・Entity丸ごと
これらを入れると Old固定化されてしまう。
原則:
・IDだけ持つ
・必要時DB再取得
・軽量DTO化
③ 静的保持(static確保大量)
public static List dataList = new ArrayList<>();
staticは「アプリ終了まで解放されない」つまり「GC対象にならない」(=メモリリークと同義)。特に危険なのは
・staticコレクション
・staticシングルトン
・static ThreadLocal
まとめ
| 意識 | 理由 |
|---|---|
| 使い終わったら参照を切る | 参照がある限りGCされない |
| スコープを狭くする | メソッド内変数は自然に消える |
| staticは極力使わない | ほぼ解放されない |
| キャッシュは設計する | なんとなく保持は危険な為 |
Javaメモリ設計で最も重要なのは「どこに確保されるか」ではなく「どれだけ長生きさせてしまうか」。
ここを意識できるSEは、GCログを見なくてもトラブルを未然に防げるかもしれません。