ガベージコレクタ徹底解説:アルゴリズムの特徴・言語別実装・実務チューニングの完全ガイド

ガベージコレクタとは(概説)

ガベージコレクタ(Garbage Collector、GC)は、プログラム実行中に不要になったメモリ領域(ガベージ)を自動的に回収して再利用可能にする仕組みです。手動でメモリ管理を行う(malloc/freeやnew/delete)言語と対照的に、GCを持つ言語や実行環境(Java、C#/.NET、Go、JavaScript、Pythonの一部実装など)は、メモリ解放を自動化することで開発生産性や安全性(メモリリーク・ダングリングポインタの軽減)を高めます。

基本概念と用語

  • ルート(roots):スタックやレジスタ、グローバル変数など、到達可能性判定の起点となる参照集合。
  • 到達可能性(reachability):ルートから参照経路で辿れるオブジェクトは「生きている」と見なされ、辿れないオブジェクトは回収対象。
  • ヒープ:オブジェクトが配置される領域。世代別に分かれることが多い。
  • 停止(stop-the-world):GCの一部でアプリケーションスレッドを停止すること。レイテンシに影響。

主要なアルゴリズム(概要と長所短所)

  • 参照カウント(Reference Counting)
    各オブジェクトが参照されている回数を保持し、0になれば即座に回収する方式。即時解放の利点があるが、循環参照(サイクル)を検出できない点が欠点。Python(CPython)の主要方式は参照カウント+循環検出のハイブリッド。
  • マーキング&スイープ(Mark-and-Sweep)
    ルートから到達可能なオブジェクトをマークし、マークされなかった領域をスイープ(回収)する方式。断片化が起こる可能性があり、スイープ処理のコストや停止時間が課題。
  • マーキング&コンパクト(Mark-Compact)
    マーク後に生きているオブジェクトを詰めて(コンパクト化)断片化を解消する方式。ポインタ更新が必要だが、断片化を防げる。
  • コピー方式(Copying/Semi-space)
    ヒープを2領域に分け、一方から生きているオブジェクトをもう一方へコピーする。断片化が発生せずコピー先は連続する。コピーコストがかかる。若世代を短命オブジェクトで想定する場合に有効。
  • 世代別(Generational GC)
    若いオブジェクトは短命であるという経験則(generational hypothesis)に基づき、若年領域と高年領域に分けて若年領域を頻繁に回収する方式。多くのモダンGCで採用される。
  • 並列/並行(Parallel / Concurrent)
    マルチコアを活用してGC作業を並列化する(並列)/アプリケーションスレッドと並行してGCを行い停止時間を短くする(並行)。実装は複雑で、Write barrierやRead barrierといった補助機構を要することが多い。

内部動作のポイント:トライカラーとバリア

モダンなGCは「トライカラー・マーキング(白・灰・黒)」概念を使って並行マーキングを安全に行います。さらに、並行やインクリメンタルGCでは、ミューテータ(アプリ側)の書き込みを追跡するために書き込みバリア(write barrier)や読み取りバリア(read barrier)が導入されます。これにより、GCとアプリケーションの並行実行中に新たに生成される参照や変更を矛盾なく扱えます。

言語/実行環境ごとの代表的実装

  • Java(HotSpot)
    代表的なGC実装にSerial、Parallel(Throughput GC)、CMS(Concurrent Mark-Sweep、古い)、G1(Garbage-First)、ZGC、Shenandoahなどがあり、用途によって選択可能。G1はリーク管理と一貫した低遅延を目指し、ZGCやShenandoahは大量ヒープでの低停止時間を重視した設計(ほぼ並行・非停止を目指す)。
  • .NET(CLR)
    世代別GCを採用し、ワークステーション・サーバーモード、バックグラウンドGCや並列GCを提供。大規模アプリ向けにサーバーGCでは複数プロセッサを活用して吞吐量を上げる。
  • CPython
    基本は参照カウントで、循環参照を回収するために補助的な世代別サイクル検出GCを持つ。参照カウントの即時解放はデターミニスティック(確定的)だが、拡張モジュールやネイティブ領域は別途管理が必要。
  • Go
    現在のGoランタイムは並行トレース・マーク&スイープを採用し、停止時間を小さく抑える設計。非移動(non-moving)であるため、Cと共有する領域などでポインタ整合を保ちやすい。
  • JavaScript(V8など)
    多くは世代別+並行/インクリメンタルマーキングを併用。ブラウザやNode.jsの応答性に直結するため、低レイテンシ設計が重視される。

性能指標とトレードオフ

GC設計やチューニングは主に以下のトレードオフを調整します。

  • スループット(throughput):プログラム実行に割けるCPU時間の割合。大きなヒープや頻繁な並列GCは高スループットを狙える。
  • レイテンシ/停止時間(latency / pause time):レスポンスタイムに敏感なアプリでは短い停止時間が重要。並行GCや低停止時間GC(ZGC, Shenandoah等)が有利。
  • メモリフットプリント:コピー方式や世代間移動は追加のメモリを必要とする。メモリを節約したい場合はトレードオフを検討。
  • 実装の複雑さとオーバーヘッド:書き込みバリアや並行処理はランタイムオーバーヘッドを伴う。

現場での対策とチューニング実践

  • ヒープサイズ設定(例えばJavaの-Xms/-Xmx)を適切に行い、過度なGC発生やスワップを防ぐ。
  • オブジェクトの生成を減らす:短寿命オブジェクトの大量生成は若世代GCを頻繁化させるため、バッファ再利用やプリミティブ配列化を検討。
  • エスケープ解析とスタック割り当ての活用:JITやコンパイラがスタック割り当てを行うとGC負荷が下がる。
  • 大きなオブジェクトはLOH(Large Object Heap)へ配置されることがあるため、大オブジェクト生成の頻度を下げる。
  • 弱参照(WeakReference)やファイナライザの注意:ファイナライザは予測不能な遅延を招くため、代替(try-with-resourcesや明示的クローズ)を優先。

落とし穴(注意点)

  • ネイティブリソースのリーク:GCは通常ネイティブ領域のリソース(ファイルディスクリプタ、C側メモリ)を自動解放しない。明示的に解放する必要がある。
  • 参照を保持し続けることでの疑似リーク:コレクションされないパターン(コレクションされたくない要素を静的コレクションへ入れっぱなし)に注意。
  • ファイナライザとオブジェクト再生(resurrection):ファイナライザでオブジェクトを再び参照可能にすると回収が遅れる。
  • パフォーマンスの誤診:CPU負荷が高くてもGCが原因とは限らない。GCログやプロファイラで原因切り分けを行う。

デバッグ・プロファイリングのツール

  • Java:GCログ(-Xlog:gc*)、jmap、jstat、VisualVM、Java Flight Recorder。
  • .NET:PerfView、dotnet-trace、dotnet-counters、CLR Profiler。
  • Go:GODEBUG=gctrace=1やpprof。
  • Python:tracemalloc、gcモジュールのデバッグ出力。
  • ブラウザ/Node.js:V8のヒープスナップショットやデベロッパーツール。

まとめ(実務での指針)

ガベージコレクタはメモリ管理を自動化して多くのバグを防ぐ一方で、設計や運用の観点から理解しておくべき複雑性を伴います。選ぶGCアルゴリズムやランタイム、チューニングはアプリケーション特性(スループット重視か低レイテンシ重視か、ヒープサイズ、オブジェクト寿命パターン)に依存します。まずはGCログやプロファイリングで実際の挙動を観察し、オブジェクトの生成パターンの改善や適切なGC設定、ネイティブリソースの明示的管理を行うことが実務上の近道です。

参考文献