ガベージコレクションの仕組みを徹底解説(第2回:Rubyの実装編)
Ruby Magicの前回の記事では、なぜガベージコレクション(GC)が必要なのか、そしてその基本的な仕組みについて解説しました。今回はさらに踏み込んで、RubyにおけるGCの実装方法について詳しく見ていきます。
さまざまなRuby実装
Rubyには複数の実装が存在します。代表的なものとして、MRI(Matz's Ruby Interpreter)、Rubinius、JRubyの3つが挙げられます。それぞれの実装で採用されているGCの方式は異なります。本記事では、最も多くのRuby開発者が利用しているMRIに焦点を当てて解説します。
Rubyのヒープ
コンピュータのメモリには「スタック」と「ヒープ」の2種類があります。スタックは非常に高速で、関数呼び出しのコンテキストにローカルな領域です。つまり、スタック上で宣言された変数は、関数の処理が完了すると同時に自動的に解放されます。ただし、スタックはサイズが非常に限られているため、画像やファイルのデータなど、大きなオブジェクトを格納することはできません。
この特性は、Rubyオブジェクトの保存には適していません。Rubyオブジェクトはメソッド呼び出しよりも長い期間生存することが多く、また、オブジェクトがスタックに収まるサイズかどうかを事前に予測するのはほぼ不可能だからです。
そこでRubyは、もう一方のメモリである「ヒープ」を使用します。ヒープでは、プログラムが必要なメモリを確保し、使い終わった後に自ら解放する責任を負います。Rubyはこの仕組みを利用して、Rubyオブジェクトを格納するためのメモリ領域を確保しています。これが「Rubyのヒープ」と呼ばれるものです。
| スタックとヒープの比較 | |
|---|---|
| スタックメモリ | 非常に高速 関数呼び出しの終了時に使用済みメモリが自動的に解放される サイズが非常に限られている |
| ヒープメモリ | スタックよりやや低速 自動的なクリーンアップなし サイズはコンピュータの利用可能メモリによってのみ制限される |
したがって、Rubyオブジェクトは常にRubyのヒープ上に割り当てられます。オブジェクトのサイズが40バイト未満の場合、その内容はオブジェクト自体に直接埋め込まれます。それ以外の場合、オブジェクトはRubyのヒープ上の別のメモリセグメントを参照します。つまり、Rubyオブジェクトはしばしばメモリ上の全く異なる2つの場所に格納されることになります。Rubyのヒープがいっぱいになると、新しいオブジェクト用に新しいヒープが作成されます。
Mark and Sweep(マーク&スイープ)
MRIは「Mark and Sweep(マーク・アンド・スイープ)」と呼ばれるGCアルゴリズムを採用しています。まず最初にマークフェーズが実行されます。マークフェーズでは、ガベージコレクタが現在存在するすべてのオブジェクトを走査し、クリーンアップ可能と判断したオブジェクトにマークフラグを設定します。
マークフェーズの間、コードの実行は一時停止されます。これは、ガベージコレクタがオブジェクト間のすべての関係性を正確に把握する必要があるためです。マーク処理中にプログラムが動作し続けると、その間に状態が変化し、ガベージコレクタがオブジェクトの現在の状態を確実に判断できなくなってしまうからです。
次にスイープフェーズが開始されます。Ruby 1.9以降では、このフェーズはバックグラウンドで実行されます。ガベージコレクタは、マークフェーズでマークされたオブジェクトを静かに解放していきます。メモリが再び利用可能になるのは、スイープ完了後です。
マークフェーズはコードの実行を停止させるため、本番環境で問題が発生しやすいポイントとなります。一方、スイープフェーズは比較的影響の少ない処理です。
| Mark and Sweep のまとめ | |
|---|---|
| マークフェーズ | 既存のオブジェクトを走査する クリーンアップ可能なオブジェクトにマークフラグを設定する コードの実行を停止させる |
| スイープフェーズ | バックグラウンドで実行される マークされたオブジェクトを解放する スイープ完了後にメモリが再び利用可能になる |
メジャーGCとマイナーGC
Ruby 2.1以降のガベージコレクタは、「メジャーGC」と「マイナーGC」の2種類の実行を行います。GCはどのオブジェクトが新しいかを追跡しており、数回のGCを生き延びたオブジェクトは「老齢(old)」として扱われます。老齢オブジェクトはマイナーGCでは無視されます。これにより、マイナーGCでは新しく割り当てられたオブジェクトのみを走査すればよいため、処理の負荷が大幅に軽減されます。
これは非常に有効な仕組みです。というのも、メモリの中には決してクリーンアップされるべきではない部分が存在するからです。例えばRailsアプリケーションを起動すると、Railsフレームワーク全体がメモリにロードされます。これらはアプリケーションのライフタイム全体を通じてメモリに残り続けるため、毎回すべてのオブジェクトをチェックするのはリソースの無駄になってしまいます。
| メジャーGCとマイナーGCの比較 | |
|---|---|
| メジャーGC | 実行頻度が低い 実行コストが高い |
| マイナーGC | 老齢オブジェクトを無視する 実行頻度が高い 実行時の負荷が小さい |
さらに深く学びたい方へ
本記事の内容については、Aman Gupta氏の素晴らしいブログから多くを学びました。このテーマについてより深く知りたい方は、ぜひ同ブログをご覧ください。
次回予告:実践的なガベージコレクションチューニング
ガベージコレクタの動作をチューニングするために測定できる指標や、変更可能な設定項目は多数存在します。このGCシリーズの次回では、これらの指標と設定パラメータについて詳しく解説します。
-
TCMallocを使ってRubyのメモリ割り当てをプロファイリングする方法
Rubyでは、メモリの割り当てはどのように行われているのでしょうか? Rubyは「ページ(page)」と呼ばれる塊単位でメモリを確保し、新しく生成されたオブジェクトはこのページの中に格納されていきます。 しかし、 ページがいっぱいになると、さらなるメモリが必要になります。 そのときRubyは、malloc関数を使ってオペレーティングシステム(OS)に追加のメモリを要求します。 このmalloc関数はOS標準で提供されているものですが、代替となる実装に差し替えることも可能です。 その代表的な実装のひとつが、Google製のTCMallocです。 TCMallocは「gperftools(Goog
-
Rubyの内部構造に迫る:オブジェクトのメモリレイアウトを徹底解説
Rubyの内部構造をちょっと覗いてみませんか? それなら、この記事はきっとお役に立ちます。 なぜなら… この記事では、Rubyオブジェクトがメモリ上でどのように配置されているのか、そして内部データ構造を操作してクールなことを実現する方法を、一緒に探検していきます。 シートベルトを締めて、Rubyインタプリタの深淵への旅に出かけましょう! 配列(Array)のメモリレイアウト 配列を作成すると、Rubyはそのデータを保持するためにシステムメモリと、少しのメタデータを確保します。 メタデータには以下が含まれます: 配列のサイズ(要素数) 配列の容量(capacity) クラス情報 オブジェクトの