.Net 7 GC垃圾回收对象跨代简析

楔子

很久没有发.Net 7 CLR的文章了,本篇来看下之前研究过的跨代的两个函数

问题

跨代对象的引用的定义是:比如第一代的某个对象(ObjOne)的成员(MemberOne)引用了第0代的某个对象(ObjTwo),如果此时GC垃圾回收的时候,回收的是第0代。假设第0代上没有对象引用ObjTwo,ObjTwo可能会被回收掉。而此时,因为对象被回收了,所以MemberOne引用的就是空值。这会导致出错。.

为了解决这个问题,Card_table出现了。

代码

先上一段简单的代码,作为例子说明

  internal class Program
    {
        class A
        {
            public Program PMA=null;
        }
        static void Main(string[] args)
        {
            Console.WriteLine("Tian Xia Feng Yun Chu Wo Bei!");
            Console.ReadLine();
            A a=new A();
            GC.Collect();
            Program PM =new Program();
            a.PMA = PM;
            GC.Collect();
        }
    }

对象a因为被GC过一次,所以升代了。PM对象在第一次GC之后,第二次GC之前,所以它一定是第0代。不同代的对象a和PM,对象a的成员PMA被赋值为了0代的PM对象,所以造成了对象引用的跨代。

Card_table

卡片表(card_table),主要是在第1代某个对象的成员引用第0代某个对象的时候,设置第1代对象的成员在卡片表里面相对应的字节为0XFF,二进制为1111 1111。

这里,CLR规定了卡片表一个位(bit)为对应256个字节也就是2的8次方。8个bit位也就是一个字节则对应了2的11次方个字节,十进制的2048个字节。如果再升级下,比如,4个字节对应了2的13次方个字节,十进制是8192个字节。

设置卡片表

先看一段代码:

inline
void gc_heap::set_card (size_t card)
{
    size_t word = card_word (card);
    card_table[word] = (card_table [word] | (1 << card_bit (card)));

#ifdef FEATURE_MANUALLY_MANAGED_CARD_BUNDLES
    // Also set the card bundle that corresponds to the card
    size_t bundle_to_set = cardw_card_bundle(word);

    card_bundle_set(bundle_to_set);

    dprintf (3,("Set card %Ix [%Ix, %Ix[ and bundle %Ix", card, (size_t)card_address (card), (size_t)card_address (card+1), bundle_to_set));
#endif
}

局部变量card就是一个字节的长度,局部变量word则是表示四个字节的长度。card_bit是一个字节的长度模(%)上2的五次方的结果。设置card_table[word]可以看到它的值实际上是它自身与(|)上1左移card_bit的结果。

寻找卡片表

同样先上代码

BOOL gc_heap::find_card(uint32_t* card_table,
                        size_t&   card,
                        size_t    card_word_end,
                        size_t&   end_card)
{
    uint32_t* last_card_word;
    uint32_t card_word_value;
    uint32_t bit_position;

    if (card_word (card) >= card_word_end)
        return FALSE;

    // Find the first card which is set
    last_card_word = &card_table [card_word (card)];
    bit_position = card_bit (card);

    {
        card_word_value = (*last_card_word) >> bit_position;
    }

    if (!card_word_value)
    {
        do
        {
            ++last_card_word;
        }

        while ((last_card_word < &card_table [card_word_end]) && !(*last_card_word));
        if (last_card_word < &card_table [card_word_end])
        {
            card_word_value = *last_card_word;
        }
        else
        {
            // We failed to find any non-zero card words before we got to card_word_end
            return FALSE;
        }
#endif //CARD_BUNDLE
    }

    // card is the card word index * card size + the bit index within the card
    card = (last_card_word - &card_table[0]) * card_word_width + bit_position;

    do
    {
        bit_position++;
        card_word_value = card_word_value / 2;
        end_card = (last_card_word - &card_table [0])* card_word_width + bit_position;
    } while (card_word_value & 1);

    end_card = (last_card_word - &card_table [0])* card_word_width + bit_position;
    return TRUE;
}

这里面的整个过程就是设置扫描档范围,当发现0代的某个对象在卡片表相对应的位置设置了,那么这个对象将不会被回收。虽然它没有被0代其它对象引用。

局部变量 last_card_word 是卡片表四字节处的地址。
局部变量bit_position是card_bit计算的结果。

 card_word_value = (*last_card_word) >> bit_position;

上面这句就是找到真正的card_table[card_word(card)]所在的卡片标记值。因为set_card到时候,设置了位于1左移card_bit的值。
++last_card_word实际上是找到卡片表结束的地址。

card = (last_card_word - &card_table[0]) * card_word_width + bit_position;

卡片表的1字节表示公式,具体为:结束四字节卡片表的地址减去四字节卡片表的起始地址然后乘以三十二,再加上被去除掉的余数,结果就是卡片表1字节地址

end_card = (last_card_word - &card_table [0])* card_word_width + bit_position;

这个bit_postion经过循环之后,找到的结束卡片表1字节的地址。

原理

它的原理主要是通过遍历当前卡片表所表示的范围,其实范围,和结束范围。在这个里面搜索第0代被其它代所引用的对象。然后进行标记,不至于被误回收。