JavaScriptにおけるメモリ管理の理解 - V8のガーベージコレクションとOrinocoの深掘り
Takashi Yamamoto
Infrastructure Engineer · Leapcell

はじめに
JavaScript開発の世界では、パフォーマンスとメモリ効率が最優先事項です。JavaScriptは、低レベルのメモリ管理を抽象化する高水準言語として一般的に認識されていますが、その表面下には、JavaScriptエンジンによってオーケストレーションされた、高度に最適化された複雑なシステムが存在します。ChromeやNode.jsで実行されるGoogleのオープンソース高性能JavaScriptおよびWebAssemblyエンジンであるV8は、メモリを自動的に管理するために洗練されたガベージコレクション(GC)メカニズムを使用しています。V8のガベージコレクションを理解することは、単なる学術的な演習ではありません。開発者がより効率的なコードを記述し、メモリリークをデバッグし、パフォーマンスのボトルネックを予測することを可能にします。この記事では、V8のガベージコレクションの複雑さを掘り下げ、世代別アプローチ(ヤングジェネレーションとオールドジェネレーション)と強力なOrinocoパイプラインに焦点を当てます。
V8ガベージコレクションのコアコンセプト
詳細に入る前に、V8のガベージコレクション戦略に不可欠ないくつかの基本概念を確立しましょう。
ヒープ
ヒープは、オブジェクト(文字列、配列、オブジェクトなど)が格納されるメモリ領域です。静的メモリ割り当て(関数呼び出し、ローカル変数など)に使用されるスタックとは異なり、ヒープは動的メモリ割り当てに使用されます。つまり、プログラムが必要とするメモリが実行時に割り当ておよび解除されます。
ガベージコレクション
ガベージコレクションとは、ルート(グローバルオブジェクト、コールスタック)から到達不能または「ライブ」でなくなったオブジェクトによって占有されているメモリを再取得するプロセスです。オブジェクトが到達不能になると、ガベージコレクタはそれを「ゴミ」として識別し、占有しているメモリを解放して、新しい割り当てに利用できるようにします。これにより、メモリリークを防ぎ、効率的なメモリ利用を保証します。
世代別仮説
世代別仮説は、V8を含む多くの最新ガベージコレクタの基礎となる基本的な原則です。それは次のように述べています。
- ほとんどのオブジェクトは若くして死ぬ: 新しく割り当てられたオブジェクトの大部分は、比較的すぐに到達不能になります。
- 長命なオブジェクトは長命であり続ける傾向がある: いくつかのガベージコレクションサイクルを生き残ったオブジェクトは、長期間生き続ける可能性が高いです。
この仮説により、ガベージコレクタはヒープを異なる「世代」に分割し、それぞれに異なるコレクション戦略を適用することで、プロセスを最適化できます。
Orinoco
V8のガベージコレクションパイプライン全体の包括的な名称です。V8のGCにおける重要な進化を表しており、並列、並行、増分的コレクション技術を導入して、Webアプリケーションにおけるスムーズなユーザーエクスペリエンスの重要な要素である一時停止時間を削減しています。
V8の世代別ガベージコレクション
V8は、ヒープを主に2つの世代に分割します。ヤングジェネレーション(またはスキャベンジスペース)とオールドジェネレーション(またはオールドスペース)です。各世代には、最適化された独自のコレクションアルゴリズムがあります。
ヤングジェネレーション(スキャベンジスペース)
ヤングジェネレーションは、新しく割り当てられたオブジェクトが最初に配置される場所です。ほとんどのオブジェクトは若くして死ぬという世代別仮説を反映して、通常はヒープの小さい部分です。
スキャベンジャーアルゴリズム
V8は、ヤングジェネレーションのためにCheneyのアルゴリズムに基づくスキャベンジャーを使用します。これはセミスペースコピーコレクタです。ヤングジェネレーションは、等しいサイズの2つのセミスペース、From-spaceとTo-spaceに分割されます。
仕組みは次のとおりです。
- 新しく割り当てられたオブジェクトはFrom-spaceに配置されます。
- From-spaceがいっぱいになると、スキャベンジコレクションがトリガーされます。
- スキャベンジャーは、ルートからオブジェクトグラフをたどることによって、From-space内のすべての「ライブ」オブジェクトを識別します。
- ライブオブジェクトはTo-spaceにコピーされます。このコピー中に、オブジェクトは移動および圧縮され、断片化が解消されます。
- すべてのライブオブジェクトがコピーされた後、From-spaceとTo-spaceの役割が交換されます。前のFrom-space(空またはゴミのみを含む)は新しいTo-spaceになり、次のサイクルで新しいオブジェクトまたはコピーされたオブジェクトを受信する準備ができます。
スキャベンジコレクションを生き残ったオブジェクト(つまり、To-spaceにコピーされたオブジェクト)は「エイジド(aged)」であると言われます。オブジェクトが2回のスキャベンジコレクションを生き残った場合、「成熟した」と見なされ、オールドジェネレーションに昇格されます。
例(概念):
let obj1 = { a: 1 }; // Young Generation (From-space)に割り当て let obj2 = { b: 2 }; // Young Generation (From-space)に割り当て function doSomething() { let tempObj = { c: 3 }; // Young Generation (From-space)に割り当て // doSomethingが戻るときにtempObjは到達不能になる } doSomething(); // tempObjは現在ゴミ // スキャベンジコレクションが発生します。 // obj1とobj2はまだ到達可能なので、To-spaceにコピーされます。 // tempObjは到達可能でないため、残されています(ゴミ)。 // コレクション後、From-spaceとTo-spaceの役割が交換されます。obj1, obj2は現在「新しい」From-spaceにあります。 // obj1とobj2が別のスキャベンジを生き残った場合、Old Generationに昇格する可能性があります。
スキャベンジャーは、ライブオブジェクトのみを処理すればよいため、短命なオブジェクトに対して非常に効率的です。コストは、総ヒープサイズではなく、ライブオブジェクトの数に比例します。
オールドジェネレーション(オールドスペース)
オールドジェネレーションは、複数のスキャベンジコレクションを生き残ったオブジェクトを格納します。そのため、長命であると想定されています。このスペースは、通常、ヤングジェネレーションよりも大きいです。
マーク-スイープ・コンパクトアルゴリズム
オールドジェネレーションに対して、V8はマーク・スイープ・コンパクトアルゴリズムを採用しています。このプロセスは、スキャベンジよりも複雑で、潜在的に時間がかかる可能性があります。
-
マークフェーズ: GCは、ルートからオブジェクトグラフをたどることによって、オールドジェネレーション内のすべての到達可能なオブジェクトを識別します。これらのオブジェクトを「ライブ」としてマークします。このフェーズは、一時停止時間を最小限に抑えるために、JavaScript実行と並行して実行される(並行マーク)ことができます。
-
スイープフェーズ: マークの後、GCはオールドジェネレーションヒープ全体を反復処理します。ライブとしてマークされなかったオブジェクトはゴミと見なされます。これらのゴミオブジェクトによって占有されているメモリは、フリーリストに追加され、新しい割り当てに利用できるようになります。このフェーズも並行して実行される(並行スイープ)ことができます。
-
コンパクトフェーズ(オプション): オブジェクトが割り当てられ、解除されるにつれて、オールドジェネレーションは断片化する可能性があります。つまり、ヒープ全体にわたって、小さく使用できない空きメモリのギャップが分散している状態になります。これを解決するために、コンパクトフェーズがトリガーされることがあります。圧縮は、ライブオブジェクトをまとめてギャップを解消し、割り当て効率を改善することを含みます。圧縮は一般的にストップ・ザ・ワールド操作ですが、V8はOrinocoパイプライン内で「並列圧縮」(複数のスレッドがヒープの異なる部分を圧縮する)や「増分圧縮」(小さなステップで圧縮する)などの技術を利用して、影響を軽減しています。
例(概念):
let globalConfig = { version: '1.0', settings: { timeout: 5000 } }; // 長命なオブジェクト。Old Generationに昇格する可能性が高い // 他の多くのオブジェクトがYoung Generationで作成およびガベージコレクションされる。 // 最終的に、Old Generationがいっぱいになるか、V8がメジャーコレクションが必要だと判断する。 // マークフェーズの開始: globalConfigはライブとしてマークされる。 // スイープフェーズ: Old Generation内の到達不能なオブジェクトのメモリが回収される。 // コンパクトフェーズ(必要な場合): globalConfigはヒープの断片化を解消するために異なるメモリ位置に移動される可能性がある。
Orinoco:最新のV8 GCパイプライン
Orinocoは、ストップ・ザ・ワールドの一時停止を最小限に抑え、スムーズなユーザーエクスペリエンスを提供するように設計された、V8の洗練された多角的なガベージコレクションアプローチを表します。さまざまな技術を統合しています。
- 世代別GC: 前述のように、オブジェクトをヤングジェネレーションとオールドジェネレーションに分離します。
- 増分GC: すべてのGCプロセスを一度に実行するのではなく、OrinocoはメジャーGCサイクルをより小さなステップに分割できます。たとえば、マーキングは、JavaScript実行とインターリーブされた増分で実行できます。
- 並行GC: GC作業の一部(マーキングとスイープなど)は、メインのJavaScript実行スレッドと並行して、別のスレッドで実行できます。これにより、メインスレッドの一時停止時間が大幅に短縮されます。
- 並列GC: 複数のヘルパースレッドが同じGCタスクで並列に作業できるため、圧縮などの操作が高速化されます。
- アイドルタイムGC: V8はJavaScript実行中のアイドル時間を利用してGC作業を実行できるため、インタラクティブなパフォーマンスへの影響を最小限に抑えられます。
これらの技術は、Orinocoの下で統合され、多くのシナリオでほぼストップ・ザ・ワールドなしのガベージコレクションを実現し、WebアプリケーションやNode.jsサーバーの応答性を向上させます。
結論
V8のガベージコレクションを、効率的なヤングジェネレーションスキャベンジャーから、Orinocoパイプラインによってオーケストレーションされた洗練されたオールドジェネレーションマーク・スイープ・コンパクトサイクルまで理解することは、真剣なJavaScript開発者にとって不可欠です。オブジェクトのライフサイクルを最適化し、短命なオブジェクトを可能な限り優先し、長命な参照を意識することで、私たちは暗黙的にV8のメモリ管理をガイドし、よりパフォーマンスが高く安定したアプリケーションにつながります。最終的に、V8の革新的なガベージコレクション戦略は、高性能なJavaScriptランタイムエクスペリエンスを提供する継続的な努力の証です。