一、简介
这是一个自己写C++垃圾自动回收器,用到的都是标准C++语法。采用了引用计数加mark-sweep的方法。在没有循环引用的情况下,引用计数可以保证垃圾实时得到回收;对于有循环引用的情况下,计数就不能回收了,这时就要用mark-sweep的方法。其实完全使用mark- sweep的方法也是可以的,但有了引用计数,可以回收大量的非循环引用垃圾,减少最后的mark-sweep时的工作量。
考虑到大家的15分钟阅读热情,在说细节之前,先show一下这个指针怎么使用。顺便提一下,这个指针可以在Windows+MSVC和Linux+GCC下编译,使用。代码下载在http://download.csdn.net/source/267439
class A
{
SmartPtr<B> b;
};
class B : public A
{
SmartPtr<A> a;
};
void main()
{
A* _a = new A(c);
SmartPtr<A> pA = _a;
SmartPtr<B> pB = new B(c); //#add没有兼容auto_ptr用法atuto_ptr<B> p(new B) ,为一不足
pA->b = pB;
pB->a = pA;
SmartPtr<A> pA1 = new A(c);
pA1 = new A(c);
pA1->b = pB;
SmartPtr<C> pC = new C();
SmartPtr<B> pB = pC;
ArrayPtr<SmartPtr<A> > ps = new SmartPtr<A>[2];
ps[0] = pA;
}
下面是一些细节,有兴趣的网友可以接着看。
二、C++已经提供了的辅助内存管理方法
其实,在C++的标准库里面,有一个auto_ptr的类,这个类在一定程度上帮助开发人员进行内存管理,避免内存泄露。比如可以这样用:
void myfun()
{
auto_ptr<MyClass> myObj(new MyClass());
//. . . some other operations
//myObj will be freed automatically
}
这个方法用在函数中的局部变量时非常有效,这个有效是相对于C语言时代,经常在开始的地方分配内存,然后在每一个函数return语句之前进行内存释放, C语言时代,只要足够细心,这种“人肉保证”还是靠的住的。C++代码中,由于异常的存在,使得程序常常在无法预知的地方退出,前面的这中方法也就彻底失效了。auto_ptr正是弥补了这个空缺,利用C++异常处理堆栈回卷的时候会清除所有的临时对象这个特点,在auto_ptr的析构函数里进行内存释放。
然而auto_ptr并不适合做全局对象或者要在一个大的上下文范围内活动的对象的管理。原因在于auto_ptr不允许一个对象同时被多个指针对象拥有,这样就无法在多个上下文中引用同一个对象。
不能被多个上下文同时引用,使得auto_ptr在Win32 COM编程时毫无用处。因为COM的基本思想就是服务共享,并且接口与对象分离,客户代码得到的几个不同的接口实际上可能是由同一个对象提供的。因此微软的VC++同时也提供了另外一个指针对象,就是CComPtr。与auto_ptr不同,CComPtr在指针赋值时不是把控制器从一个指针对象移交给另一个指针对象,而是调用AddRef对引用计数器加1,析构时用Release将引用计数减1,达到多个引用共享对象的目的(#add 这是核心)。auto_ptr并不拥有引用计数,因而在指针析构时直接调用delete把引用对象销毁。引用计数在一定程度上实现了内存的自动管理,可以允许一个对象被多个上下文环境引用,并在合适的时候自动释放。
然而,当对象之间存在循环引用时,这种基于引用计数的方法就不能好好工作了。比如,两个对象A和B互相引用,而且A和B都不被其他对象引用。因为A和B不能被从根对象开始的引用路径访问到,就成为了内存垃圾。但是A和B相互引用,导致A和B的引用计数都是1,而不是0,这样A和B就不能被自动回收。
三、基于引用计数的垃圾回收
虽然引用计数不是完整的回收方法,但仍然是一种非常有用的方法。最重要的是,这是一种成本非常低廉的方法。就像汽车安全带相比安全气囊,前者以极其低廉的成本避免了90%的安全伤害。因此,在本文实现的内存回收方法里,依然把引用计数作为垃圾回收方法的一部分,加上额外的搜索方法完成完整的垃圾回收工作。
要使用引用计数进行垃圾回收,显然首先要实现引用计数。所谓的引用计数就是记录当前指向对象的引用个数。在C++里,也就是指向某个对象的指针的个数。当一个指针引用到一个对象时,就把这个对象的引用计数加1,这样有多少个指针引用,计数就是几。相反,在一个指针不再引用该对象时(比如指针超出了作用域,指针被赋了其他值,指针所在的对象被销毁),就把引用计数减1,当引用计数减到0时就说明没有指针在使用该对象,就可以把这个对象删除掉了。引用计数要每个对象都保留一个计数器,并且提供一个加计数的操作和减计数的操作,以及在计数值到达0时进行删除。这正是COM实现里面必不可少一部分,因此每个COM对象都会这样的一些相似的代码。显然,这是一个可以重复使用的共同部分,完全可以做成一个基类Countable,然后让需要的类从这个类继承就可以了。
这个方法仍有一个小问题。通常,在一个软件设计里面类之间的关系并不是任意的。不同的类之间会有继承关系,如果全部要求从这个能进行计数的Countable进行继承,会引入多重继承,C++设计里面经常会希望能避免多重继承。另外,程序往往会需要使用类库里面已有的类,这些类都不是从Countable继承的,而且开发者也没有这些类库的源代码,不可能修改这些已有类的定义从Countable继承。同样,对于整型,浮点型这些基本数据类型,不从任何类继承,更无法要求其从我们上面说的类继承。为了能把引用计数应用的更为广泛的,我们要换另外一个方法。
我们要实现的目的是把一个引用计数器和一个对象联系在一起。类的继承通过在编写代码时指定的继承关系,分配一个子类对象时 内部自然包含父类的数据成员,形成一片连续的内存空间。这就像是用数组存储对象,一个挨着一个。除了数组,另一种常用的线性数据结构是链表,对象之间通过指针相连。同样的原理,我们也可以把引用计数和被计数的对象通过指针连接在一起。
这样完成后,客户代码引用对象时就要引用带有引用计数器的新对象。就像是在原来的对象外面又包裹了一层。因此,我们把这个提供引用计数的类命名为ObjectWrapper。
class ObjectWrapper
{
int count; //reference count
void *pTarget; //the real object allocated by usrer;
public:
void addRef();
void release();
void* getTarget();
};
细心的读者看到上面的代码可能就要问了,这里为什么不使用模板?我要提醒一下,我们现在还是在试图实现一个能够被类似CComPtr的指针配合使用的对象,随后我们会实现一个类似CComPtr指针的SmartPtr。ObjectWrapper类对客户代码是透明的,在这里提供的类型信息不会对客户代码有用,另外,ObjectWrapper对象需要被一个对象管理器管理起来,使用了模板的话不同对象就会是不同的类型,给对象管理带来困难。随后就会看到SmartPtr的实现使用了模板。
在ObjectWrapper类里面,提供了addRef, release, getTarget这三个访问函数。谁,在什么时候来调用这三个函数呢?自然,这三个函数是要被客户代码使用的。但是,如果客户代码随时要想着在合适的位置调用addRef, release的话,对程序员的负担恐怕比要求用delete进行手工内存释放还要重,所谓的垃圾自动回收更是不可能的。只有能让这些操作自动进行,垃圾回收才能成为可能。因此我们接下来要实现一个SmartPtr类,作为指针对象使用。这个指针对象要
1. 在指针构造,赋值,析构时调用addRef, release管理引用计数
2. 提供C++裸指针能提供的所有操作,包括->操作符,*操作,与NULL的比较,或者临时蜕化为裸指针。好让程序员认为他真的在使用一个C++的“指针”,虽然你其实这已经被一个“对象”代替了。
SmartPtr对象包含一个指向ObjectWrapper对象的指针。
要实现第一点,只需要在SmartPtr的构造函数,拷贝构造函数,赋值操作符,析构函数里面加上对ObjectWrapper对象的addRef, release函数的调用即可。第二点,需要重载->运算符和一个隐含类型转换符。这个要复杂一点,本文就不详述了,后面的代码可以说明一切,而且,这些都不是新鲜玩意儿,在《More Effective C++》的第29条早有详细的描述了,大家可以去看这本书。
template<typename T>
class SmartPtr
{
private:
ObjectWrapper *pWrapper;
public:
T* operator->() {return getTarget();}
const T* operator->() const {return getTarget();}
~SmartPtr(void)
{
pWrapper->release();
}
SmartPtr<T>& operator=(T* p)
{
ObjectWrapper *old = pWrapper;
pWrapper = WrapperManager::getInstance()->getWrapper<T>(p);
old->release();
return *this;
}
SmartPtr<T>& operator=(const SmartPtr<T> &ptr)
{
if(pWrapper == ptr.pWrapper)//assign to self
return *this;
pWrapper->release();
pWrapper = ptr.pWrapper;
pWrapper->addRef();
return *this;
}
SmartPtr(const SmartPtr<T>& ptr)
{
SmartPtrManager::getInstance()->add(this);
pWrapper = ptr.pWrapper;
pWrapper->addRef();
}
SmartPtr(T* pObj=NULL)
{
SmartPtrManager::getInstance()->add(this);
pWrapper = WrapperManager::getInstance()->getWrapper<T>(pObj);
}
operator const T* () const
{
return getTarget();
}
operator T* ()
{
return getTarget();
}
private:
inline const T* getTarget() const
{
return static_cast<T*> (pWrapper->getTarget());
}
inline T* getTarget()
{
return static_cast<T*> (pWrapper->getTarget());
}
};
到此为止,我门已经实现了一个使用引用计数进行内存管理的垃圾回收。
四、用mark-sweep方法进行垃圾回收
我们前面已经说过了,只使用引用计数进行垃圾回收在有循环引用的情况下就会失效。因此我们需要更有效、更主动的回收算法。在进行更为主动的垃圾回收之前,需要解决的最基本的一个问题就是:什么样的对象是垃圾对象,也就是说,如何将垃圾对象与非垃圾对象区分开来。 解决这个问题需要能做到下面两点:
1.能够界定对象的大小和起始位置。
2.能够分辨对象的当前使用状态,找出垃圾对象。
对于第一个问题,之所以会出现这个问题,是因为在C++的内存管理里面,是缺乏类型信息的。也就是说,给定一个地址,我们并不知道这个地址是属于那个对象,什么类型,以及这个地址的对象大小。在这个问题上RTTI机制也无法为我们提供任何帮助,RTTI只能提供对象类型名字等有限的支持,而且只能对多态对象,也就是定义了有虚函数的对象有用。最理想的方法是如果我们能Hook所有的对象分配,自然能获得每一个对象的大小,起始地址这些第一手信息。一般,我们可以重载C++的operator new(注意,不是new operator,不是文字游戏,这两个是有区别的)来达到这个目的,但是往往有些类在设计时就已经重载了new了,而且我们的重载只能是全局的,不能代替类自己对new的重载。结果就是,如果某个类自己重载了new运算符,我们的全局重载就不会被使用,我们也就丧失了跟踪对象分配的能力。所幸,无论是VC++还是GCC都在各自的C运行库里面提供一些API函数,使得我们仍然可以得到对象的大小和起始位置,这样就成功解决了第一个问题。为了不离开垃圾回收这个主话题太远,这个方法的具体实施后面再专门讲。
第二个问题找出垃圾对象是最关键的问题。对象的使用状态可以分为可到达和不可到达两种。处于可到达状态,也就是可以被用户通过合法的引用路径引用到(C++允许用户进行任意的类型转换,你甚至可以把一个整数转换成一个地址。所以,在C++里,你“总是”有办法访问到任一个地方,但这不是我们要讨论的内容)的对象,是要保留的。而处于不可到达状态的对象就是要回收的垃圾了。前面设计的对对象进行引用计数,就是一种简单而有可靠的标识对象状态的方法。如果对象的引用计数为0,那么对象一定是不可到达,因为没有任何指针指向该对象。但这句话的逆命题,如果对象不可到达,那么引用计数一定为0,却并不正确。这是因为可能有循环引用的存在。这个时候,最直接的方法就是最可可靠的方法。我们只要从根引用开始,依次向下将所有的子对象访问一遍,能被访问到的对象就是可到达的,其他的就是不可到达的。
下面我们就开始着手将所有的对象访问一遍。为了使描述更加具体化,我以下面的代码作为例子进行讨论
class A { ... };
class B
{
public:
SmartPtr<A> pA;
B() { pA = new A(); }
...
};
void main()
{
SmartPtr<B> pB = new B();
...
}
在这段代码里,我们分配了两类的两个对象,pA指向的A对象和pB指向的B对象,同时有两个指针pA和pB。pB是在main函数里面定义的,和整个应用程序具有相同的声明周期,是根指针。pA是处于B对象的内部,B对象销毁时这个指针也会被销毁,不是根指针。按照下面的规则,我们开始访问所有的对象:
1. 将所有的指针对象分成根指针和非根指针。但是我们并不知道根指针在什么地方创建,幸好,我们知道非根指针总是位于其他对象内部。我们依次检查pA,pB, 发现pA位于B对象内部,pB不位于任何对象内部,因此pB是根指针,pA不是。
这个工作不需要每次进行垃圾扫描时都重复进行,指针对象一旦生成,它的地位是不会变的,不可能从根指针变成非根指针,也不可能相反。只是在两次垃圾扫描的间隙时间,会有新的指针生成,需要检查新生成的指针,确定新生成的指针的类型。
2.对于每一个根指针,访问它只向的对象,并对该对象内部的所有指针,重复这个过程。从pB,我们访问到了B对象,然后在B对象内部,我们发现了pA指针,然后重复,从pA指针访问到A对象,在A对象内部,没有发现任何指针。
每次访问到一个对象,我们就将这个对象标记为可到达状态,因此,A对象和B对象都被标记为可到达状态。
3.遍历所有的对象,没有被标记为可到达状态的对象就是垃圾,进行清除工作。
到此,一个完整的垃圾回收器就完成了,是不是很简单!?
里面还有一些细节,以后在继续写
1)如何确定一个对象的大小
2)如何找到所有的指针对象
3)如果找到所有的用户对象
4)如果确定一个指针是否在一个对象内部
5) 如何调用对象的正确的析构函数进行析构
6) 如何判断系统是否空闲以便进行垃圾回收
7) 垃圾回收线程与用户线程见的同步问题
8) 如何处理对象的继承与多态
9) 性能与内存开销