一、简单介绍
这是一个自己写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);
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;
< !--[if !vml]--><!--[endif]-->
}
以下是一些细节,有兴趣的网友能够接着看。
二、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,达到多个引用共享对象的目的。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继承。相同,对于整型,浮点型这些基本数据类型,不从不论什么类继承,更无法要求其从我们上面说的类继承。为了能把引用计数应用的更为广泛的,我们要换另外一个方法。
我们要实现的目的是把一个引用计数器和一个对象联系在一起。类的继承通过在编写代码时指定的继承关系,分配一个子类对象时内部自然包括父类的数据成员,形成一片连续的内存空间。这就像是用数组存储对象,一个挨着一个。除了数组,还有一种经常使用的线性数据结构是链表,对象之间通过指针相连。相同的原理,我们也能够把引用计数和被计数的对象通过指针连接在一起。
<!--[if !vml]--><!--[endif]-->
这样完毕后,客户代码引用对象时就要引用带有引用计数器的新对象。就像是在原来的对象外面又包裹了一层。因此,我们把这个提供引用计数的类命名为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方法进行垃圾回收
我们前面已经说过了,仅仅使用引用计数进行垃圾回收在有循环引用的情况下就会失效。因此我们须要更有效、更主动的回收算法。在进行更为主动的垃圾回收之前,须要解决的最主要的一个问题就是:什么样的对象是垃圾对象,也就是说,怎样将垃圾对象与非垃圾对象区分开来。
解决问题须要能做到以下两点:
<!--[if !supportLists]-->1.
<!--[endif]-->可以界定对象的大小和起始位置。
<!--[if !supportLists]-->2.
<!--[endif]-->可以分辨对象的当前使用状态,找出垃圾对象。
对于第一个问题,之所以会出现这个问题,是由于在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对象销毁时这个指针也会被销毁,不是根指针。依照以下的规则,我们開始訪问全部的对象:
<!--[if !supportLists]-->1)
<!--[endif]-->将全部的指针对象分成根指针和非根指针。可是我们并不知道根指针在什么地方创建,幸好,我们知道非根指针总是位于其它对象内部。我们依次检查pA,pB,
发现pA位于B对象内部,pB不位于不论什么对象内部,因此pB是根指针,pA不是。
这个工作不须要每次进行垃圾扫描时都反复进行,指针对象一旦生成,它的地位是不会变的,不可能从根指针变成非根指针,也不可能相反。仅仅是在两次垃圾扫描的间隙时间,会有新的指针生成,须要检查新生成的指针,确定新生成的指针的类型。
<!--[if !supportLists]-->2)
<!--[endif]-->对于每个根指针,訪问它仅仅向的对象,并对该对象内部的全部指针,反复这个过程。从pB,我们訪问到了B对象,然后在B对象内部,我们发现了pA指针,然后反复,从pA指针訪问到A对象,在A对象内部,没有发现不论什么指针。
每次訪问到一个对象,我们就将这个对象标记为可到达状态,因此,A对象和B对象都被标记为可到达状态。
<!--[if !supportLists]-->3)
<!--[endif]-->遍历全部的对象,没有被标记为可到达状态的对象就是垃圾,进行清除工作。
<!--[if !vml]--><!--[endif]-->
到此,一个完整的垃圾回收器就完毕了,是不是非常easy!?
里面另一些细节,以后在继续写
<!--[if
!supportLists]-->1)
<!--[endif]-->怎样确定一个对象的大小
<!--[if
!supportLists]-->2)
<!--[endif]-->怎样找到全部的指针对象
<!--[if
!supportLists]-->3)
<!--[endif]-->假设找到全部的用户对象
<!--[if
!supportLists]-->4)
<!--[endif]-->假设确定一个指针是否在一个对象内部
<!--[if
!supportLists]-->5)
<!--[endif]-->怎样调用对象的正确的析构函数进行析构
<!--[if
!supportLists]-->6)
<!--[endif]-->怎样推断系统是否空暇以便进行垃圾回收
<!--[if
!supportLists]-->7)
<!--[endif]-->垃圾回收线程与用户线程见的同步问题
<!--[if
!supportLists]-->8)
<!--[endif]-->怎样处理对象的继承与多态
<!--[if
!supportLists]-->9)
<!--[endif]-->性能与内存开销