http://lucifer1982.wordpress.com.cn/2007/12/23/d%E8%AF%AD%E8%A8%80%E7%9A%84gc/
从目前掌握的资料来看,似乎只有D语言规范里稍稍提了一下D语言的内存模型,并没有深入描写.以下细节来自D 2.0语言运行时的实现代码,若以后运行时实现有所变更,请参考最新D运行时实现(所有代码均参考DMD的实现,GDC应该与其差别不大).
D主要有三种分配内存的途径.
* 静态数据,分配在默认数据段上.
* 堆栈数据,分配在线程堆栈上.
* 垃圾收集数据,动态分配在GC Heap上.
前面的两种与C/C++没什么区别.我们主要讨论第三种.
先来看看C/C++的内存管理模式.
1. 首先我们会为某个对象或类型分配内存.
2. 初始化上一步所得的内存.
3. 通过访问对象或类型成员来使用资源.
4. 销毁资源状态,执行资源清理工作.
5. 释放内存.
这种模式看起来相当的简单,但是却是导致很多问题的根源.想想有多少次我们忘记了释放无用的内存.想想有多少次调试的时候,内存问题是浪费我们时间的最大元凶.
GC正是为了解决该问题而产生的.它接管了上述步骤的第五步.对于一些本地资源(比如文件,数据库连接,套接字,同步对象,位图,图标等),通常在其对象的内存即将被回收时,必须执行一些资源清理工作.
下面来探讨一下D语言的GC内存分配和资源初始化.所有继承自Object的对象均在GC Heap上分配,此外还有动态数组.
D运行时内部有一个GC类.当应用程序的进程完成初始化后,GC也跟着初始化.GC内部会保留一块连续的地址空间,这段空间最初并不对应任何物理内存.该地址空间即为GC Heap.GC Heap上维护着一个指针,该指针表示下一个新建对象分配时在GC Heap中所处的位置.开始时,该指针被设置为保留地址空间的基地址.
当我们new一个对象时,D运行时会调用Object _d_newclass(ClassInfo ci)函数来创建一个新的对象.至于该函数实现细节,因为没有找到相关代码,所以无法具体描述.我们可以猜测其实现细节如下:
1. 计算类型所需要的字节数.此外,应该还有相关的一些额外开销所需要的字节数.这些额外开销应该包含有一个类型对象指针和一个同步索引块(该同步索引块会在后面详细解说).
2. D运行时检查保留地址空间是否满足分配新对象所需要的字节数.如果需要则commit物理内存.如果GC Heap中还有足够的剩余空间,那么对象将被分配在GC内部维护指针所指示的地方,并且所分配的地址空间中的字节被清零.接着,调用类型的实例构造器返回对象的内存地址(GC内部维护的指针会传递给this参数,而类型的静态构造函数会在main()函数前调用完毕).而在new返回对象的地址之前,GC 内部维护的指针会越过对象所处的内存区域,并指示出下一个新建对象在GC Heap中的地址.
这与C运行时库中的堆分配内存有着本质的区别.这样的内存分配方式使得分配对象的速度相当快,几乎可以与在线程堆栈中分配对象一样快!
接下来我们来看一下D语言中GC是如何工作的.
每个应用程序都有一组root.当GC开始执行时,它假设GC Heap中所有的对象都是垃圾.这样GC会查找所有的root(比如静态字段,方法参数,局部变量,CPU寄存器).如果发现root引用了一个对象,那么就mark它.接着GC继续查找,如果GC试图将一个先前已经标记过的对象再次mark时,它会停止该对象标记的路径方向上的遍历活动.这种行为有两个目的.首先,可以避免GC多次遍历;其次,如果对象之间出现了循环引用,可以避免陷入无限循环.
GC一旦检查完所有的root,便会回收那些未被mark的对象内存.接下来就到了压缩阶段.GC可能会压缩内存,也可能不会.这取决于找到的内存块容量.当GC找到比较大的连续内存块时,会把内存中的一些非垃圾对象搬移到这些连续内存块以压缩GC Heap.
显然,搬移内存中的对象将使所有所有包含这些对象指针的变量和CPU寄存器变得无效.因此,GC需要重新访问所有的root,并修改他们以使其指向这些对象的新内存位置.另外,如果对象包含有指向另一移动过的对象的字段,那么GC也会负责矫正这些字段.在GC Heap中的内存被压缩之后,GC内部维护的指针将被设置为指向最后一个非垃圾对象之后.
垃圾收集会给应用程序带来相当的性能开销,这也是使用GC时主要的负面影响.所以,GC会有一些特殊的设计来大幅提高GC的性能.D语言的GC是个基于代的垃圾收集器.引入代的唯一目的就是为了提高GC的性能.
前面讲到在创建新对象时,会创建一个同步索引块.它的目的是为了线程同步.当我们使用synchronized来创建线程安全的程序时,D运行时会把通过该同步索引块关联到一个同步锁.Windows下会关联到一个CRITICAL_SECTION,Linux则关联到一个 pthread_mutex_t.这个不能用于多进程同步.D语言的GC实现了多线程安全.
此外,D语言的析构函数不表示确定性析构,但是通过delete表达式却实现了确定性析构的效果.当一些昂贵的资源需要及时析构时,采取这种方式是有效的.大多数的资源交给GC来回收即可.
唯一担心的是这种方式的滥用,因为确定性析构很可能会造成应用程序性能的下降.这会加大GC的压力,使得GC频繁的进行垃圾回收.建议采用.NET的终结模式.