非常に悪い説明を見つけたのか、それとも詳細を期待しすぎたのかはわかりませんが、私が見た説明には非常に満足しています。説明が簡潔で単純に聞こえる場合、それは実際にはかなり単純なメカニズムだからです。
すでにご存知のように、世代別ガベージ コレクターは、若いオブジェクトを参照する古いオブジェクトを列挙できる必要があります。すべての古いオブジェクトをスキャンするのは正しいことですが、それでは世代別アプローチの利点が失われるため、対象を絞り込む必要があります。その方法に関係なく、書き込みバリアが必要です。これは、(参照型の) メンバー変数が割り当てられたり書き込まれたりするたびに実行されるコードです。新しい参照が若いオブジェクトを指し、それが古いオブジェクトに格納されている場合、書き込みバリアはその事実をガベージ コレクションに記録します。違いは、その記録方法にあります。若いオブジェクトへの参照を持っている(ある時点で持っていた)すべての古いオブジェクトのコレクションである、いわゆる記憶セットを使用する正確なスキームがあります。ご想像のとおり、これにはかなりのスペースが必要です。
カード テーブルにはトレードオフがあります。どのオブジェクトに若いポインターが正確に含まれているか (または、少なくともある時点で含まれていたか) を伝える代わりに、オブジェクトを固定サイズのバケットにグループ化し、どのバケットに若いポインターを持つオブジェクトが含まれているかを追跡します。もちろん、これによりスペースの使用量が削減されます。正確を期すために、一貫性がある限り、オブジェクトをどのようにバケット化するかは問題ではありません。効率を高めるために、メモリアドレスでグループ化し(無料で利用できるため)、より大きな2の累乗で割って(除算を安価なビット単位の操作にするため)、それらをグループ化します。
また、バケットの明示的なリストを維持する代わりに、可能なバケットごとに事前にスペースを予約します。具体的には、N ビットまたはバイトの配列があり、N はバケットの数であるためi
、th バケットに若いポインターが含まれていない場合は 0 になり、i
若いポインターが含まれている場合は 1 になります。これがカードテーブル本体です。通常、この領域は、ヒープ (の一部) として使用される大きなメモリ ブロックと共に割り当てられ、解放されます。拡張する必要がない場合は、メモリ ブロックの先頭に埋め込むこともできます。アドレス空間全体がヒープとして使用されていない限り (これは非常にまれです)、上記の式はstart_of_memory_region >> K
0 ではなく 0 から始まる数値を与えるため、カード テーブルへのインデックスを取得するには、ヒープの開始アドレスの開始を減算する必要があります。
要約すると、書き込みバリアは、ステートメントsome_obj.field = other_obj;
が古いオブジェクトに若いポインターを格納していることを検出すると、次のことを行います。
card_table[(&old_obj - start_of_heap) >> K] = 1;
は、現在若いポインター&old_obj
を持つオブジェクトのアドレスです (古いオブジェクトを参照することが決定されたため、既にレジスターにあります)。マイナー GC 中に、ガベージ コレクターはカード テーブルを調べて、若いポインターをスキャンするヒープ領域を決定します。
for i from 0 to (heap_size >> K):
if card_table[i]:
scan heap[i << K .. (i + 1) << K] for young pointers