思来想去,决定总结一下垃圾回收机制。引用计数与我结缘最早,也比较简单、基础,遂决定从引用计数入手。—— 不管人非笑,不管人毁谤,不管人荣辱,任他功夫有进有退,我只是这致良知的主宰不息,久久自然有得力处
Reference Counting
对象在创建时保存一个自身被引用的计数,初始值为1。每次被新的变量引用,该值加1。相反,则减去1。当该值等于0时,占用空间被系统回收。
什么是对象呢?
var neojos int64 = 32var ptrNeojos *int64 = &name
如上所示,我们创建了一个int64
类型的object
,命名为neojos
。程序中对该object
的操作都是通过使用neojos
来实现的。而ptrNeojos
其实又创建了一个*int64
类型的object
,但它的值保存的是neojos
的地址。
对于ptrNeojos
来说,它的生命周期跟普通变量的生命周期没有区别。唯一区别的是,当它生命周期结束后,ptrNeojos
会被垃圾回收,而底层指向的object
却不会。
如何计数呢?
Object * obj1 = new Object(); // RefCount(obj1) starts at 1Object * obj2 = obj1; // RefCount(obj1) incremented to 2 as new reference is addedObject * obj3 = new Object(); obj2->SomeMethod();obj2 = NULL; // RefCount(obj1) decremented to 1 as ref goes awayobj1 = obj3; // RefCount(obj1) decremented to 0 and can be collected
obj1
指向了一个匿名对象,为了方便,我们叫anonymousObj
。上述代码展示了anonymousObj
从创建到被垃圾回收的整个过程。垃圾回收对象的内存空间,上述过程中obj1
对象的地址不会发生改变,只是底层引用的对象发生了变化。
下面的例子,用于测试ptrName
代表的对象在赋值过程中不会发生变化。
func TestCase2(t *testing.T) { var name int64 = 32 var ptrName *int64 = &name t.Log(&ptrName) //0xc42000e078 ptrName = nil t.Log(&ptrName) //0xc42000e078}
如何实现
基于不同的语言会有不同的实现方式,但思路是相通的。
存储结构和申请空间
创建对象的时候,申请额外的空间用于存储引用计数,同时对外隐藏该空间的存在。如下图,Header
部分就用于存储引用计数。所以,程序返回的指针实际是ActualData
的首地址,调用者完全意识不到header
的存在,而GC
执行的时候却可以通过对象的地址访问Header
。
如下代码,申请地址时,将引用计数初始化为1。同时,返回ActualData
的指针地址。后续的引用计数更新,释放对象空间都通过判断Header
来处理。
//Header结构struct MemHeader{ UINT32 refCount;};// cb is the number of bytes to be allocatedPVOID GC_Alloc(size_t cb){ // allocate MemHeader + cb but cast it to MemHeader MemHeader* pHdr = (MemHeader*)PlatformAlloc(MEMHEADERSIZE + cb); if (pHdr == NULL) return NULL; // set the initial refCount pHdr->refCount = 1; // increment the pointer by the size of MemHeader // and make it point to the start of the actual data ++pHdr; return (PVOID)pHdr;}//访问Header头inline MemHeader * GetHeader(PVOID pMem){ return ((MemHeader*)pMem) - 1;}
基类实现
对象可以意识到引用计数机制的存在,明确的增加或减少引用计数。这种情况适用于:调用者手动释放空间的场合。那么,所有对象需要继承一个通用的基类,来实现这部分计数逻辑。
class ReferenceCount{ int count; ReferenceCount() { count = 1; //start at 1 as creation implies at least once reference is being made } void increment() { count++; } void decrement() { count--; if( count == 0 ) delete this; }}; //any reference counted object simply derives from the above typeclass MyType : public ReferenceCount { ... }
对象引用关系
对象与对象之间存在相互调用,当其中一个对象的引用计数减为0时,该对象“引用链”上其他对象的引用计数都需要被更新。GC
如何执行清理的呢?
当object1
释放object3
的引用时,object3
和object5
的引用计数都需要被更新,而这是一个递归检查、更新的过程。
VOID GC_ReleaseRef(PVOID pMem){ if (pMem == NULL) return; MemHeader *pHdr = GetHeader(pMem); --(pHdr->refCount); if (pHdr->refCount == 0) { foreach(PVOID pChild in Get_Child(pHdr)) GC_ReleaseRef(pChild); PlatformFree(pHdr); }}
GC扫描
除了自动回收垃圾外,GC
的扫描是从哪里开始的?拿Java
来解释,GC roots
就是Java
中的ClassLoader
。
ClassLoader
After that when we try to use a Class, Java ClassLoader loads that class into memory
ClassLoader
按需将使用到的class
加载到内存,熟悉PHP
的可以跟Laravel Container
做类比。
GC roots
In Java, there are special objects called Garbage Collection Roots (GC roots). They serve as a root objects for Garbage Collection marking mechanism (see picture).
参考文章: