🗑️

TypeScriptのGCはどのように動いているのか

に公開

はじめに

こんにちは。株式会社ソニックムーブの中崎です。

最近? JJUG CCC 2025 Springというイベントに参加しました。(もう2ヶ月経ってるやん…)

参加理由は友人がスピーカーをやるということで、特にJavaのことはよく知らずに参加しました。
そこでガベージコレクションについてのセッションが2つもあり、普段使っているTypeScriptのGCはどうなっているのか気になって調べてみることにしました!

GCとは

プログラムが動作する中で生成されたデータ(オブジェクトや変数など)は、使われなくなっても自動では消えません。そこで登場するのが「ガベージコレクション(以下GC)」という仕組みです。

GCは、プログラムから参照されなくなった変数やオブジェクト、関数を自動的に検出し、メモリーから削除してくれる仕組みです。

C言語やC++のようなGCを搭載していない言語ではメモリーをプログラマーが管理する必要があります。
このような言語ではメモリーの管理を怠ると、解放忘れや二重解放といった不具合が発生しやすくなります。

そのため、自動でメモリー管理を行ってくれるGCは、プログラムの信頼性や保守性を高めるうえで重要な役割を果たしています。

ただ、GCも完璧ではなくコードの書き方によってはパフォーマンスが低下する場合もあります。

この記事では、TypeScriptのGCの基本的な仕組みやアルゴリズムについて、図解も交えながら解説していきます。

TypeScriptのGC

TypeScriptのGCでは世代別GCというメモリー管理手法を、メモリー開放ではScavengerとMark-Sweep-Compactの2つのアルゴリズムを採用しています。

世代別GC

世代別GCとは、メモリー空間を2つの世代に分けて管理する手法です。

この手法は「多くのオブジェクトは生成直後に不要になる」という経験則に基づいています。関数内で作られる一時的な変数やオブジェクトは、関数の実行が終わると同時に不要になることがほとんどです。

一方で、グローバル変数やクラスのインスタンスなど、長期間使われ続けるオブジェクトも存在します。

TypeScriptでは、この特性を活かして以下のように世代を分けています。

世代 特徴 GC頻度 アルゴリズム 処理速度
Young Generation 新しいオブジェクト
短命(関数内変数など)
高頻度 Scavenger
高速
Old Generation 長寿命オブジェクト
(グローバル変数など)
低頻度 Mark-Sweep-Compact 低速

この世代別管理により、TypeScriptのGCは効率的にメモリーを管理できます。

また昇格という仕組みが存在します。
Young世代で新しく作られたオブジェクトは、GCが実行されるたびに「まだ使われているか」をチェックされます。2回のGCを生き延びたオブジェクトは「長寿命である」と判断され、Old世代に昇格します。

処理頻度に関してもYoung世代では頻繁にGCを実行して短命オブジェクトを素早く回収を行うことで、メモリーの確保を行い、長期間保持されるOld世代では低頻度で実行し、システム全体への影響を最小限に抑制しています。

この仕組みにより、プログラム全体のパフォーマンスを維持しながら、効率的なメモリー管理を実現しています。

2つのメモリー解放のアルゴリズム

前述の世代別GCでは、それぞれの世代に最適化された異なるアルゴリズムを使用しています。
Young世代では高速性を重視した「Scavenger」、Old世代では徹底性を重視した「Mark-Sweep-Compact」が採用されています。
それぞれの仕組みを詳しく見ていきましょう。

Scavenger

Scavengerは「コピーGC」とも呼ばれ、Young世代専用の高速なアルゴリズムです。

基本的な仕組み

  1. Young世代を「From領域」と「To領域」の2つに分割
  2. 通常はFrom領域にオブジェクトを配置
  3. GC実行時に生存オブジェクトのみをTo領域にコピー
  4. From領域を一括でクリア
  5. From領域とTo領域を入れ替え(スワップ)

特徴

  • 高速: 生存オブジェクトのみを処理するため高速
  • 断片化なし: コピー時に連続配置されるため断片化が発生しない
  • メモリー効率: 常に50%のメモリーしか使用できない制約あり
  • 実行条件: From領域割り当て時に割り当て容量が存在しなかった場合

Mark-Sweep-Compact

Mark-Sweep-Compactは3段階のプロセスでOld世代のメモリーを管理する、より徹底的なアルゴリズムです。

3つのフェーズ

1. Markフェーズ(マーク)

  • ルートオブジェクトから到達可能なすべてのオブジェクトを探索
  • 生存しているオブジェクトにマークを付与

2. Sweepフェーズ(スイープ)

  • メモリー全体をスキャンし、マークされていないオブジェクトを削除
  • 削除されたオブジェクトの領域を空きリストに追加

3. Compactフェーズ(圧縮)

  • 生存オブジェクトを詰めて配置し直し
  • 断片化を解消し、連続した空きスペースを確保

特徴

  • 徹底的: すべてのオブジェクトを検査し、確実に不要なものを回収
  • 断片化解消: Compactにより効率的なメモリー使用を実現
  • 低速: 3段階の処理のため時間がかかる
  • 一時停止: 処理中はプログラムの実行が一時停止する場合がある
  • 実行条件: Old領域に十分な容量が存在しなかった場合

まとめ

この記事では、TypeScriptで使用されるV8エンジンのガベージコレクションについて、その仕組みとアルゴリズムを詳しく解説しました。

TypeScriptのGCの特徴

  • 世代別GC: オブジェクトの生存期間に基づいて効率的に管理
  • 2つのアルゴリズム: Young世代には高速なScavenger、Old世代には徹底的なMark-Sweep-Compact
  • 自動最適化: プログラマーの負担を軽減しながら、メモリ使用効率を向上

世代別GCのメリット
Young世代では頻繁で高速なGCを実行し、Old世代では低頻度で徹底的なGCを実行することで、全体的なパフォーマンスを最適化しています。昇格の仕組みにより、短命なオブジェクトと長寿命なオブジェクトを適切に振り分け、それぞれに最適な処理を適用できます。

アルゴリズムの使い分け

  • Scavenger: コピー方式で断片化を防ぎ、高速処理を実現
  • Mark-Sweep-Compact: 3段階の処理で断片化を解消し、メモリ使用効率を向上

GCは完全に自動化されているため、多くの場合プログラマーが意識する必要はありません。しかし、その仕組みを理解することで、なぜTypeScriptが効率的なメモリ管理を実現できているのかがわかり、より良いコードを書く際の参考になるでしょう。

最後まで読んでいただき、ありがとうございました!

株式会社ソニックムーブ

Discussion