「駑馬十駕」を信念に IT系情報を中心に調べた事をコツコツ綴っています。

GCとは何か

Java で開発をしていると、よく耳にする「GC(Garbage Collection)」。
これは 不要になったオブジェクトを自動で回収してメモリを解放する仕組み のことです。C言語のように手動で free() を呼ぶ必要はなく、Java VM が裏側でメモリ管理を行います。

 

ざっくり構造・最近のGC

  • 世代別回収:Eden/Survivor(若世代)→ Old(老世代)

  • Minor GC:Edenが埋まったら短命オブジェクト中心に回収

  • Major/Full GC:Oldが逼迫、断片化、クラス/メタ領域逼迫などで広域回収

  • 既定GC:G1GC(Java 9+)。低停止要求は ZGC / Shenandoah も選択肢

主なトリガ

  • Eden満杯(Minor) / Old高水準(Major)

  • 巨大配列(Humongous)割当て(G1)

  • System.gc() 明示呼び出し

  • メタスペース/オフヒープ圧迫(DirectByteBuffer/JNI など)


“悪い例 → 良い例”で学ぶメモリ/GC対策

1) 無制限キャッシュ(静的Map地獄)

悪い例

// 無制限に溜まる
private static final Map<String, Object> CACHE = new HashMap<>();

public Object get(String key) {
  return CACHE.computeIfAbsent(key, this::loadSlow);
}

良い例(上限+期限+統計)

// Caffeine を例に(依存追加で利用可)
Cache<String, Object> cache = Caffeine.newBuilder()
    .maximumSize(10_000)         // 上限
    .expireAfterAccess(Duration.ofMinutes(30)) // 期限
    .recordStats()
    .build();

public Object get(String key) {
  return cache.get(key, this::loadSlow);
}

ポイント:上限なしは必ずOldを膨らませる。キャッシュは 容量・期限・エビクションを設計。


2) リスナ/コールバック未解除

悪い例

class View {
  void addListener(Listener l) { /*...*/ }
}
View v = ...;
v.addListener(this); // 解除しない → 長寿命が短命を掴んでリーク

良い例(ライフサイクルで必ず解除 / AutoCloseable化)

class View {
  Closeable addListener(Listener l) {
    // 登録...
    return () -> removeListener(l); // try-with-resources で自動解除
  }
}

try (var h = view.addListener(event -> {/*...*/})) {
  // 利用中
} // スコープを出ると自動で removeListener

補足WeakReference リスナはイベント強度低下意図せぬ解放のリスク。基本は明示解除


3) ThreadLocal の放置(プールスレッドに張り付く)

悪い例

private static final ThreadLocal<byte[]> BUF =
    ThreadLocal.withInitial(() -> new byte[1<<20]);

// 使い終わっても remove しない

良い例(finallyで確実に除去)

private static final ThreadLocal<byte[]> BUF =
    ThreadLocal.withInitial(() -> new byte[1<<20]);

void work() {
  try {
    var buf = BUF.get();
    // 処理...
  } finally {
    BUF.remove(); // プールスレッドに残さない
  }
}

ポイントスレッドプール=長寿命remove() を忘れると実質グローバル保持


4) System.gc() 乱用

悪い例

// タスク終わりに毎回呼ぶなど
System.gc(); // 不要なFull GCで停止時間が跳ね上がる

良い例

- 基本呼ばない
- どうしても必要なら -XX:+DisableExplicitGC-XX:+ExplicitGCInvokesConcurrent で悪影響を抑制

5) ラムダ/内部クラスが外側(巨大オブジェクト)をキャプチャ

悪い例

class Holder {
  byte[] big = new byte[10_000_000];
  Runnable r = () -> System.out.println(big.length); // this を捕捉
}

良い例(必要最小限のデータだけ渡す・static化)

class Holder {
  byte[] big = new byte[10_000_000];

  Runnable r() {
    int len = big.length;        // プリミティブだけ抜き出す
    return () -> System.out.println(len);
  }
}
// あるいは static ネストクラスで暗黙の外部参照を避ける

ポイントキャプチャ=保持。意図せず大物を延命していないか疑う。


6) ループ内の大量一時オブジェクト

悪い例

for (int i = 0; i < n; i++) {
  log(i + ":" + list.get(i)); // 毎回 StringBuilder 暗黙生成+ボクシング
}

良い例(StringBuilder再利用・ボクシング回避)

StringBuilder sb = new StringBuilder(256);
for (int i = 0; i < n; i++) {
  sb.setLength(0);
  sb.append(i).append(':').append(list.get(i));
  log(sb.toString());
}

7) finalize/Cleaner頼み(遅延・不確実)

悪い例

class NativeHolder {
  @Override protected void finalize() { nativeFree(); } // いつ走るかわからない
}

良い例(確実な即時解放) 

class Resource implements AutoCloseable {
  private final FileChannel ch = FileChannel.open(...);
  @Override public void close() throws IOException { ch.close(); }
}

try (Resource r = new Resource()) {
  // 利用
} // ここで必ず解放

8) クラスローダ・アプリ再デプロイ時のリーク

悪い例

// 静的フィールドにアプリクラスを保持
public class Globals {
  public static ClassFromWar APP_CLASS = ...; // アンロード不能に
}

良い例(クラスローダ境界を越える参照を断つ)

- static にアプリクラス/インスタンスを保持しない
- ThreadLocal を必ず remove
- Executor/Timer を停止(shutdown)してからアンデプロイ
- JDBC ドライバ/ログアダプタ等の明示的解除を守る

 

9) 巨大配列・Humongous割当ての長期保持(G1)

悪い例

byte[] huge = new byte[50_000_000]; // region を跨ぐ巨大ブロック

良い例(分割・ストリーミング・寿命短縮)

// 分割保持やチャンク処理でピークメモリを抑え、短命化する
byte[][] chunks = new byte[50][1_000_000]; // 例:サイズ分割

ポイント:巨大ブロックは断片化回収コスト増の温床。


10) 無制限のキュー/バッファ

悪い例

BlockingQueue<Event> q = new LinkedBlockingQueue<>(); // 無制限

良い例(有界+バックプレッシャ)

BlockingQueue<Event> q = new ArrayBlockingQueue<>(10_000);

boolean offered = q.offer(event, 100, TimeUnit.MILLISECONDS);
if (!offered) {
  // 混雑時の方針:間引き/遅延/古いもの破棄など
}

GCログ・計測の始め方(JDK 9+)

# 起動オプション例
-Xms1g -Xmx1g \
-XX:+UseG1GC \
-XX:MaxGCPauseMillis=200 \
-Xlog:gc*,safepoint:file=gc.log:time,uptime,level,tags
  • まずはコードの割当て削減 → その後にヒープ/GC調整

  • 監視:jcmd <pid> GC.heap_info / jstat -gc <pid> 1000

  • ボトルネック特定:JFR(Java Flight Recorder) で割当てホットスポットを把握

  • 必要なら ZGC/Shenandoah も評価(レイテンシ目標に応じて)


実務チェックリスト(配布推奨)

  1. System.gc() を禁止/抑制

  2. キャッシュ・キューは有界+期限

  3. ThreadLocal は finally で remove

  4. リスナ/コールバックは確実に解除(AutoCloseable化が効く)

  5. ループ内の一時オブジェクトを減らす(Builder再利用/ボクシング回避)

  6. 巨大配列は分割・短命化

  7. クラスローダ境界を跨ぐ静的参照禁止、Executor停止・ドライバ解除

  8. try-with-resourcesでオフヒープ即時解放

  9. GCログ/JFRで事実ベースに調整

  10. 目標停止時間(例:MaxGCPauseMillis)を定めて検証


まとめ

GCは“自動”でも“万能”ではありません。
「GCが働きやすいコード」(不要参照を残さない・波及して大物を掴ませない・ピークメモリを避ける)を心がけ、ログ/計測で改善ループを回すのが最短距離です。

0 0
Article Rating
申し込む
注目する
guest
0 コメント一覧
最も古い
最新 高評価
インラインフィードバック
すべてのコメントを見る

Ads by Google

0 0
Article Rating
申し込む
注目する
guest
0 コメント一覧
最も古い
最新 高評価
インラインフィードバック
すべてのコメントを見る
0
あなたの考えが大好きです、コメントしてください。x