引子:今天在看CLR via C#的时候看到C#的垃圾回收算法--引用跟踪算法的时候想到以下几个问题。
一、引用计数法存在的问题
一般引用计数法存在的问题就是不好处理循环引用的问题,但是C++不是有weak_ptr吗?
这个引用跟踪的垃圾回收算法看起来还蛮复杂的,跟引用计数法比起来性能消耗估计得大的更多。难不成是引用计数法还有解决不了的问题?于是便用C++的智能指针想出了一种场景:就是既有循环又得传递参数的情形,代码如下所示:
class A;
class B;
class A
{
public:
std::shared_ptr<B> ptrb;
int k = 1;
};
class B
{
public:
std::weak_ptr<A> ptra;
int k = 2;
};
//可以解决循环引用
void testPtr()
{
std::shared_ptr<B> ptrB(new B());
std::shared_ptr<A> ptrA(new A());
ptrA->ptrb = ptrB;
ptrB->ptra = ptrA;
}
//但是还想能解决参数传递,似乎就不行了
void testRef(std::shared_ptr<B>& b)
{
std::shared_ptr<B> ptrB(new B());
std::shared_ptr<A> ptrA(new A());
ptrA->ptrb = ptrB;
ptrB->ptra = ptrA;
b = ptrB;
}
int main()
{
std::shared_ptr<B> b;
testRef(b);
std::cout << b->ptra.lock()->k << std::endl;//在这就出错了,该空间被释放掉了
return 0;
}
上面的代码构造了一个即想要解决循环引用又想解决对象的参数传递,但不幸的是在参数传递的时候因为弱指针指向的空间为空导致异常。这么一想引用计数好像还确实比较麻烦,有些极端的情况解决不了。
二、C#中的循环引用
class A
{
public B ptrb;
public int k=1;
}
class B
{
public A ptra;
public int k = 2;
}
static class Program
{
static void tesrRef(out A aptr)
{
A a = new A();
B b = new B();
a.ptrb = b;
b.ptra = a;
aptr = a;
}
public static int Main()
{
A a;
tesrRef(out a);
Console.WriteLine(a.ptrb.k);
return 0;
}
}
然后试了下用C#解决循环引用确实很不错,即使传递参数也不会出现啥问题。
然后看看CLR via C#中对引用跟踪法的解释:
然后简单凭个人理解了下跟引用跟踪法比引用计数法厉害的地方(可能理解的还不太准确,先暂时这么说服自己):引用跟踪法可以把一个根中的所有成员变量一起作为根拿出来去引用开辟在托管堆中对象,因此在一个根不引用托管堆中对象的对象的时候会让其成员变量根也一起不引用其所对应的托管堆对象,而这正是引用计数法做不到的。引用计数法只能让自身的根不引用自身的堆对象而不能让自身的成员变量不去引用堆对象因此才会造成循环引用问题。
本文参考:
智能指针之弱引用的指针
New book: CLR via C#, Fourth Edition | Microsoft Learn