ガベージコレクション(GC)完全ガイド
**GC(Garbage Collection)は「不要になったメモリを自動的に回収する」仕組みである。**
ガベージコレクション(GC)完全ガイド
GC(Garbage Collection)は「不要になったメモリを自動的に回収する」仕組みである。 プログラマの負担を劇的に軽減する一方、制御不能な停止時間(STW: Stop-The-World)という トレードオフを伴う。本ガイドでは GC の理論的基盤から主要言語の実装詳細、 チューニング戦略、そしてアンチパターンの回避まで体系的に解説する。
この章で学ぶこと
- GC が解決する問題と、GC が生み出す新たな課題を理解する
- マーク&スイープ、コピー GC、参照カウントなど主要アルゴリズムの動作原理を把握する
- 世代別仮説(Generational Hypothesis)の根拠と応用を理解する
- Java(G1/ZGC)、Go、Python、JavaScript(V8)の GC 実装を比較できる
- GC チューニングの基本戦略を習得する
- GC に起因するパフォーマンス問題を診断・解決できる
- 所有権システム(Rust)や手動管理(C/C++)との本質的な違いを説明できる
前提知識
このガイドを読む前に、以下の知識があると理解が深まります:
- 基本的なプログラミングの知識
- 関連する基礎概念の理解
- スタックとヒープ の内容を理解していること
目次
- なぜ GC が必要か
- GC の基本概念とルート集合
- マーク&スイープ(Mark and Sweep)
- 参照カウント(Reference Counting)
- コピー GC(Copying GC)
- 世代別 GC(Generational GC)
- 並行・インクリメンタル GC
- 主要言語の GC 実装比較
- GC チューニング戦略
- アンチパターンと回避策
- 演習問題(3段階)
- FAQ(よくある質問)
- まとめと次のステップ
- 参考文献
1. なぜ GC が必要か
1.1 手動メモリ管理の3大問題
プログラムが動的にメモリを確保する場合、そのメモリをいつ解放するかは本質的に困難な 問題である。C/C++ のような手動管理言語では、以下の3つのバグが繰り返し発生してきた。
手動メモリ管理の3大問題:
1. メモリリーク(Memory Leak)
─────────────────────────────────────────────────────
malloc() で確保 → 使用 → free() を忘れる → メモリ枯渇
長時間動作するサーバで致命的。メモリ使用量が単調増加し、
最終的に OOM Killer によりプロセスが強制終了される。
例: 1リクエストあたり 100B のリーク × 100万リクエスト/日
= 約 95MB/日 のメモリリーク
2. ダングリングポインタ(Dangling Pointer)
─────────────────────────────────────────────────────
free(ptr) → ... → *ptr にアクセス → 未定義動作(UB)
解放済みメモリの内容が偶然正しく見えることがあり、
問題の発覚が遅れる。セキュリティ脆弱性の主要因。
Use-After-Free(UAF)脆弱性として CVE に頻出。
3. 二重解放(Double Free)
─────────────────────────────────────────────────────
free(ptr) → ... → free(ptr) → ヒープ破壊・クラッシュ
メモリアロケータの内部データ構造が破壊され、
後続の malloc/free が予測不能な動作をする。
1.2 C言語での具体例
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// --- 問題1: メモリリーク ---
char* create_greeting(const char* name) {
// 呼び出し側が free する責任を負う(忘れがち)
char* buf = (char*)malloc(256);
if (!buf) return NULL;
snprintf(buf, 256, "Hello, %s!", name);
return buf; // 所有権が呼び出し側に移転
}
void process_request(const char* name) {
char* greeting = create_greeting(name);
printf("%s\n", greeting);
// free(greeting); ← これを忘れるとリーク!
}
// --- 問題2: ダングリングポインタ ---
int* get_local_ptr() {
int local_val = 42;
return &local_val; // ローカル変数へのポインタを返す(UB)
}
// --- 問題3: 二重解放 ---
void double_free_example() {
int* p = (int*)malloc(sizeof(int));
*p = 100;
free(p);
// ... 後続のコード ...
free(p); // 二重解放! ヒープ破壊
}
// --- GCがあれば全て解決 ---
// Java/Go/Python では上記3つの問題は構造的に発生しない1.3 GC のトレードオフ
GC は上記の問題を自動的に解決するが、代償として以下のオーバーヘッドが発生する。
GC のトレードオフ:| GC のメリット |
|---|
| + メモリリーク、ダングリングポインタ、二重解放が構造的に排除 |
| + 開発速度の向上(メモリ管理コードが不要) |
| + メモリ安全性の保証 |
| GC のデメリット |
| - STW(Stop-The-World)による予測不能な停止 |
| - メモリ使用量の増加(GC メタデータ + 遅延回収) |
| - CPU 使用率の増加(GC スレッドの実行) |
| - キャッシュ効率の低下(オブジェクトの再配置) |
| - リアルタイム性の保証が困難 |
性能特性の比較:
手動管理 ████████████████████████████ 最高性能(ただし安全性は低い)
GC付き ████████████████████░░░░░░░░ 典型的に 5-20% のオーバーヘッド
所有権 ██████████████████████████░░ 手動に近い性能 + 安全性
1.4 歴史的経緯
GC は1959年に John McCarthy が Lisp のために発明した。これはプログラミング言語の 歴史の中でも最も古く、最も重要な発明の一つである。
GC の歴史年表:
1959 McCarthy が Lisp 用に Mark & Sweep GC を発明
1960 Collins が参照カウント方式を提案
1963 Minsky が Copying GC を発明
1969 Fenichel & Yochelson が Semi-space Copying GC を開発
1984 Lieberman & Hewitt が世代別 GC を提案
1992 Boehm が保守的 GC(C/C++向け)を開発
2004 Bacon らが Real-time GC の研究を発表
2006 Java 6 で Parallel GC がデフォルトに
2014 Java 8 で G1 GC が成熟
2017 Go 1.8 で STW < 100μs を達成
2018 Java 11 で ZGC(実験的)導入
2021 Java 17 LTS で ZGC が本番利用可能に
2023 Java 21 LTS で世代別 ZGC が導入
2. GC の基本概念とルート集合
2.1 到達可能性(Reachability)
GC の中核概念は「到達可能性」である。ルート集合(Root Set)から参照のチェーンを 辿って到達できるオブジェクトは「生存」、到達できないオブジェクトは「ゴミ」と判断される。
到達可能性の判定:
ルート集合(GC Roots)| スタック上のローカル変数 |
|---|
| グローバル変数 / 静的変数 |
| JNI 参照(Java の場合) |
| アクティブスレッドのスタックフレーム |
| クラスローダーの参照(Java) |
| レジスタに格納されたポインタ |
│ │ │
▼ ▼ ▼
[Obj A] ──→ [Obj B] ──→ [Obj C] ← 到達可能(生存)
│
▼
[Obj D] ← 到達可能(生存)
[Obj E] ──→ [Obj F] ← 到達不能(ゴミ → 回収対象)
↑ │
└───────────┘ 循環参照しているが
ルートから到達不能なので回収対象
2.2 ルート集合の詳細
ルート集合の構成要素は言語・ランタイムによって異なる。以下は主要なルートの分類である。
ルート集合の分類:
種類 │ 説明 │ 例
─────────────────┼──────────────────────────────┼────────────────────
スタックルート │ メソッド/関数のローカル変数 │ int x = new ...
グローバルルート │ 静的フィールド、グローバル変数 │ static Map cache
レジスタルート │ CPU レジスタ内のポインタ │ JIT最適化で使用
JNI ルート │ ネイティブコードからの参照 │ JNI GlobalRef
ファイナライザ │ finalize待ちオブジェクト │ Weak/Phantom Ref
スレッドルート │ スレッドオブジェクト自体 │ Thread.currentThread
2.3 オブジェクトグラフとポインタ解析
GC は実行時にオブジェクトグラフを走査する。このグラフの構造によって GC の 効率が大きく変わる。
// Java: オブジェクトグラフの構築例
public class ObjectGraphDemo {
// ルートから辿れるオブジェクトグラフ
public static void main(String[] args) {
// root1: スタック変数(ルート)
List<String> list = new ArrayList<>();
list.add("Hello"); // list → "Hello"
list.add("World"); // list → "World"
// root2: ローカル変数(ルート)
Map<String, List<String>> map = new HashMap<>();
map.put("greetings", list); // map → list → {"Hello", "World"}
// 到達不能になるオブジェクト
{
byte[] temp = new byte[1024 * 1024]; // 1MB 確保
// ... temp を使用 ...
}
// ← ブロックを抜けた時点で temp は到達不能
// 次の GC で回収される
// map と list はまだルートから到達可能
System.out.println(map.get("greetings"));
}
}3. マーク&スイープ(Mark and Sweep)
3.1 アルゴリズムの概要
マーク&スイープは John McCarthy が1959年に Lisp のために発明した最初の GC アルゴリズムである。概念的に最もシンプルで、他の全ての GC アルゴリズムの基礎となっている。
マーク&スイープの2フェーズ:| Phase 1: Mark(マーク) ── 到達可能オブジェクトの探索 |
|---|
| 1. 全オブジェクトの mark ビットを 0 にリセット |
| 2. ルート集合からの参照を辿り、深さ優先探索(DFS) |
| 3. 到達可能なオブジェクトの mark ビットを 1 に設定 |
| Phase 2: Sweep(スイープ) ── ゴミの回収 |
| 1. ヒープ全体を線形走査 |
| 2. mark ビットが 0 のオブジェクトをフリーリストに返却 |
| 3. mark ビットが 1 のオブジェクトのビットを 0 にリセット |
3.2 動作の詳細な可視化
マーク&スイープの段階的な動作:
=== 初期状態 ===
ルート: [R1] ─→ [A] ─→ [B]
[R2] ─→ [C] ─→ [D]
↗
孤立: [E] ─→ [F] ─┘ ← E,F は到達不能(に見えるが D 経由で...)
実際のグラフ:
[R1]──→[A]──→[B]
↗
[R2]──→[C]──→[D]
[E]──→[F] ← ルートから到達不能
=== Phase 1: Mark ===
Step 1: R1 から探索開始
R1 → A (mark=1) → B (mark=1)
Step 2: R2 から探索開始
R2 → C (mark=1) → D (mark=1)
結果:
A(1) B(1) C(1) D(1) E(0) F(0)
=== Phase 2: Sweep ===
ヒープを走査:
A(1) → mark=0 にリセット、保持
B(1) → mark=0 にリセット、保持
C(1) → mark=0 にリセット、保持
D(1) → mark=0 にリセット、保持
E(0) → フリーリストに追加(回収) ★
F(0) → フリーリストに追加(回収) ★
回収されたメモリ: E + F のサイズ分
3.3 擬似コード実装
# マーク&スイープ GC の擬似コード実装
class Object:
def __init__(self, name, size):
self.name = name
self.size = size
self.marked = False
self.references = [] # 他オブジェクトへの参照
class MarkSweepGC:
def __init__(self, heap_size):
self.heap = [] # 全オブジェクトのリスト
self.roots = [] # ルート集合
self.free_list = [] # フリーリスト
self.heap_size = heap_size
self.used = 0
def allocate(self, name, size):
"""メモリ確保。空きがなければGCを実行"""
if self.used + size > self.heap_size:
self.collect()
if self.used + size > self.heap_size:
raise MemoryError("Out of memory after GC")
obj = Object(name, size)
self.heap.append(obj)
self.used += size
return obj
def collect(self):
"""GC の実行: Mark → Sweep"""
print("=== GC Start ===")
# Phase 1: Mark
for obj in self.heap:
obj.marked = False # 全ビットをリセット
for root in self.roots:
self._mark(root) # ルートから到達可能なオブジェクトをマーク
# Phase 2: Sweep
alive = []
freed = 0
for obj in self.heap:
if obj.marked:
obj.marked = False
alive.append(obj)
else:
freed += obj.size
print(f" Freed: {obj.name} ({obj.size} bytes)")
self.heap = alive
self.used -= freed
print(f"=== GC End: freed {freed} bytes ===")
def _mark(self, obj):
"""再帰的に到達可能オブジェクトをマーク(DFS)"""
if obj is None or obj.marked:
return
obj.marked = True
for ref in obj.references:
self._mark(ref)
# --- 使用例 ---
gc = MarkSweepGC(heap_size=1024)
# オブジェクト確保
a = gc.allocate("A", 100)
b = gc.allocate("B", 200)
c = gc.allocate("C", 150)
d = gc.allocate("D", 100)
# 参照グラフ構築
a.references.append(b) # A → B
b.references.append(c) # B → C
# ルート設定(A のみルートから到達可能)
gc.roots = [a]
# GC 実行 → D は到達不能なので回収される
gc.collect()
# 出力:
# === GC Start ===
# Freed: D (100 bytes)
# === GC End: freed 100 bytes ===3.4 マーク&スイープの利点と欠点
| マーク&スイープの評価 | |
|---|---|
| 利点 | 詳細 |
| 循環参照の処理 | 参照カウントと異なり、循環参照を |
| 正しく検出・回収できる | |
| 実装のシンプルさ | 2つのフェーズで構成される |
| 基本的な理解が容易 | |
| 確保コストゼロ | オブジェクト確保時に追加の |
| 管理コストが不要 | |
| 欠点 | 詳細 |
| STW(全停止) | マーク・スイープ中はアプリケーション |
| 全体が停止する | |
| メモリ断片化 | 回収後にメモリの穴ができ、 |
| 大きな連続領域の確保が困難になる | |
| ヒープ全走査 | スイープフェーズで全ヒープを走査 |
| ヒープが大きいと時間がかかる |
3.5 断片化問題の図解
メモリ断片化の問題:
GC 前:| A | B | C | D | E | F | G | H |
|---|---|---|---|---|---|---|---|
| 100B | 200B | 150B | 100B | 300B | 50B | 200B | 100B |
GC 後(B, D, F が回収):| A | (空) | C | (空) | E | (空) | G | H |
|---|---|---|---|---|---|---|---|
| 100B | 200B | 150B | 100B | 300B | 50B | 200B | 100B |
空き合計: 350B あるが、最大の連続空き領域は 200B
→ 250B のオブジェクトを確保できない!(外部断片化)
解決策: コンパクション(Compaction)| A | C | E | G | H | 空き(350B) |
|---|---|---|---|---|---|
| 100B | 150B | 300B | 200B | 100B |
→ 連続した350Bの空き領域を確保できる
→ ただしコンパクションにはポインタの書き換えが必要(高コスト)
4. 参照カウント(Reference Counting)
4.1 アルゴリズムの概要
参照カウントは、各オブジェクトが「何個のポインタから参照されているか」を記録する 方式である。参照カウントが 0 になった時点で即座にオブジェクトを回収する。
参照カウントの基本動作:
操作 カウント変化
─────────────────────────────────────────────
a = new Obj() Obj.rc = 1
b = a Obj.rc = 2
a = null Obj.rc = 1
b = null Obj.rc = 0 → 即座に回収!
タイムライン:
時刻 操作 a b Obj.rc 状態
────────────────────────────────────────────────
T1 a=new() →Obj - 1 生存
T2 b=a →Obj →Obj 2 生存
T3 a=null null →Obj 1 生存
T4 b=null null null 0 ★回収★
4.2 Python の参照カウント実装
Python は参照カウントをメインの GC 機構として採用している唯一の主要言語である。
import sys
import gc
# === 参照カウントの観察 ===
class MyObject:
def __init__(self, name):
self.name = name
def __del__(self):
print(f" デストラクタ呼出: {self.name} を回収")
print("--- 参照カウントの基本 ---")
obj = MyObject("Alpha")
print(f"参照カウント: {sys.getrefcount(obj) - 1}")
# getrefcount 自体の一時参照分を -1 する
ref2 = obj
print(f"参照追加後: {sys.getrefcount(obj) - 1}")
del ref2
print(f"参照削除後: {sys.getrefcount(obj) - 1}")
del obj # rc=0 → 即座にデストラクタが呼ばれる
print("del obj の直後")
# 出力:
# --- 参照カウントの基本 ---
# 参照カウント: 1
# 参照追加後: 2
# 参照削除後: 1
# デストラクタ呼出: Alpha を回収
# del obj の直後
print("\n--- 循環参照の問題 ---")
# 参照カウントだけでは回収できないケース
a = MyObject("CycleA")
b = MyObject("CycleB")
a.partner = b # a → b
b.partner = a # b → a(循環参照)
# 外部参照を削除しても rc は 0 にならない
del a # CycleA.rc: 2→1(b.partner からまだ参照されている)
del b # CycleB.rc: 2→1(a.partner からまだ参照されている)
# → デストラクタは呼ばれない!
# → Python の世代別 GC(サイクルコレクタ)が回収する
print("手動で gc.collect() を呼ぶ:")
collected = gc.collect()
print(f"回収されたオブジェクト数: {collected}")
# 出力:
# --- 循環参照の問題 ---
# 手動で gc.collect() を呼ぶ:
# デストラクタ呼出: CycleA を回収
# デストラクタ呼出: CycleB を回収
# 回収されたオブジェクト数: 24.3 循環参照問題の図解
循環参照問題:
=== 外部参照がある状態 ===
ルート| b ───┼──→ ↑ |
|---|
rc=2 の内訳:
ObjA: ルート a (1) + ObjB.partner (1) = 2
ObjB: ルート b (1) + ObjA.partner (1) = 2
=== del a, del b の後 ===
ルート| ↑ |
|---|
rc=1 の内訳:
ObjA: ObjB.partner (1) = 1 ← 0 にならない!
ObjB: ObjA.partner (1) = 1 ← 0 にならない!
→ 参照カウント方式だけでは回収不可能
→ 別途「サイクルコレクタ」が必要
4.4 参照カウントの利点と欠点
参照カウント方式の評価:| 利点 |
|---|
| 1. 即時回収: rc=0 になった瞬間にメモリが解放される |
| → デストラクタの実行タイミングが予測可能 |
| → メモリ使用量のピークが低く抑えられる |
| 2. STW なし: GC による一括停止が発生しない |
| → レイテンシが予測しやすい |
| 3. 局所性: 参照の変更時にのみコストが発生 |
| → GC に比べてコストが分散される |
| 欠点 |
| 1. 循環参照: 参照カウントだけでは回収不可能 |
| → 別途サイクルコレクタが必要(Python の実装) |
| 2. カウント更新のオーバーヘッド |
| → 全ての参照操作で原子的な inc/dec が必要 |
| → マルチスレッド環境では特に高コスト |
| 3. 連鎖的解放: 大きなデータ構造の解放が連鎖し、 |
| 一時的に長い停止が発生する可能性がある |
| 例: 100万ノードの木を解放 → 100万回の再帰的 free |
| 4. メモリオーバーヘッド: 各オブジェクトに rc フィールド |
| が必要(通常 4-8 バイト) |
4.5 Swift の ARC(Automatic Reference Counting)
Swift は参照カウントをコンパイラレベルで自動化した ARC を採用している。
プログラマが手動で retain/release を書く必要はないが、循環参照は
weak / unowned キーワードで明示的に断ち切る必要がある。
// Swift: ARC の動作と循環参照の回避
class Person {
let name: String
var apartment: Apartment? // 強参照
init(name: String) {
self.name = name
print("\(name) を確保")
}
deinit {
print("\(name) を解放")
}
}
class Apartment {
let unit: String
weak var tenant: Person? // 弱参照(循環参照の回避)
init(unit: String) {
self.unit = unit
print("Apt \(unit) を確保")
}
deinit {
print("Apt \(unit) を解放")
}
}
// 使用例
var john: Person? = Person(name: "John") // John.rc = 1
var apt: Apartment? = Apartment(unit: "101") // Apt101.rc = 1
john!.apartment = apt // Apt101.rc = 2(強参照)
apt!.tenant = john // John.rc = 1(weak なので rc は増えない)
john = nil // John.rc = 0 → 解放、Apt101.rc = 1
apt = nil // Apt101.rc = 0 → 解放
// weak を使わないと:
// john = nil → John.rc=1(apt.tenant が強参照)→ 解放されない!
// apt = nil → Apt101.rc=1(john.apartment が強参照)→ 解放されない!
// → メモリリーク4.6 参照カウントとマーク&スイープの比較
| 特性 | 参照カウント | マーク&スイープ |
|---|---|---|
| 回収タイミング | 即時(rc=0時) | GC実行時 |
| STW | なし | あり(全停止) |
| 循環参照 | 処理不可 | 処理可能 |
| CPU オーバーヘッド | 分散的(常時) | 集中的(GC時) |
| メモリ効率 | 高い(即時回収) | やや低い(遅延回収) |
| 実装の複雑さ | 低い | 中程度 |
| スレッド安全性 | 原子操作が必要 | GCスレッドで一括 |
| 連鎖解放 | 発生する | 発生しない |
| 採用言語 | Python, Swift, | Java, Go, JS, |
| Objective-C, Perl | Ruby, C# |
5. コピー GC(Copying GC)
5.1 セミスペース方式の原理
コピー GC はヒープを2つの等しいサイズの空間(セミスペース)に分割し、 生存オブジェクトを一方から他方へコピーする方式である。1963年に Marvin Minsky が 提案し、1969年に Fenichel と Yochelson が現在知られる形に洗練した。
セミスペース・コピー GC の動作:
=== GC 前 ===
From空間(アクティブ) To空間(空き)| [A 100B] [dead] [B 80B] | ||
|---|---|---|
| [dead] [C 60B] [dead] | (未使用) | |
| [D 40B] [dead] [dead] |
生存: A,B,C,D 死亡: 5個
=== GC 実行(コピー) ===
ルートから到達可能なオブジェクトだけを To にコピー:
From空間 To空間| [A' 100B][B' 80B] | ||
|---|---|---|
| (全体を破棄) | ←→ | [C' 60B] [D' 40B] |
| (空き領域) |
From と To を交換
=== GC 後 ===
From空間(旧 To、新アクティブ) To空間(旧 From、空き)| [A' 100B][B' 80B] | ||
|---|---|---|
| [C' 60B] [D' 40B] | (未使用) | |
| (空き領域) |
特徴:
- コンパクションが自動的に行われる(断片化なし)
- 生存オブジェクト数に比例するコスト(死亡オブジェクトは無視)
- メモリの半分しか使えないというトレードオフ
5.2 フォワーディングポインタ
コピー GC では、コピー元のオブジェクトに「フォワーディングポインタ」を設置して、 他のオブジェクトからの参照を新しいアドレスに転送する。
フォワーディングポインタの仕組み:
Step 1: ルートから A を発見、To にコピー
From: [A] ──(forwarding)──→ To: [A']
↑
Step 2: B を発見(A を参照している)
From: [B] → [A のfwd] → To: [A']
↓ コピー
To: [B'] → [A'] ← 参照先が自動的に更新される
Step 3: C を発見(A を参照している)
From: [C] → [A のfwd] → To: [A'] ← 既にコピー済み、再コピーしない
↓ コピー
To: [C'] → [A']
→ 全ての参照が新しいアドレスを指すように自動的に書き換えられる
5.3 Cheney のアルゴリズム
1970年に C.J. Cheney が提案した、スタックを使わない幅優先探索(BFS)ベースの コピー GC アルゴリズムは、再帰を使わないため実装が効率的である。
# Cheney のコピー GC アルゴリズム(擬似コード)
class CheneyGC:
def __init__(self, space_size):
self.space_size = space_size
# From 空間と To 空間
self.from_space = bytearray(space_size)
self.to_space = bytearray(space_size)
self.alloc_ptr = 0 # From 空間の確保位置
self.scan = 0 # To 空間のスキャンポインタ
self.free = 0 # To 空間の空きポインタ
def collect(self, roots):
"""GC 実行: Cheney のBFSコピー"""
self.scan = 0
self.free = 0
# Step 1: ルートから直接参照されるオブジェクトをコピー
new_roots = []
for root in roots:
new_roots.append(self._copy(root))
# Step 2: BFS でコピー済みオブジェクトの参照先もコピー
while self.scan < self.free:
obj = self._object_at(self.to_space, self.scan)
for i, ref in enumerate(obj.references):
obj.references[i] = self._copy(ref)
self.scan += obj.size
# Step 3: From と To を交換
self.from_space, self.to_space = self.to_space, self.from_space
self.alloc_ptr = self.free
return new_roots
def _copy(self, obj):
"""オブジェクトを To 空間にコピー(未コピーの場合のみ)"""
if obj.forwarding is not None:
return obj.forwarding # 既にコピー済み
# To 空間にコピー
new_obj = self._copy_bytes(obj, self.to_space, self.free)
self.free += obj.size
# フォワーディングポインタを設置
obj.forwarding = new_obj
return new_obj5.4 コピー GC の性能特性
コピー GC の性能分析:
生存率(生存オブジェクト / 全オブジェクト)と GC コストの関係:
コスト
高 │ ╱
│ ╱
│ ╱
│ ╱
│ ╱
│ ╱
│ ╱
│ ╱ ← コピー GC: 生存率に比例
│ ╱
│ ╱
│ ╱
│─────╱──────────────────────── ← マーク&スイープ: ほぼ一定
│ ╱ (全ヒープ走査のため)
低 │╱
└──────────────────────────────→ 生存率
0% 100%
結論:
- 生存率が低い場合(<50%): コピー GC が有利
- 生存率が高い場合(>50%): マーク&スイープが有利
- Young 世代は生存率が低い → コピー GC が最適
- Old 世代は生存率が高い → マーク&スイープが最適
6. 世代別 GC(Generational GC)
6.1 世代別仮説(Generational Hypothesis)
世代別 GC は「ほとんどのオブジェクトは若くして死ぬ」(Infant Mortality)という 経験則に基づく最適化手法である。この仮説は1984年に David Ungar が Smalltalk の 研究で体系化した。
世代別仮説の根拠:
オブジェクトの寿命分布(典型的なアプリケーション):
オブジェクト数
多 │██
│██
│██
│██░░
│██░░
│██░░░░
│██░░░░░░
│██░░░░░░░░
│██░░░░░░░░░░░░░░
│██░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
少 │██░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
└──────────────────────────────────────────────────────→ 寿命
短 長
█ = Young 世代で死亡(80-98%)
░ = Old 世代まで生存(2-20%)
具体例:
- メソッドのローカル変数、一時オブジェクト → すぐ死亡
- イテレータ、ラムダのクロージャ → すぐ死亡
- キャッシュ、設定オブジェクト → 長期生存
- シングルトン → アプリ終了まで生存
6.2 Java HotSpot の世代別構成
Java HotSpot VM のヒープ構成:| Java ヒープ | ||||||
|---|---|---|---|---|---|---|
| Young Generation(若い世代) Old Generation(古い世代) | ||||||
| ┌────────┬──────┬──────┐ ┌──────────────────────┐ | ||||||
| Eden | S0 | S1 | ||||
| (from) | (to) | 昇格 | Tenured Space | |||
| 新規 | ────────→ | |||||
| オブジェ | サバイ | サバイ | 長寿命オブジェクト | |||
| クト | バ 0 | バ 1 | ||||
| └────────┴──────┴──────┘ └──────────────────────┘ | ||||||
| ←── Minor GC(高頻度)──→ ←── Major GC(低頻度)──→ | ||||||
| Metaspace(Java 8+: ネイティブメモリ) | ||||||
| ┌──────────────────────────────────────────────────────────┐ | ||||||
| クラスメタデータ、メソッド情報、定数プール | ||||||
| └──────────────────────────────────────────────────────────┘ |
デフォルトの比率:
- Young : Old = 1 : 2(-XX:NewRatio=2)
- Eden : S0 : S1 = 8 : 1 : 1(-XX:SurvivorRatio=8)
6.3 Minor GC の詳細な動作
Minor GC のステップバイステップ:
=== Step 1: Eden が満杯 ===
Eden S0(from) S1(to) Old| A B C D E | F | Z | ||||
|---|---|---|---|---|---|---|
| (全て新規) | age1 |
※ B, D は既に到達不能
=== Step 2: Minor GC 実行 ===
- Eden の生存オブジェクト(A,C,E)を S1(to) にコピー
- S0 の生存オブジェクト(F, age=1)を S1(to) にコピー(age+1)
- B, D は到達不能なのでコピーされない(回収)
Eden S0(from) S1(to) Old| (空) | (空) | A | Z | |||
|---|---|---|---|---|---|---|
| C | ||||||
| E | age0 | |||||
| F | age2 |
=== Step 3: S0 と S1 の役割を交換 ===
Eden S0(=旧S1) S1(=旧S0) Old| (空) | A | Z | ||||
|---|---|---|---|---|---|---|
| C | (空) | |||||
| E | age0 | |||||
| F | age2 |
=== Step 4: 次の Minor GC で F(age>=閾値) を Old へ昇格 ===
- MaxTenuringThreshold(デフォルト15)に達したら昇格
- Survivor が溢れた場合も Old に直接昇格
6.4 書き込みバリア(Write Barrier)
世代別 GC では、Old から Young への参照を追跡する必要がある。これを実現するのが 書き込みバリアである。
書き込みバリアの必要性:
問題: Old → Young への参照
Old Young| OldObj | ──ref──→ | YoungObj |
|---|---|---|
Minor GC は Young 世代だけを走査する。
しかし OldObj → YoungObj の参照を見落とすと、
YoungObj を誤って回収してしまう!
解決策: 書き込みバリア + Remembered Set
1. Old オブジェクトへの書き込み時にバリアが発動
2. Old → Young の参照を Remembered Set(カードテーブル)に記録
3. Minor GC 時に Remembered Set もルートとして扱う
カードテーブル: ┌───┬───┬───┬───┬───┬───┬───┬───┐
│ 0 │ 0 │ 1 │ 0 │ 0 │ 1 │ 0 │ 0 │ ← 各カードは512Bの領域に対応
└───┴───┴───┴───┴───┴───┴───┴───┘↑ ↑
このカードの領域に このカードの領域に
Young への参照あり Young への参照あり
→ dirty なカードの領域だけスキャンすればよい
6.5 V8(JavaScript)の世代別 GC
V8 エンジンの GC アーキテクチャ(Orinoco):| Young Generation(Scavenger) | ||||
|---|---|---|---|---|
| Semi-space 方式(コピー GC) | ||||
| ┌──────────────┐ ┌──────────────┐ | ||||
| From-space | To-space | |||
| 新規オブジェ | (GC後の生存 | |||
| クトを確保 | オブジェクト) | |||
| └──────────────┘ └──────────────┘ | ||||
| - サイズ: 1-8MB(デフォルト) | ||||
| - 2回の Scavenge を生き延びたら Old へ昇格 | ||||
| - 停止時間: 1-2ms | ||||
| Old Generation(Mark-Sweep-Compact) | ||||
| ┌──────────────────────────────────────────────────┐ | ||||
| インクリメンタルマーキング: | ||||
| GC を小さなステップに分割し、メインスレッドの | ||||
| 停止を最小化 | ||||
| 並行マーキング: | ||||
| ワーカースレッドでマーキングを並行実行 | ||||
| 並行スイープ: | ||||
| メインスレッドの実行中にバックグラウンドでスイープ | ||||
| コンパクション(必要な場合のみ): | ||||
| 断片化が閾値を超えた場合に実行 | ||||
| └──────────────────────────────────────────────────┘ | ||||
| - サイズ: 数百MB〜数GB | ||||
| - Idle-Time GC: ブラウザのアイドル時間に GC を実行 |
6.6 Node.js での V8 GC チューニング
// Node.js: V8 GC の挙動観察とチューニング
// --- GC イベントの監視 ---
// 起動オプション: node --expose-gc --trace-gc app.js
// ヒープ統計の取得
const v8 = require('v8');
function printHeapStats() {
const stats = v8.getHeapStatistics();
console.log('=== V8 Heap Statistics ===');
console.log(` Total heap size: ${(stats.total_heap_size / 1024 / 1024).toFixed(2)} MB`);
console.log(` Used heap size: ${(stats.used_heap_size / 1024 / 1024).toFixed(2)} MB`);
console.log(` Heap size limit: ${(stats.heap_size_limit / 1024 / 1024).toFixed(2)} MB`);
console.log(` External memory: ${(stats.external_memory / 1024 / 1024).toFixed(2)} MB`);
console.log(` Malloced memory: ${(stats.malloced_memory / 1024 / 1024).toFixed(2)} MB`);
console.log(` Number of contexts: ${stats.number_of_native_contexts}`);
}
// --- メモリリークの検出パターン ---
// 問題のあるコード: クロージャによるリーク
const leakyCache = [];
function processRequest(data) {
// クロージャが data を捕捉し続ける → GC が回収できない
const handler = () => {
return data.length; // data への参照を保持
};
leakyCache.push(handler); // 配列に蓄積 → メモリリーク
}
// 改善: WeakRef を使用(Node.js 14.6+)
const cache = new Map();
function processRequestFixed(id, data) {
// WeakRef: GC が必要に応じて回収できる
cache.set(id, new WeakRef(data));
}
function getCachedData(id) {
const ref = cache.get(id);
if (ref) {
const data = ref.deref(); // null の場合は GC に回収された
if (data) return data;
cache.delete(id); // 回収済みエントリを削除
}
return null;
}
// --- チューニングオプション ---
// node --max-old-space-size=4096 app.js # Old 世代を 4GB に
// node --max-semi-space-size=64 app.js # Young 世代を 64MB に
// node --expose-gc app.js # global.gc() を有効化
// node --trace-gc app.js # GC イベントをログ出力
// node --gc-interval=100 app.js # GC 間隔の調整
printHeapStats();7. 並行・インクリメンタル GC
7.1 STW 問題と解決アプローチ
マーク&スイープや世代別 GC の最大の課題は STW(Stop-The-World)である。 GC 実行中にアプリケーションのスレッドが全て停止するため、レスポンスタイムに 予測不能なスパイクが発生する。
STW の影響:
時間軸 ──────────────────────────────────────────→
アプリ ████████████│ │████████████████████
スレッド │ STW ! │
│ GC実行 │
│ (50ms) │
│ │
レスポンス │ │
タイム 2ms 3ms │ 53ms!! │ 2ms 2ms 3ms
↑
ここでリクエストを受けたら
50ms のレイテンシスパイク
解決アプローチ:| 1. インクリメンタル GC: GC を小分けにして実行 |
|---|
| 2. 並行(Concurrent)GC: バックグラウンド実行 |
| 3. 並列(Parallel)GC: 複数スレッドで GC 実行 |
| 4. 上記の組み合わせ |
7.2 三色マーキング(Tri-color Marking)
並行 GC の正確性を保証するための核心技術が三色マーキングである。 1975年に Dijkstra らが提案した。
三色マーキング:
色の定義:
■ 黒(Black): 走査完了。自身も全ての子もマーク済み
░ 灰(Gray): 自身はマーク済みだが、子の走査がまだ
□ 白(White): 未走査。GC 終了時に白なら回収対象
マーキングの進行:
Step 0(初期状態): 全オブジェクト白、ルートの直接参照先を灰に ┌──────┐
│ root │─→ ░A ─→ □B ─→ □C
└──────┘ │└─→ □D ─→ □E
Step 1: A を処理(灰→黒)、A の子を灰に ┌──────┐
│ root │─→ ■A ─→ ░B ─→ □C
└──────┘ │└─→ ░D ─→ □E
Step 2: B を処理(灰→黒)、B の子を灰に ┌──────┐
│ root │─→ ■A ─→ ■B ─→ ░C
└──────┘ │└─→ ░D ─→ □E
Step 3: C を処理(灰→黒)、子なし ┌──────┐
│ root │─→ ■A ─→ ■B ─→ ■C
└──────┘ │└─→ ░D ─→ □E
Step 4: D を処理(灰→黒)、D の子を灰に ┌──────┐
│ root │─→ ■A ─→ ■B ─→ ■C
└──────┘ │└─→ ■D ─→ ░E
Step 5: E を処理(灰→黒) ┌──────┐
│ root │─→ ■A ─→ ■B ─→ ■C
└──────┘ │└─→ ■D ─→ ■E
完了: 灰オブジェクトがなくなったらマーキング終了
→ 白のオブジェクトはルートから到達不能 → 回収
7.3 並行 GC の不変条件と書き込みバリア
並行 GC ではアプリケーション(Mutator)と GC(Collector)が同時に動作するため、 参照の変更を正しく追跡する必要がある。
並行 GC のロストオブジェクト問題:
Mutator が GC 中に参照を変更すると、生存オブジェクトが
誤って回収される可能性がある(ロストオブジェクト問題)。
=== 問題のシナリオ ===
1. GC が ■A を走査済み、░B を走査中
■A ─→ ░B ─→ □C
2. Mutator が以下の操作を同時に実行:
A.ref = C (A から C への参照を追加)
B.ref = null (B から C への参照を削除)
3. 結果:
■A ─→ □C A は既に黒なので再走査されない
■B B からの C への参照は削除済み
→ C は到達可能なのに白のまま → 誤って回収される!
=== 解決策: 書き込みバリア ===
Dijkstra バリア(スナップショット方式):
参照の書き込み時に、新しい参照先を灰に変更
→ 「新しく参照されるオブジェクトは確実に走査される」
Yuasa バリア(削除バリア):
参照の削除時に、削除される参照先を灰に変更
→ 「削除される参照先は確実に走査される」
Steele バリア(インクリメンタルアップデート):
黒オブジェクトに新しい参照が追加されたら、黒→灰に戻す
→ 「参照を追加された黒オブジェクトは再走査される」
7.4 Go のゴルーチンと並行 GC
// Go: 並行 GC の実践
package main
import (
"fmt"
"runtime"
"runtime/debug"
"time"
)
func main() {
// === GC 統計の取得 ===
var stats debug.GCStats
debug.ReadGCStats(&stats)
fmt.Printf("GC 回数: %d\n", stats.NumGC)
fmt.Printf("最後の GC: %v\n", stats.LastGC)
// === メモリ統計の取得 ===
var m runtime.MemStats
runtime.ReadMemStats(&m)
fmt.Printf("ヒープ使用量: %.2f MB\n", float64(m.HeapAlloc)/1024/1024)
fmt.Printf("ヒープ確保量: %.2f MB\n", float64(m.HeapSys)/1024/1024)
fmt.Printf("GC 回数: %d\n", m.NumGC)
fmt.Printf("GC 停止時間合計: %v\n", time.Duration(m.PauseTotalNs))
// === GOGC の設定 ===
// GOGC=100(デフォルト): ヒープが100%成長したら GC
// GOGC=50: より頻繁に GC → メモリ節約、CPU 消費増
// GOGC=200: GC 頻度減 → スループット向上、メモリ消費増
// GOGC=off: GC を無効化(特殊用途のみ)
old := debug.SetGCPercent(50)
fmt.Printf("旧 GOGC: %d, 新 GOGC: 50\n", old)
// === GOMEMLIMIT の設定(Go 1.19+)===
// ソフトなメモリ上限を設定
// GOGC と組み合わせて使用するのが推奨
debug.SetMemoryLimit(512 * 1024 * 1024) // 512MB
// === 手動 GC の実行 ===
runtime.GC() // 即座に GC を実行
// === GC 完了の待機 ===
// runtime.GC() は GC が完了するまでブロックする
// === ファイナライザの設定 ===
type Resource struct {
name string
}
r := &Resource{name: "database-connection"}
runtime.SetFinalizer(r, func(res *Resource) {
fmt.Printf("ファイナライザ: %s を解放\n", res.name)
})
// r が到達不能になったとき、次の GC でファイナライザが実行される
// 注意: ファイナライザの実行タイミングは保証されない
}8. 主要言語の GC 実装比較
8.1 Java の GC コレクタ一覧
Java GC コレクタの進化:| コレクタ | 導入 | ターゲット | 特徴 |
|---|---|---|---|
| Serial GC | JDK 1.0 | 小規模 | シングルスレッド |
| クライアント | STW あり | ||
| Parallel GC | JDK 1.4 | スループット | 複数スレッドで GC 実行 |
| (Throughput) | 重視 | STW あり | |
| CMS | JDK 1.4 | 低レイテンシ | 並行マーク&スイープ |
| (非推奨) | Java 14 で削除 | ||
| G1 GC | JDK 7 | バランス型 | Region ベース |
| (デフォルト) | (Java 9) | 停止時間目標を設定可能 | |
| ZGC | JDK 11 | 超低レイテンシ | 停止時間 < 1ms |
| (JDK 15) | 大規模ヒープ | 最大 16TB ヒープ対応 | |
| カラーポインタ | |||
| Shenandoah | JDK 12 | 超低レイテンシ | ZGC と同等の低レイテンシ |
| ブルックスポインタ | |||
| Red Hat 主導 | |||
| Generational | JDK 21 | 最新の推奨 | ZGC + 世代別の組合せ |
| ZGC | Young/Old 世代を分離 |
8.2 G1 GC の Region ベースアーキテクチャ
G1 GC のヒープ構造:| E | E | S | O | O | H | O | E |
|---|---|---|---|---|---|---|---|
| O | E | O | O | E | H | S | O |
| E | O | O | S | O | O | E | O |
| O | O | E | O | O | E | O | O |
E = Eden Region S = Survivor Region
O = Old Region H = Humongous Region(大オブジェクト用)
動作:
1. ヒープを 1-32MB の Region に分割(通常 2048 個)
2. 各 Region は E/S/O/H のいずれかの役割を持つ
3. Mixed GC: 回収効率の高い Region を優先的に回収
→ "Garbage First" の名前の由来
4. 停止時間目標(-XX:MaxGCPauseMillis=200)に収まるよう
回収する Region 数を調整
Java コマンドライン:
java -XX:+UseG1GC \
-XX:MaxGCPauseMillis=200 \
-XX:G1HeapRegionSize=4m \
-Xms4g -Xmx4g \
-Xlog:gc*:file=gc.log \
MyApplication
8.3 ZGC の革新的技術
ZGC(Z Garbage Collector)のアーキテクチャ:
核心技術: カラーポインタ(Colored Pointer)
64ビットポインタの構成:| 63 47 | 46 | 45 | 44 | 43 | 42 | 41 0 |
|---|---|---|---|---|---|---|
| (未使用) | M | R | F | Md | Met | オブジェクトアドレス |
| a | e | i | a | (42ビット = 4TB) | ||
| r | m | n | ||||
| k | a | a | ||||
| e | p | l | ||||
| d |
- Marked: マーキング済みフラグ
- Remapped: 再配置済みフラグ
- Finalizable: ファイナライザ保留フラグ
ZGC のフェーズ:
1. Pause Mark Start ← STW(極短: < 1ms)
ルートのスキャンのみ
2. Concurrent Mark ← アプリと並行
オブジェクトグラフの走査
3. Pause Mark End ← STW(極短: < 1ms)
参照処理の完了
4. Concurrent Relocate ← アプリと並行
オブジェクトの再配置
Java コマンドライン:
java -XX:+UseZGC \
-XX:+ZGenerational \ # 世代別 ZGC(JDK 21+推奨)
-Xms8g -Xmx8g \
-Xlog:gc*:file=gc.log \
MyApplication
8.4 Go の並行マーク&スイープ
Go の GC 特性:
設計思想:
- 世代別ではない(シンプルさを重視)
- 並行マーク&スイープ
- STW を最小化(目標 < 500μs)
- レイテンシ重視(スループットよりレイテンシを優先)
GC サイクル:| ┌─STW─┐ ┌─STW─┐ | ||||||
| Mark | Concurrent Mark & Sweep | Mark | ||||
| Start | Term | |||||
| ┌──────────────────────────┐ | ||||||
| <1ms | Mutator と GC が並行動作 | <1ms | ||||
| └─────┘ └──────────────────────────┘ └─────┘ | ||||||
| STW1 並行マーキング STW2 スイープ | ||||||
| (短い) (アプリ実行中) (短い) (並行) |
GC ペーシング:
- GOGC=100: ヒープが前回GC後の2倍になったらGC開始
- GOMEMLIMIT: ソフトメモリ上限(Go 1.19+)
- GC はヒープの成長率に基づいて開始タイミングを調整
チューニングパラメータ:| パラメータ | 説明 |
|---|---|
| GOGC=100 | ヒープが100%成長したらGC開始 |
| GOGC=50 | 頻繁にGC→メモリ節約・CPU増加 |
| GOGC=200 | GC頻度減→スループット向上 |
| GOGC=off | GCを無効化 |
| GOMEMLIMIT=512MiB | ソフトメモリ上限 |
| GODEBUG=gctrace=1 | GCトレースの有効化 |
8.5 Python の複合型 GC
Python(CPython)の GC アーキテクチャ:| 層1: 参照カウント(メイン機構) |
|---|
| - 全オブジェクトに参照カウント(ob_refcnt) |
| - 参照の追加/削除で即座に増減 |
| - rc=0 で即座に解放 |
| - GIL により原子操作が不要(シングルスレッド保証) |
| 問題: 循環参照を回収できない |
| 層2: 世代別サイクルコレクタ(補助機構) |
| 世代0: 新しいオブジェクト(閾値: 700) |
| 世代1: 世代0を1回生き延びたオブジェクト(閾値: 10) |
| 世代2: 世代1を1回生き延びたオブジェクト(閾値: 10) |
| 動作: |
| 1. 世代0 のオブジェクト数が閾値を超えたら GC 実行 |
| 2. 循環参照を検出して回収 |
| 3. コンテナオブジェクトのみが対象 |
| (int, str 等は循環参照を形成しないため対象外) |
# Python: GC の詳細な制御
import gc
import sys
# === GC の状態確認 ===
print("GC 有効:", gc.isenabled())
print("世代別閾値:", gc.get_threshold())
# 出力: (700, 10, 10)
# 世代0: 700回のalloc-dealloc差で発動
# 世代1: 世代0が10回実行されたら発動
# 世代2: 世代1が10回実行されたら発動
# === GC 統計 ===
stats = gc.get_stats()
for i, gen in enumerate(stats):
print(f"世代{i}: collections={gen['collections']}, "
f"collected={gen['collected']}, "
f"uncollectable={gen['uncollectable']}")
# === GC チューニング ===
# 閾値の調整(パフォーマンス重視)
gc.set_threshold(1000, 15, 15) # GC 頻度を下げる
# GC の一時無効化(ベンチマーク等)
gc.disable()
# ... 計測コード ...
gc.enable()
# === 回収不能オブジェクトの検出 ===
# __del__ を持つ循環参照は回収できない場合がある
gc.set_debug(gc.DEBUG_SAVEALL)
gc.collect()
print("回収不能:", gc.garbage)8.6 主要言語 GC 比較表
| 言語 | GC 方式 | STW 時間 | 世代別 | 特記事項 |
|---|---|---|---|---|
| Java | G1/ZGC | <1ms(ZGC) | あり | 複数コレクタ選択可 |
| (HotSpot) | <200ms(G1) | JFR で詳細分析 | ||
| Go | 並行M&S | <500μs | なし | GOGC/GOMEMLIMIT |
| シンプルさ重視 | ||||
| Python | 参照カウント | 数ms〜数十ms | あり | GIL との相互作用 |
| (CPython) | +サイクルGC | (3世代) | 参照カウントがメイン | |
| JS (V8) | Orinoco | <2ms(Young) | あり | Idle-Time GC |
| M&S+Copy | <10ms(Old) | (2世代) | インクリメンタル | |
| C# | 世代別 | <10ms | あり | LOH, POH(NET5+) |
| (.NET) | M&S+Compact | (3世代) | Server/Workstation | |
| Ruby | 世代別 | <10ms | あり | Incremental M&S |
| (CRuby) | M&S | (2世代) | RUBY_GC_*環境変数 | |
| Swift | ARC | なし | なし | weak/unowned 必要 |
| (参照カウント) | コンパイル時管理 | |||
| Rust | なし | なし | なし | 所有権+借用で管理 |
| (所有権) | GC オーバーヘッドゼロ |
9. GC チューニング戦略
9.1 チューニングの基本原則
GC チューニングは「万能の設定」が存在しない領域である。アプリケーションの特性 (レイテンシ重視 vs スループット重視 vs メモリ効率重視)に応じて戦略が変わる。
GC チューニングの三角形(トレードオフ):
レイテンシ
(低停止時間)
╱╲
╱ ╲
╱ ╲
╱ GC ╲
╱ チューニング╲
╱ の三角形 ╲
╱________________╲
スループット メモリ効率
(高処理能力) (低メモリ使用量)
全てを同時に最適化することは不可能:
- レイテンシ優先 → GC 頻度増 → スループット低下
- スループット優先 → GC 頻度減 → メモリ使用量増加
- メモリ効率優先 → ヒープを小さく → GC 頻度増 → レイテンシ悪化
9.2 Java GC チューニング実践
// Java: GC チューニングの段階的アプローチ
// === Step 1: GC ログの有効化 ===
// JDK 9+ 統一ログ:
// java -Xlog:gc*:file=gc.log:time,uptime,level,tags -jar app.jar
// === Step 2: GC ログの分析 ===
// GC ログの読み方:
// [0.234s][info][gc] GC(0) Pause Young (Normal) (G1 Evacuation Pause)
// 12M->8M(256M) 3.456ms
// ↑ ↑ ↑ ↑
// GC前 GC後 ヒープ 停止時間
// === Step 3: ヒープサイズの設定 ===
// -Xms と -Xmx を同じ値にする(ヒープのリサイズを回避)
// java -Xms4g -Xmx4g -jar app.jar
// === Step 4: GC コレクタの選択 ===
// レイテンシ重視:
// java -XX:+UseZGC -XX:+ZGenerational -jar app.jar
// バランス型:
// java -XX:+UseG1GC -XX:MaxGCPauseMillis=100 -jar app.jar
// スループット重視:
// java -XX:+UseParallelGC -jar app.jar
// === Java Flight Recorder(JFR)による分析 ===
// java -XX:StartFlightRecording=filename=recording.jfr,
// duration=60s,settings=profile -jar app.jar
// === プログラムからの GC 情報取得 ===
import java.lang.management.GarbageCollectorMXBean;
import java.lang.management.ManagementFactory;
import java.util.List;
public class GCMonitor {
public static void printGCInfo() {
List<GarbageCollectorMXBean> gcBeans =
ManagementFactory.getGarbageCollectorMXBeans();
for (GarbageCollectorMXBean gcBean : gcBeans) {
System.out.printf("GC名: %s%n", gcBean.getName());
System.out.printf(" 回数: %d%n", gcBean.getCollectionCount());
System.out.printf(" 累積時間: %d ms%n", gcBean.getCollectionTime());
}
}
// Weak Reference を活用したキャッシュ
// GC がメモリ不足時に自動的に回収してくれる
private static final java.util.Map<String, java.lang.ref.WeakReference<byte[]>> cache
= new java.util.concurrent.ConcurrentHashMap<>();
public static void cacheData(String key, byte[] data) {
cache.put(key, new java.lang.ref.WeakReference<>(data));
}
public static byte[] getCachedData(String key) {
java.lang.ref.WeakReference<byte[]> ref = cache.get(key);
if (ref != null) {
byte[] data = ref.get();
if (data != null) return data;
cache.remove(key); // GC に回収された
}
return null;
}
}9.3 GC チューニングのチェックリスト
GC チューニング・チェックリスト:
□ 1. 目標の明確化
├── 最大停止時間の目標は? (例: 100ms 以下)
├── スループット目標は? (例: GC に費やす時間 < 5%)
└── メモリ制約は? (例: 最大 4GB)
□ 2. 現状の把握
├── GC ログを有効化して分析
├── GC 頻度、停止時間、ヒープ使用量の傾向を把握
└── メモリリークの有無を確認
□ 3. ヒープサイズの最適化
├── -Xms = -Xmx(リサイズ回避)
├── ヒープが小さすぎ → GC 頻発
└── ヒープが大きすぎ → GC 停止時間増加
□ 4. GC コレクタの選択
├── G1: ほとんどのケースで適切(Java 9+ デフォルト)
├── ZGC: 超低レイテンシが必要な場合
├── Parallel: バッチ処理等スループット重視
└── Serial: 小規模アプリ / コンテナ
□ 5. 世代別サイズの調整
├── Young 世代が小さい → Minor GC 頻発
├── Young 世代が大きい → Minor GC の停止時間増
└── -XX:NewRatio, -XX:SurvivorRatio で調整
□ 6. 継続的モニタリング
├── JFR / JMX でリアルタイム監視
├── ダッシュボード(Grafana 等)でトレンド分析
└── アラート設定(GC 停止時間 > 閾値)
9.4 メモリ管理パターン: GC vs 手動管理 vs 所有権
3つのメモリ管理パラダイムの総合比較:| 観点 | GC | 手動管理 | 所有権(Rust) |
|---|---|---|---|
| 安全性 | 高い | 低い | 最も高い |
| メモリリーク | 論理的リーク可 | 頻発 | 構造的に防止 |
| ダングリング | 発生しない | 頻発 | コンパイルエラー |
| 二重解放 | 発生しない | 頻発 | コンパイルエラー |
| 性能予測性 | 低い(STW) | 高い | 高い |
| 開発効率 | 高い | 低い | 中〜高 |
| 実行時コスト | 5-20% | 0% | 0-3% |
| コンパイル時 | なし | なし | 借用チェック |
| 学習曲線 | 低い | 中程度 | 急峻 |
| リアルタイム | 困難 | 可能 | 可能 |
| 大規模開発 | 適している | 困難 | 適している |
| 代表言語 | Java,Go, | C,C++ | Rust |
| Python,JS,C# |
選定ガイドライン:
- Web アプリ、ビジネスロジック → GC(Java, Go, C#)
- OS、組込み、ゲームエンジン → 手動管理(C, C++)
- システムプログラミング → 所有権(Rust)
- スクリプティング、データ分析 → GC(Python, JS)
10. アンチパターンと回避策
10.1 アンチパターン1: 隠れたメモリリーク(論理的リーク)
GC 言語でも「論理的メモリリーク」は発生する。GC はルートから到達可能なオブジェクトを 回収しないため、不要な参照を保持し続けるとメモリが際限なく増加する。
// Java: 論理的メモリリークの典型例
// === アンチパターン: 無制限のキャッシュ ===
public class LeakyCache {
// 問題: 追加されたエントリは永遠に GC されない
private static final Map<String, byte[]> cache = new HashMap<>();
public static void addToCache(String key, byte[] data) {
cache.put(key, data); // 無制限に蓄積 → メモリリーク
}
// 呼び出し側:
// for (Request req : requests) {
// addToCache(req.getId(), req.getPayload());
// // cache は永遠に成長し続ける → OOM
// }
}
// === 修正1: サイズ制限付きキャッシュ(LRU) ===
public class BoundedCache {
private static final int MAX_SIZE = 1000;
private static final Map<String, byte[]> cache =
new LinkedHashMap<String, byte[]>(MAX_SIZE, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry<String, byte[]> eldest) {
return size() > MAX_SIZE; // 最大サイズを超えたら最古を削除
}
};
public static void addToCache(String key, byte[] data) {
cache.put(key, data);
}
}
// === 修正2: WeakHashMap を使用 ===
public class WeakCache {
// キーが他から参照されなくなったら自動的にエントリが削除される
private static final Map<Object, byte[]> cache = new WeakHashMap<>();
public static void addToCache(Object key, byte[] data) {
cache.put(key, data);
}
}
// === 修正3: Caffeine ライブラリ(推奨) ===
// Caffeine: 高性能な Java キャッシュライブラリ
//
// Cache<String, byte[]> cache = Caffeine.newBuilder()
// .maximumSize(10_000)
// .expireAfterWrite(Duration.ofMinutes(5))
// .build();10.2 アンチパターン2: ファイナライザの乱用
// Java: ファイナライザのアンチパターンと正しい代替手段
// === アンチパターン: finalize() によるリソース解放 ===
public class BadResourceHandler {
private java.io.InputStream stream;
public BadResourceHandler(String path) throws Exception {
this.stream = new java.io.FileInputStream(path);
}
// 問題点:
// 1. finalize() の実行タイミングは不定
// 2. finalize() が実行される保証がない
// 3. finalize() の実行により GC が遅延する
// 4. finalize() 内の例外は無視される
// 5. Java 9+ で非推奨、Java 18+ で削除対象
@Override
protected void finalize() throws Throwable {
try {
if (stream != null) stream.close();
} finally {
super.finalize();
}
}
}
// === 修正: try-with-resources + AutoCloseable ===
public class GoodResourceHandler implements AutoCloseable {
private final java.io.InputStream stream;
public GoodResourceHandler(String path) throws Exception {
this.stream = new java.io.FileInputStream(path);
}
@Override
public void close() throws Exception {
if (stream != null) stream.close();
}
// 使用例:
// try (GoodResourceHandler handler = new GoodResourceHandler("data.txt")) {
// // handler を使用
// } ← ブロック終了時に自動的に close() が呼ばれる
}
// === Java 9+: Cleaner API(finalize の代替) ===
import java.lang.ref.Cleaner;
public class ModernResourceHandler implements AutoCloseable {
private static final Cleaner cleaner = Cleaner.create();
private final Cleaner.Cleanable cleanable;
private final ResourceState state;
// 内部状態クラス(Runnable 実装)
// 重要: 外部クラスへの参照を持たないこと
private static class ResourceState implements Runnable {
private java.io.InputStream stream;
ResourceState(java.io.InputStream stream) {
this.stream = stream;
}
@Override
public void run() {
// GC 時のフォールバック清掃
try {
if (stream != null) stream.close();
} catch (Exception e) {
// ログ記録
}
}
}
public ModernResourceHandler(String path) throws Exception {
this.state = new ResourceState(new java.io.FileInputStream(path));
this.cleanable = cleaner.register(this, state);
}
@Override
public void close() {
cleanable.clean(); // 明示的に清掃
}
}10.3 アンチパターン3: 大量の短命オブジェクトの生成
大量の一時オブジェクト生成の問題:
問題のあるパターン:| for (int i = 0; i < 1_000_000; i++) { |
|---|
| String result = "prefix_" + i + "_suffix"; // ★ |
| // 各イテレーションで複数の String オブジェクトが生成 |
| // "prefix_" + i → 新 String |
| // 新 String + "_suffix" → 別の新 String |
| process(result); |
| } |
| → 200万個以上の一時 String が生成 → GC 圧力が急増 |
改善:| StringBuilder sb = new StringBuilder(64); |
|---|
| for (int i = 0; i < 1_000_000; i++) { |
| sb.setLength(0); // バッファをクリア(再利用) |
| sb.append("prefix_").append(i).append("_suffix"); |
| process(sb.toString()); |
| } |
| → StringBuilder を再利用 → 一時オブジェクト大幅削減 |
10.4 アンチパターン4: System.gc() の呼び出し
// アンチパターン: System.gc() を明示的に呼ぶ
// 問題のあるコード:
public void processLargeData(byte[] data) {
// ... データ処理 ...
data = null;
System.gc(); // ★ GC を強制実行しようとしている
}
// なぜダメなのか:
// 1. System.gc() は「ヒント」に過ぎず、GC の実行は保証されない
// 2. Full GC が発生すると長い STW を引き起こす可能性がある
// 3. GC のタイミング最適化(ペーシング)を妨害する
// 4. 本番環境では -XX:+DisableExplicitGC で無効化されることが多い
// 正しいアプローチ:
// - JVM の GC に任せる
// - 必要ならヒープサイズや GC パラメータを調整する
// - 不要な参照を null にする(大きなオブジェクトの場合のみ有効)11. 演習問題(3段階)
Level 1: 基礎(GC アルゴリズムの理解)
問題 1-1: マーク&スイープの手動トレース
以下のオブジェクトグラフに対してマーク&スイープを実行し、 回収されるオブジェクトを全て列挙せよ。
ルート: R1 → A, R2 → B
A → C, A → D
B → D
C → E
D → (なし)
E → (なし)
F → G
G → F(循環参照)
H → (なし)
解答(クリックで展開)
マーキングフェーズ:
R1 → A(mark) → C(mark) → E(mark)
→ D(mark)
R2 → B(mark) → D(既にmark)
マークされたオブジェクト: A, B, C, D, E
マークされていないオブジェクト: F, G, H
回収対象: F, G, H
注: F と G は循環参照しているが、ルートから到達不能なので回収される。
これが参照カウント方式との大きな違い。参照カウントでは F, G は
rc=1 のままで回収できない。
問題 1-2: 参照カウントの手動トレース
以下の Python コードの各行実行後の参照カウントを追跡せよ。
a = [1, 2, 3] # (1) a の参照カウントは?
b = a # (2) a の参照カウントは?
c = [a, b] # (3) a の参照カウントは?
del b # (4) a の参照カウントは?
c.pop() # (5) a の参照カウントは?
c.pop() # (6) a の参照カウントは?
del a # (7) リストオブジェクトはどうなる?解答(クリックで展開)
(1) a=[1,2,3] → リストの rc=1(a からの参照)
(2) b=a → リストの rc=2(a, b からの参照)
(3) c=[a,b] → リストの rc=4(a, b, c[0], c[1] からの参照)
(4) del b → リストの rc=3(a, c[0], c[1] からの参照)
(5) c.pop() → リストの rc=2(a, c[0] からの参照)
※ c[1] が削除された
(6) c.pop() → リストの rc=1(a からの参照)
※ c[0] が削除された
(7) del a → リストの rc=0 → 即座に解放!
補足: sys.getrefcount(a) で確認すると +1 されるのは
getrefcount の引数として一時的に参照が増えるため。
Level 2: 応用(GC チューニングと診断)
問題 2-1: GC ログの解析
以下の Java GC ログから問題を診断し、改善策を提案せよ。
[2.001s][info][gc] GC(0) Pause Young 128M->64M(512M) 5.2ms
[2.503s][info][gc] GC(1) Pause Young 192M->72M(512M) 6.1ms
[3.012s][info][gc] GC(2) Pause Young 200M->80M(512M) 7.3ms
[3.498s][info][gc] GC(3) Pause Young 208M->96M(512M) 8.5ms
[4.002s][info][gc] GC(4) Pause Young 224M->112M(512M) 10.2ms
[4.503s][info][gc] GC(5) Pause Full 240M->48M(512M) 350.1ms ★
[5.001s][info][gc] GC(6) Pause Young 176M->64M(512M) 5.0ms
...(以降繰り返し)
解答(クリックで展開)
診断:
1. Minor GC 後の生存量が単調増加: 64→72→80→96→112MB
→ Old 世代への昇格が急速に増加している
2. GC(5) で Full GC が発生: 350.1ms の STW
→ Old 世代が満杯になった
3. Full GC 後に 48MB に戻る
→ 長期生存オブジェクトは少ない(一時的な昇格が多い)
推定原因:
- 中程度の寿命のオブジェクトが多い(Young で死なず Old に昇格するが
Old でもすぐ不要になる)
- Young 世代のサイズが小さすぎる可能性
改善策:
1. Young 世代を拡大: -XX:NewRatio=1(Young:Old=1:1)
→ オブジェクトが Young 内で死ぬ確率を上げる
2. Survivor の調整: -XX:MaxTenuringThreshold=15
→ 昇格までの閾値を上げる
3. G1 GC の場合: -XX:MaxGCPauseMillis=100
→ 目標停止時間を設定してFull GCを回避
4. ZGC への切替を検討: -XX:+UseZGC
→ Full GC による長い STW を根本的に解消
問題 2-2: メモリリークの特定
以下の Node.js コードにはメモリリークがある。原因を特定し修正せよ。
const EventEmitter = require('events');
class DataProcessor extends EventEmitter {
constructor() {
super();
this.cache = new Map();
}
process(data) {
const id = data.id;
const result = this.transform(data);
this.cache.set(id, result);
const handler = (event) => {
console.log(`Event for ${id}: ${event}`);
console.log(`Cache size: ${this.cache.size}`);
};
this.on('update', handler);
return result;
}
transform(data) {
return { ...data, processed: true, timestamp: Date.now() };
}
}
const processor = new DataProcessor();
// 毎秒新しいデータを処理
setInterval(() => {
const data = { id: Math.random().toString(36), payload: 'x'.repeat(1024) };
processor.process(data);
}, 1000);解答(クリックで展開)
メモリリークの原因は2つ:
1. cache が無制限に成長する
- process() で cache.set() しているが、削除する処理がない
- → Map のエントリが際限なく増加
2. イベントリスナーが無制限に追加される
- process() の呼び出しごとに this.on('update', handler) で
新しいリスナーが追加される
- リスナーはクロージャで id, this.cache を参照し続ける
- → リスナー数が際限なく増加
修正版:
class DataProcessor extends EventEmitter {
constructor(maxCacheSize = 1000) {
super();
this.cache = new Map();
this.maxCacheSize = maxCacheSize;
}
process(data) {
const id = data.id;
const result = this.transform(data);
// 修正1: キャッシュサイズを制限
if (this.cache.size >= this.maxCacheSize) {
const oldestKey = this.cache.keys().next().value;
this.cache.delete(oldestKey);
}
this.cache.set(id, result);
// 修正2: リスナーは追加しない(別途管理する)
// または once() を使い1回だけ実行する
return result;
}
}
Level 3: 発展(GC アルゴリズムの実装と分析)
問題 3-1: 世代別 GC シミュレータの設計
以下の仕様を満たす世代別 GC シミュレータを Python で設計せよ。 (完全な実装は不要。クラス構造とメソッドのシグネチャ、主要な擬似コードを示すこと)
要件:
- Young 世代(Eden + Survivor x 2)と Old 世代を持つ
- Minor GC: Eden の生存オブジェクトを Survivor にコピー
- 昇格: 閾値回数 Minor GC を生き延びたオブジェクトを Old に移動
- Major GC: Old 世代のマーク&スイープ
- 書き込みバリア: Old → Young の参照を Remembered Set に記録
解答(クリックで展開)
class GenerationalGC:
def __init__(self, eden_size, survivor_size, old_size,
tenuring_threshold=15):
self.eden = Space("Eden", eden_size)
self.survivor_from = Space("S0", survivor_size)
self.survivor_to = Space("S1", survivor_size)
self.old = Space("Old", old_size)
self.tenuring_threshold = tenuring_threshold
self.remembered_set = set() # Old → Young の参照を記録
self.roots = []
def allocate(self, size):
"""Eden にオブジェクトを確保。満杯なら Minor GC"""
if not self.eden.can_allocate(size):
self.minor_gc()
if not self.eden.can_allocate(size):
self.major_gc() # Old も満杯なら Major GC
obj = self.eden.allocate(size)
obj.age = 0
return obj
def minor_gc(self):
"""Young 世代の GC(コピー GC)"""
# ルートの走査
# 1. スタック/グローバルルートから到達可能な Young オブジェクトをコピー
# 2. Remembered Set のエントリから到達可能な Young オブジェクトもコピー
# 3. age >= threshold なら Old に昇格
# 4. それ以外は survivor_to にコピー(age+1)
# 5. Eden と survivor_from をクリア
# 6. survivor_from と survivor_to を交換
for root in self.roots + list(self.remembered_set):
self._copy_reachable(root)
self.eden.clear()
self.survivor_from.clear()
self.survivor_from, self.survivor_to = (
self.survivor_to, self.survivor_from
)
def major_gc(self):
"""Old 世代の GC(マーク&スイープ)"""
# 1. 全オブジェクトの mark ビットをリセット
# 2. ルートから到達可能なオブジェクトをマーク
# 3. マークされていない Old オブジェクトを解放
self._mark_all()
self._sweep_old()
def write_barrier(self, src, dst):
"""Old → Young への参照を検出して記録"""
if self.old.contains(src) and not self.old.contains(dst):
self.remembered_set.add(src)12. FAQ(よくある質問)
Q1: GC があればメモリリークは発生しないのか?
A: いいえ。GC は「到達不能なオブジェクト」のみを回収する。到達可能だが不要なオブジェクト (論理的リーク)は回収されない。典型的な例として以下がある:
- 無制限に成長するキャッシュ(HashMap 等)
- 登録したまま解除しないイベントリスナー
- スレッドローカル変数のクリア忘れ(スレッドプールでの使用時)
- static フィールドに保持し続けるコレクション
- クロージャが不要な変数をキャプチャし続けるケース
対策としては、キャッシュのサイズ制限(LRU, TTL)、WeakReference の活用、 メモリプロファイラ(VisualVM, Chrome DevTools, pprof 等)による定期的な検査が有効。
Q2: GC の停止時間をゼロにできるか?
A: 完全にゼロにすることは理論的に困難だが、極めて短くすることは可能。
- Java ZGC: STW < 1ms(ヒープサイズに依存しない)。カラーポインタと ロードバリアにより、ほぼ全ての作業を並行で実行する。
- Go: STW < 500μs を目標とし、実際には数十μs で完了することが多い。
- Azul C4 GC(商用): 「Pauseless GC」を謳い、STW なしで動作する。
注意点として、「STW がない」と「レイテンシへの影響がない」は異なる。 並行 GC はバックグラウンドで CPU を消費するため、アプリケーションの スループットやレイテンシに間接的な影響を及ぼす場合がある。
Q3: Rust に GC がないのに安全なのはなぜか?
A: Rust は GC の代わりに「所有権システム」と「借用チェッカー」を用いて コンパイル時にメモリ安全性を保証する。
- 所有権: 各値には唯一のオーナーが存在し、オーナーがスコープを抜けると自動的に解放される。
- 借用: 値への参照(借用)は不変参照(複数可)か可変参照(1つのみ)のどちらかで、 コンパイラが静的に検証する。
- ライフタイム: 参照の有効期間をコンパイラが追跡し、ダングリングポインタを構造的に防止する。
GC と比較した場合のトレードオフ:
- 利点: 実行時オーバーヘッドゼロ、STW なし、決定的なリソース解放
- 欠点: 学習曲線が急峻、循環的なデータ構造の表現がやや煩雑(Rc/Arc + RefCell が必要)
Q4: どの言語を選ぶべきか(メモリ管理の観点から)?
A: プロジェクトの要件に依存する。
| 要件 | 推奨言語 | 理由 |
|---|---|---|
| Web バックエンド | Java, Go, C# | GC が成熟、エコシステムが豊富 |
| 低レイテンシ | Go, Java(ZGC), Rust | 短い STW、または STW なし |
| 組込みシステム | C, Rust | GC オーバーヘッドが許容されない |
| データサイエンス | Python | GC を意識せず開発可能 |
| フロントエンド | JavaScript/TypeScript | V8 の GC が自動最適化 |
| ゲームエンジン | C++, Rust | リアルタイム性が必須 |
| モバイルアプリ | Swift(iOS), Kotlin(Android) | ARC/GC がプラットフォーム標準 |
Q5: GC のオーバーヘッドはどの程度か?
A: ワークロードによって大きく異なるが、一般的な目安は以下の通り。
- CPU オーバーヘッド: 全体の 2-10%(GC スレッドの実行コスト)
- メモリオーバーヘッド: 20-50%(GC メタデータ + 遅延回収による余剰使用量)
- レイテンシ影響: GC コレクタによる。ZGC なら < 1ms、G1 なら数百 ms の場合あり
GC のオーバーヘッドを最小化するために:
- オブジェクトの確保率を下げる(オブジェクトプーリング、バッファの再利用)
- 長寿命オブジェクトの不要な参照を断つ
- 適切な GC コレクタとパラメータを選択する
- ヒープサイズを適切に設定する
FAQ
Q1: このトピックを学ぶ上で最も重要なポイントは何ですか?
実践的な経験を積むことが最も重要です。理論だけでなく、実際にコードを書いて動作を確認することで理解が深まります。
Q2: 初心者がよく陥る間違いは何ですか?
基礎を飛ばして応用に進むことです。このガイドで説明している基本概念をしっかり理解してから、次のステップに進むことをお勧めします。
Q3: 実務ではどのように活用されていますか?
このトピックの知識は、日常的な開発業務で頻繁に活用されます。特にコードレビューやアーキテクチャ設計の際に重要になります。
13. まとめと次のステップ
GC アルゴリズムの全体マップ
GC アルゴリズムの分類体系:
ガベージコレクション
│
├── トレーシング GC(到達可能性ベース)
│ ├── マーク&スイープ ─── 基本形。循環参照 OK、断片化問題
│ ├── マーク&コンパクト ── 断片化解消、コスト高
│ ├── コピー GC ────── コンパクション自動、メモリ 50% 消費
│ ├── 世代別 GC ────── 若いオブジェクトの高速回収
│ ├── インクリメンタル GC ─ STW を小分け
│ └── 並行 GC ─────── バックグラウンドで GC 実行
│
└── 参照カウント(カウントベース)
├── 単純参照カウント ── 即時回収、循環参照不可
├── 遅延参照カウント ── ルートの更新を遅延
└── ARC ───────── コンパイラが自動挿入(Swift)
要点のまとめ
| アルゴリズム | 中核アイデア | 主な利点 | 主な欠点 | 使用例 |
|---|---|---|---|---|
| マーク&スイープ | 到達可能性で判定 | 循環参照OK | STW、断片化 | 多くの言語の基盤 |
| 参照カウント | 参照数で判定 | 即時回収 | 循環参照不可 | Python, Swift |
| コピー GC | 生存オブジェクトをコピー | 断片化なし | メモリ2倍 | Young 世代 |
| 世代別 GC | 世代別仮説で最適化 | 高速な Minor GC | 実装の複雑さ | Java, JS, C# |
| 並行 GC | バックグラウンド実行 | STW 最小化 | 書き込みバリア必要 | Go, Java(ZGC) |
次に読むべきガイド
14. 参考文献
-
Jones, R., Hosking, A. & Moss, E. The Garbage Collection Handbook: The Art of Automatic Memory Management. 2nd Edition, CRC Press, 2023. -- GC の理論と実装を包括的にカバーする決定版テキスト。マーク&スイープ、コピー GC、世代別 GC、並行 GC の全てを詳細に解説している。
-
McCarthy, J. "Recursive Functions of Symbolic Expressions and Their Computation by Machine, Part I." Communications of the ACM, Vol.3, No.4, pp.184-195, 1960. -- Lisp と GC の原論文。マーク&スイープ方式の最初の提案を含む、プログラミング言語史上最も重要な論文の一つ。
-
Dijkstra, E.W., Lamport, L., Martin, A.J., Scholten, C.S. & Steffens, E.F.M. "On-the-Fly Garbage Collection: An Exercise in Cooperation." Communications of the ACM, Vol.21, No.11, pp.966-975, 1978. -- 三色マーキングと並行 GC の理論的基盤を確立した古典的論文。
-
Bacon, D.F., Cheng, P. & Rajan, V.T. "A Unified Theory of Garbage Collection." Proceedings of the 19th ACM SIGPLAN Conference on Object-Oriented Programming, Systems, Languages, and Applications (OOPSLA), pp.50-68, 2004. -- トレーシング GC と参照カウントが数学的に双対であることを示した画期的論文。
-
Oracle. Java Platform, Standard Edition HotSpot Virtual Machine Garbage Collection Tuning Guide. https://docs.oracle.com/en/java/javase/21/gctuning/ -- Java 21 LTS の公式 GC チューニングガイド。G1 GC、ZGC、Serial GC、Parallel GC の設定と最適化手法を解説。
-
Go Team. A Guide to the Go Garbage Collector. https://tip.golang.org/doc/gc-guide -- Go 公式の GC ガイド。GOGC と GOMEMLIMIT の使い方、GC のペーシングアルゴリズムを解説。
-
V8 Team. Trash Talk: The Orinoco Garbage Collector. https://v8.dev/blog/trash-talk -- V8 エンジンの GC(Orinoco)の設計と実装を解説する公式ブログ記事。Scavenger、並行マーキング、インクリメンタルマーキングの詳細を含む。
-
Tene, G., Iyengar, B. & Wolf, M. "C4: The Continuously Concurrent Compacting Collector." Proceedings of the International Symposium on Memory Management (ISMM), ACM, 2011. -- Azul Systems の Pauseless GC(C4)の設計を解説した論文。商用 GC の最前線。
参考文献
- MDN Web Docs - Web技術のリファレンス
- Wikipedia - 技術概念の概要