您的位置:首页 > 其它

.Net GC 机制原理

2013-06-22 19:37 176 查看
有了Microsoft.Net clr中的垃圾回收机制程序员不需要再关注什么时候释放内存,释放内存这件事儿完全由GC做了,对程序员来说是透明的。尽管如此,作为一个.Net程序员 很有必要理解垃圾回收是如何工作的。这篇文章我们就来看下.Net是如何分配和管理托管内存的,之后再一步一步描述垃圾回收器工作的算法机制。

为程序设计一个适当的内存管理策略是困难的也是乏味的,这个工作还会影响你专注于解决程序本身要解决的问题。有没有一种内置的方法可以帮助开发人员解决内存管理的问题呢?当然有了,在.Net中就是GC,垃圾回收。

让我们想一下,每一个程序都要使用内存资源:例如屏幕显示,网络连接,数据库资源等等。实际上,在一个面向对象环境中,每一种类型都需要占用一点内存资源来存放他的数据,对象需要按照如下的步骤使用内存:
1. 为类型分配内存空间
2. 初始化内存,将内存设置为可用状态
3. 存取对象的成员
4. 销毁对象,使内存变成清空状态
5. 释放内存

这种貌似简单的内存使用模式导致过很多的程序问题,有时候程序员可能会忘记释放不再使用的对象,有时候又会试图访问已经释放的对象。这两种bug通 常都有一定的隐藏性,不容易发现,他们不像逻辑错误,发现了就可以修改掉。他们可能会在程序运行一段时间之后内存泄漏导致意外的崩溃。事实上,有很多工具 可以帮助开发人员检测内存问题,比如:任务管理器,System Monitor AcitvieX Control, 以及Rational的Purify。

而GC可以完全不需要开发人员去关注什么时候释放内存。然而,垃圾回收器并不是可以管理内存中的所有资源。有些资源垃圾回收器不知道该如何回收他 们,这部分资源就需要开发人员自己写代码实现回收。在.Net framework中,开发人员通常会把清理这类资源的代码写到Close、Dispose或者Finalize方法中,稍后我们会看下Finalize 方法,这个方法垃圾回收器会自动调用。

不过,有很多对象是不需要自己实现释放资源的代码的,比如:Rectangle,清空它只需要清空它的left,right,width,height字段就可以了,这垃圾回收器完全可以做。下面让我们来看下内存是如何分配给对象使用的。

对象分配:

.Net clr把所有的引用对象都分配到托管堆上。这一点很像c-runtime堆,不过你不需要关注什么时候释放对象,对象会在不用时自动释放。这样,就出现一个问题,垃圾回收器是怎么知道一个对象不再使用该回收了呢?我们稍后解释这个问题。

现在有几种垃圾回收算法,每一种算法都为一种特定的环境做了性能优化,这篇文章我们关注的是clr的垃圾回收算法。让我们从一个基础概念谈起。

当一个进程初始化之后,运行时会保留一段连续的空白内存空间,这块内存空间就是托管堆。托管堆会记录一个指针,我们叫它NextObjPtr,这个指针指向下一个对象的分配地址,最初的时候,这个指针指向托管堆的起始位置。

应用程序使用new操作符创建一个新对象,这个操作符首先要确认托管堆剩余空间能放得下这个对象,如果能放得下,就把NextObjPtr指针指向这个对象,然后调用对象的构造函数,new操作符返回对象的地址。



图1托管堆

这时候,NextObjPtr指向托管堆上下一个对象分配的位置,图1显示一个托管堆中有三个对象A、B和C。下一个对象会放在NextObjPtr指向的位置(紧挨着C对象)

现在让我们再看一下c-runtime堆如何分配内存。在c-runtime堆,分配内存需要遍历一个链表的数据结构,直到找到一个足够大的内存 块,这个内存块有可能会被拆分,拆分后链表中的指针要指向剩余内存空间,要确保链表的完好。对于托管堆,分配一个对象只是修改NextObjPtr指针的 指向,这个速度是非常快的。事实上,在托管堆上分配一个对象和在线程栈上分配内存的速度很接近。

到目前为止,托管堆上分配内存的速度似乎比在c-runtime堆上的更快,实现上也更简单一些。当然,托管堆获得这个优势是因为做了一个假设:地址空间是无限的。很显然这个假设是错误的。必须有一种机制保证这个假设成立。这个机制就是垃圾回收器。让我们看下它如何工作。

当应用程序调用new操作符创建对象时,有可能已经没有内存来存放这个对象了。托管堆可以检测到NextObjPtr指向的空间是否超过了堆的大小,如果超过了就说明托管堆满了,就需要做一次垃圾回收了。

在现实中,在0代堆满了之后就会触发一次垃圾回收。“代”是垃圾回收器提升性能的一种实现机制。“代”的意思是:新创建的对象是年轻一代,而在回收操作发生之前没有被回收掉的对象是较老的对象。将对象分成几代可以允许垃圾回收器只回收某一代的对象,而不是回收所有对象。

垃圾回收算法:

垃圾回收器检查看是否存在应用程序不再使用的对象。如果这样的对象存在,那么这些对象占用的空间就可以被回收(如果堆上没有足够的内存可用,那么 new操作符就会抛出OutofMemoryException)。你可能会问垃圾回收器是怎样判断一个对象是否还在用呢?这个问题不太容易得到答案。
每 个应用程序都有一组根对象,根是一些存储位置,他们可能指向托管堆上的某个地址,也可能是null。例如,所有的全局和静态对象指针是应用程序的根对象, 另外在线程栈上的局部变量/参数也是应用程序的根对象,还有CPU寄存器中的指向托管堆的对象也是根对象。存活的根对象列表由JIT(just-in- time)编译器和clr维护,垃圾回收器可以访问这些根对象的。

当垃圾回收器开始运行,它会假设托管堆上的所有对象都是垃圾。也就是说,假定没有根对象,也没有根对象引用的对象。然后垃圾回收器开始遍历根对象并构建一个由所有和根对象之间有引用关系对象构成的图。

图2显示,托管堆上应用程序的根对象是A,C,D和F,这几个对象就是图的一部分,然后对象D引用了对象H,那么对象H也被添加到图中;垃圾回收器会循环遍历所有可达对象。



图2 托管堆上的对象

垃圾回收器会挨个遍历根对象和引用对象。如果垃圾回收器发现一个对象已经在图中就会换一个路径继续遍历。这样做有两个目的:一是提高性能,二是避免无限循环。

所有的根对象都检查完之后,垃圾回收器的图中就有了应用程序中所有的可达对象。托管堆上所有不在这个图上的对象就是要做回收的垃圾对象了。构建好可 达对象图之后垃圾回收器开始线性的遍历托管堆,找到连续垃圾对象块(可以认为是空闲内存)。然后垃圾回收器将非垃圾对象移动到一起(使用c语言中的 memcpy函数),覆盖所有的内存碎片。当然,移动对象时要禁用所有对象的指针(因为他们都可能是错误的了)。因此垃圾回收器必须修改应用程序的根对象 使他们指向对象的新内存地址。此外,如果某个对象包含另一个对象的指针,垃圾回收器也要负责修改引用。图3显示了一次回收之后的托管堆。



图3 回收之后的托管堆

如图3所示在回收之后,所有的垃圾对象都被标识出来,而所有的非垃圾对象被移动到一起。所有的非垃圾对象的指针也被修改成移动后的内存地址,NextObjPtr指向最后一个非垃圾对象的后面。这时候new操作符就可以继续成功的创建对象了。

如你看到的,垃圾回收会有显著的性能损失,这是使用托管堆的一个明显的缺点。 不过,要记着内存回收操作旨在托管堆慢了之后才会执行。在满之前托管堆的性能比c-runtime堆的性能好要好。运行时垃圾回收器还会做一些性能优化,我们在下一篇文章中谈论这个。

下面的代码说明了对象是如何被创建管理的:

classApplication {

publicstaticintMain(String[] args) {

// ArrayList object created in heap, myArray is now a root

ArrayList myArray =newArrayList();

// Create 10000 objects in the heap

for(intx = 0; x < 10000; x++) {

myArray.Add(newObject()); // Object object created in heap

}

// Right now, myArray is a root (on the thread's stack). So,

// myArray is reachable and the 10000 objects it points to are also

// reachable.

Console.WriteLine(a.Length);

// After the last reference to myArray in the code, myArray is not

// a root.

// Note that the method doesn't have to return, the JIT compiler

// knows

// to make myArray not a root after the last reference to it in the

// code.

// Since myArray is not a root, all 10001 objects are not reachable

// and are considered garbage. However, the objects are not

// collected until a GC is performed.

}

}

也许你会问,GC这么好,为什么ANSI C++中没有它呢? 原因是垃圾回收器必须能找到应用程序的根对象列表,必须找到对象的指针。而在C++中对象的指针之间是可以相互转换的,没有办法知道指针指向的是一个什么 对象的指针。在CLR中,托管堆知道对象的实际类型。而元数据(metadata)信息可以用来判断对象引用了什么成员对象。

垃圾回收和Finalization

垃圾回收器提供了一个额外的功能,它可以在对象被标识为垃圾后自动调用其Finalize方法(前提是对象重写了object的Finalize方法)。

Finalize方法是object对象的一个虚方法,如果需要你可以重写这个方法,但是这个方法只能通过类似c++析构函数的方式重写。例如:

classFoo

{

~Foo(){

Console.WriteLine(“Foo Finalize”);

}

}

这里用过C++的程序员要特别注意,Finalize方法的写法和C++的析构函数完全一样,但是,.Net 中的Finalize方法和析构函数的却是不一样的,托管对象是不能被析构的,只能通过垃圾回收回收。

当你设计一个类时,最好避免重写Finalize方法,原因如下:
1. 实现Finalize的对象会被提升到更老的“代”,这会增加内存压力,使对象和此对象的关联对象不能在成为垃圾的第一时间回收掉。
2. 这些对象分配时间会更长
3. 让垃圾回收器执行Finalize方法会明显的损耗性能。请记住,每一个实现了Finalize方法的对象都需要执行Finalize方法,如果有一个长度为10000的数组对象,每个对象都需要执行Finalize方法
4. 重写Finalize方法的对象可能会 引用其他没有实现Finalize方法的对象,这些对象也会延迟回收
5. 你没有办法控制什么时候执行Finalize方法。如果要在Finalize方法中释放类似数据库连接之类的资源,就有可能导致数据库资源在时候后很久才得以释放
6. 当程序崩溃时,一些对象还被引用,他们的Finalize方法就没有机会执行了。这种情况会在后台线程使用对象,或者对象在程序退出时,或者 AppDomain卸载时。另外,默认情况下,当应用程序被强制结束时Finalize方法也不会执行。当然所有的操作系统资源会被回收;但是在托管堆上 的对象不会回收。你可以通过调用GC的RequestFinalizeOnShutdown方法改变这种行为。
7. 运行时不能控制多个对象Finalize方法执行的顺序。而有时候对象的销毁可能有顺序性

如果你定义的对象必须实现Finalize方法,那么要确保Finalize方法尽可能快的执行,要避免所有可能引起阻塞的操作,包括任何线程同步 操作。另外,要确保Finalize方法不会引起任何异常,如果有异常垃圾回收器会继续执行其他对象的Finalize方法直接忽略掉异常。

当编译器生成代码时会自动在构造函数上调用基类的构造函数。同样C++的编译器也会为析构函数自动添加基类析构函数的调用。但是,.Net中的 Finalize函数不是这样子,编译器不会对Finalize方法做特殊处理。如果你想在Finalize方法中调用父类的Finalize方法,必须 自己显示添加调用代码。

请注意在C#中Finalize方法的写法和c++中的析构函数一样,但是C#不支持析构函数,不要让这种写法欺骗你。

GC调用Finalize方法的内部实现

表面看,垃圾回收器嗲用Finalize方法很简单,你创建一个对象,当对象回收时调用它的Finalize方法。但是事实上要复杂一些。

当应用程序创建一个新对象时,new操作符在堆上分配内存。如果对象实现了Finalize方法。对象的指针会放到终结队列中。终结队列是由垃圾回收器控制的内部数据结构。在队列中每一个对象在回收时都需要调用它们的Finalize方法。

下图显示的堆上包含几个对象,其中一些对象是跟对象,一些对象不是。当对象C、E、F、I和J创建时,系统会检测这些对象实现了Finalize方法,并将它们的指针放到终结队列中。



Finalize方法要做的事情通常是回收垃圾回收器不能回收的资源,例如文件句柄,数据库连接等等。

当垃圾回收时,对象B、E、G、H、I和J被标记为垃圾。垃圾回收器扫描终结队列找到这些对象的指针。当发现对象指针时,指针会被移动到 Freachable队列。Freachable队列是另一个由垃圾回收器控制的内部数据结构。在Freachable队列中的每一个对象的 Finalize方法将执行。

垃圾回收之后,托管堆如图6所示。你可以看到对象B、G、H已经被回收了,因为这几个对象没有Finalize方法。然而对象E、I、J还没有被回收掉,因为他们的Finalize方法还没有执行。



图5 垃圾回收后的托管堆

程序运行时会有一个专门的线程负责调用Freachable队列中对象的Finalize方法。当Freachable队列为空时,这个线程会休 眠,当队列中有对象时,线程被唤醒,移除队列中的对象,并调用它们的Finalize方法。因此在执行Finalize方法时不要企图访问线程的 local storage。

终结队列(finalization queue)和Freachable队列之间的交互很巧妙。首先让我告诉你freachable的名字是怎么来的。F显然是finalization;在 此队列中的每一个对象都在等待执行他们的Finalize方法;reachable意思是这些对象来了。另一种说法,Freachable队列中的对象被 认为是跟对象,就像是全局变量或静态变量。因此,如果一个对象在freachable队列中,那么这个对象就不是垃圾。

简短点说,当一个对象是不可达的,垃圾回收器会认为这个对象是垃圾。那么,当垃圾回收器将对象从终结队列移动到Freachable队列中,这些对 象就不再是垃圾了,它们的内存也不会回收。从这一点上来讲,垃圾回收器已经完成标识垃圾,一些对象被标识成垃圾又被重新认为成非垃圾对象。垃圾回收器回收 压缩内存,清空freachable队列,执行队列中每一个对象的Finalize方法。



图6 再次执行垃圾回收后的托管堆

再次出发垃圾回收之后,实现Finalize方法的对象才被真正的回收。这些对象的Finalize方法已经执行过了,Freachable队列清空了。

垃圾回收让对象复活
在前面部分我们已经说了,当程序不使用某个对象时,这个对象会被回收。然而,如果对 象实现了Finalize方法,只有当对象的Finalize方法执行之后才会认为这个对象是可回收对象并真正回收其内存。换句话说,这类对象会先被标识 为垃圾,然后放到freachable队列中复活,然后执行Finalize之后才被回收。正是Finalize方法的调用,让这种对象有机会复活,我们 可以在Finalize方法中让某个对象强引用这个对象;那么垃圾回收器就认为这个对象不再是垃圾了,对象就复活了。

如下复活演示代码:

publicclassFoo {

~Foo(){

Application.ObjHolder =this;

}

}

classApplication{

staticpublicObject ObjHolder =null;

}

在这种情况下,当对象的Finalize方法执行之后,对象被Application的静态字段ObjHolder强引用,成为根对象。这个对象就 复活了,而这个对象引用的对象也就复活了,但是这些对象的Finalize方法可能已经执行过了,可能会有意想不到的错误发生。

事实上,当你设计自己的类型时,对象的终结和复活有可能完全不可控制。这不是一个好现象;处理这种情况的常用做法是在类中定义一个bool变量来表示对象是否执行过了Finalize方法,如果执行过Finalize方法,再执行其他方法时就抛出异常。

现在,如果有其他的代码片段又将Application.ObjHolder设置为null,这个对象变成不可达对象。最终垃圾回收器会把对象当成垃圾并回收对象内存。请注意这一次对象不会出现在finalization队列中,它的Finalize方法也不会再执行了。

复活只有有限的几种用处,你应该尽可能避免使用复活。尽管如此,当使用复活时,最好重新将对象添加到终结队列中,GC提供了静态方法ReRegisterForFinalize方法做这件事:
如下代码:

publicclassFoo{

~Foo(){

Application.ObjHolder =this;

GC.ReRegisterForFinalize(this);

}

}

当对象复活时,重新将对象添加到复活队列中。需要注意的时如果一个对象已经在终结队列中,然后又调用了GC.ReRegisterForFinalize(obj)方法会导致此对象的Finalize方法重复执行。

弱引用(Weak References)

当程序的根对象指向一个对象时,这个对象是可达的,垃圾回收器不能回收它,这称为对对象的强引用。和强引用相对的是弱引用,当一个对象上存在弱引用时,垃圾回收器可以回收此对象,但是也允许程序访问这个对象。这是怎么回事儿呢?请往下看。

如果一个对象上仅存在弱引用,并且垃圾回收器在运行,这个对象就会被回收,之后如果程序中要访问这个对象,访问就会失败。另一方面,要使用弱引用的 对象,程序必须先对这个对象进行强引用,如果程序在垃圾回收器回收这个对象之前对对象进行了强引用,这样(有了强引用之后)垃圾回收器就不能回收此对象 了。这有点绕,让我们用一段代码来说明一下:

voidMethod() {

//创建对象的强引用

Object o =newObject();

// 用一个短弱引用对象弱引用o.

WeakReference wr =newWeakReference(o);

o =null;// 移除对象的强引用

o = wr.Target;//尝试从弱引用对象中获得对象的强引用

if(o ==null) {

// 如果对象为空说明对象已经被垃圾回收器回收掉了

}else{

// 如果垃圾回收器还没有回收此对象就可以继续使用对象了

}

}

为什么需要弱对象呢?因为,有一些数据创建起来很容易,但是却需要很多内存。例如:你有一个程序,这个程序需要访问用户硬盘上的所有文件夹和文件 名;你可以在程序第一次需要这个数据时访问用户磁盘生成一次数据,数据生成之后你就可以访问内存中的数据来得到用户文件数据,而不是每次都去读磁盘获得数 据,这样做可以提升程序的性能。

问题是这个数据可能相当大,需要相当大的内存。如果用户去操作程序的另外一部分功能了,这块相当大的内存就没有占用的必要了。你可以通过代码删除这 些数据,但是如果用户马上切换到需要这块数据的功能上,你就必须重新从用户的磁盘上构建这个数据。弱引用为这种场景提供了一种简单有效的方案。

当用户切换到其他功能时,你可以为这个数据创建一个弱引用对象,并把对这个数据的强引用解除掉。这样如果程序占用的内存很低,垃圾回收操作就不会触 发,弱引用对象就不会被回收掉;这样当程序需要使用这块数据时就可以通过一个强引用来获得数据,如果成功得到了对象引用,程序就没有必要再次读取用户的磁 盘了。

WeakReference类型提供了两个构造函数:

WeakReference(objecttarget);

WeakReference(objecttarget,booltrackResurrection);

target参数显然就是弱引用要跟踪的对象了。trackResurrection参数表示当对象的Finalize方法执行之后是否还要跟踪这个对象。默认这个参数是false。有关对象的复活请参考这里

方便起见,不跟踪复活对象的弱引用称为“短弱引用”;而要跟踪复活对象的的弱引用称为“长弱引用”。如果对象没有实现Finalize方法,那么长 弱引用和短弱引用是完全一样的。强烈建议你尽量避免使用长弱引用。长弱引用允许你使用复活的对象,而复活对象的行为可能是不可以预知的。

一旦你使用WeakReference引用了一个对象,建议你将这个对象的所有强用都设置为null;如果强引用存在的话,垃圾回收器是永远都不可能回收弱引用指向的对象的。

当你要使用弱引用目标对象时,你必须为目标对象创建一个强引用,这很简单,只要用object a = weekRefer.Target;就可以了,然后你必须判断a是否为空,弱不为空才可以继续使用,弱为空就表示对象已经被垃圾回收器回收了,得通过其他 方法重新获得此对象。

弱引用的内部实现

从前文中的描述中我们可以推断出弱引用对象肯定和一般对象的处理是不一样的。一般情况下如果一个对象引用了另一个对象就是强引用,垃圾回收器就不能回收被引用的对象,而WeakReference对象却不是这样子,它引用的对象是有可能被回收的。

要完全理解弱对象是如何工作的,我们还需要看一下托管堆。托管堆上有两个内部数据结构他们的唯一作用是管理弱引用:我们可以把它们称作长弱引用表和短弱引用表;这两个表存放托管堆上的弱引用目标对象指针。

程序运行之初,这两个表都是空的。当你创建一个WeakReference对象时,这个对象并不是分配到托管堆上的,而是在弱对象表中创建一个空槽(Empty Slot)。短弱引用对象被放在短弱对象表中,长弱引用对象被放在长弱引用表中。

一旦发现空槽,空槽的值会被设置成弱引用目标对象的地址;显然长短弱对象表中的对象是不会当作应用程序的根对象的。垃圾回收器不会回收长短弱对象表中的数据。

让我们来看下垃圾回收执行时发生了什么:
1. 垃圾回收器构建一个可达对象图,构建步骤请参考上文
2. 垃圾回收器扫描短弱对象表,如果弱对象表中指向的对像没有在可达对象图中,那么这个对像就被标识为垃圾对象,然后短对象表中的对象指针被设置为空
3. 垃圾回收器扫描终结队列(参考上文),如果队列中的对象不在可达对象图中,这个对象从终结队列中移动到Freachable队列中,这时候,这个对象又被标识为可达对象,不再是垃圾了
4. 垃圾回收器扫描长弱引用表。如果表中的对象不在可达对象图中(可达对象图中包括在Freachable队列中对象),将长引用对象表中对应的对象指针设置为null
5. 垃圾回收器移动可达对象

一旦你理解了垃圾回收器的工作过程,就很容易理解弱引用是如何起作用了。访问WeakReference的Target属性导致系统返回弱对象表中的目标对象指针,如果是null,表示对象已经被回收了。

短弱引用不跟踪复活,这意味着垃圾回收器可以在扫描终结队列之前检查弱引用表中指向的对象是否是垃圾对象。

而长弱引用跟踪复活对象,这意味着垃圾回收器必须在确认对象回收之后才可以将弱引用表中的指针设置为null。

代:

提起.Net的垃圾回收,c++或者c程序员可能就会想,这么管理内存会不会出现性能问题呢。GC的开发人员一直在调整垃圾回收器提升它的性能。代就是一种为了降低垃圾回收对性能影响的机制。垃圾回收器在工作时会假定如下说法是成立的:

1. 一个对象越新,那么这个对象的生命周期就越短
2. 一个对象越老,那么这个对象的生命周期就越长
3. 新对象之间通常更可能和新对象之间存在引用关系
4. 压缩堆的一部分要比压缩整个堆要快

当然大量研究证明以上几个假设在很多程序上是成立的。那就让我们来谈谈这几个假设是如何影响垃圾回收器工作的吧。

在程序初始化时,托管堆上没有对象。这时候新添到托管堆上的对象是的代是0.如下图所示,0代对象是最年轻的对象,他们从来没有经过垃圾回收器的检查。



图1 托管堆上的0代对象

现在如果堆上添加了更多的对象,堆填满时就会触发垃圾回收。当垃圾回收器分析托管堆时,会构建一个垃圾对象(图2中浅紫色块)和非垃圾对象的图。所有没有被回收的对象会被移动压缩到堆的最底端。这些没有被回收掉的对象就成为了1代对象,如图2所示



图2 托管堆上的0代1代对象

当堆上分配了更多的对象时,新对象被放在了0代区。如果0代堆填满了,就会触发一次垃圾回收。这时候活下来的对象成为1代对象被移动到堆的底部;再此发生垃圾回收后1代对象中存活下来的对象会提升为2代对象并被移动压缩。如图3所示:



图3 托管堆上的0、1、2代对象
2代对象是目前垃圾回收器的最高代,当再次垃圾回收时,没有回收的对象的代数依然保持2.

垃圾回收分代为什么可以优化性能

如前所述,分代回收可以提高性能。当堆填满之后会触发垃圾回收,垃圾回收器可以只选择0代上的对象进行回收,而忽略更高代堆上的对象。然而,由于越年轻的对象生命周期越短,因此,回收0代堆可以回收相当多的内存,而且回收所耗的性能也比回收所有代对象要少得多。

这是分代垃圾回收的最简单优化。分代回收不需要便利整个托管堆,如果一个根对象引用了一个高代对象,那么垃圾回收器可以忽略高代对象和其引用对象的遍历,这会大大减少构建可达对象图的时间。

如果回收0代对象没有释放出足够的内存,垃圾回收器会尝试回收1代和0代堆;如果仍然没有获得足够的内存,那么垃圾回收器会尝试回收2,1,0代堆。具体会回收那一代对象的算法不是确定的,微软会持续做算法优化。

多数堆(像c-runtime堆)只要找到足够的空闲内存就分配给对象。因此,如果我连续分配多个对象时,这些对象的地址空间可能会相差几M。然而在托管堆上,连续分配的对象的内存地址是连续的。

前面的假设中还提到,新对象之间更可能存在相互引用关系。因此新对象分配到连续的内存上,你可以获得就近引用的性能优化(you gain performance from locality of reference)。这样的话很可能你的对象都在CPU的缓存中,这样CPU的很多操作就不需要去存取内存了。

微软的性能测试显示托管堆的分配速度比标准的win32 HeapAlloc方法还要快。这些测试也显示了200MHz的Pentium的CPU做一次0代回收时间可以小于1毫秒。微软的优化目的是让垃圾回收耗用的时间小于一次普通的页面错误。

使用System.GC类控制垃圾回收

类型System.GC运行开发人员直接控制垃圾回收器。你可以通过GC.MaxGeneration属性获得GC的最高代数,目前最高代是定值2.

你可以调用GC.Collect()方法强制垃圾回收器做垃圾回收,Collect方法有两个重载:

voidGC.Collect(Int32 generation)

voidGC.Collect()

第一个方法允许你指定要回收那一代。你可以传0到GC.MaxGeneration的数字做参数,传0只做0代堆的回收,传1会回收1代和0代堆,而传2会回收整个托管堆。而无参数的方法调用GC.Collect(GC.MaxGeneration)相当于整个回收。

在通常情况下,不应该去调用GC.Collect方法;最好让垃圾回收器按照自己的算法判断什么时候该调用Collect方法。尽管如此,如果你确 信比运行时更了解什么时候该做垃圾回收,你就可以调用Collect方法去做回收。比如说程序可以在保存数据文件之后做一次垃圾回收。比如你的程序刚刚用 完一个长度为10000的大数组,你不再需要他了,就可以把它设置为null然后执行垃圾回收,缓解内存的压力。

GC还提供了WaitForPendingFinalizers方法。这个方法简单的挂起执行线程,知道Freachable队列中的清空之后,执行完所有队列中的Finalize方法之后才继续执行。

GC还提供了两个方法用来返回某个对象是几代对象,他们是

Int32 GC.GetGeneration(objecto);

Int32 GC.GetGeneration(WeakReference wr)

第一个方法返回普通对象是几代,第二个方法返回弱引用对象的代数。

下面的代码可以帮助你理解代的意义:

privatestaticvoidGenerationDemo() {

// Let's see how many generations the GCH supports (we know it's 2)

Display("Maximum GC generations: "+ GC.MaxGeneration);

// Create a new BaseObj in the heap

GenObj obj =newGenObj("Generation");

// Since this object is newly created, it should be in generation 0

obj.DisplayGeneration();// Displays 0

// Performing a garbage collection promotes the object's generation

GC.Collect();

obj.DisplayGeneration();// Displays 1

GC.Collect();

obj.DisplayGeneration();// Displays 2

GC.Collect();

obj.DisplayGeneration();// Displays 2 (max generation)

obj =null;// Destroy the strong reference to this object

GC.Collect(0);// Collect objects in generation 0

GC.WaitForPendingFinalizers();// We should see nothing

GC.Collect(1);// Collect objects in generation 1

GC.WaitForPendingFinalizers();// We should see nothing

GC.Collect(2);// Same as Collect()

GC.WaitForPendingFinalizers();// Now, we should see the Finalize

// method run

Display(-1,"Demo stop: Understanding Generations.", 0);

}

classGenObj{

publicvoidDisplayGeneration(){

Console.WriteLine(“my generationis” + GC.GetGeneration(this));

}

~GenObj(){

Console.WriteLine(“My Finalize method called”);

}

}

垃圾回收机制的多线程性能优化

在前面的部分,我解释了GC的算法和优化,然后讨论的前提都是在单线程情况下的。而在真实的程序中,很可能是多个线程一起工作,多个线程一起操纵托 管堆上的对象。当一个线程触发了垃圾回收,其他所有的线程都应该暂停访问任何引用对象(包括他们自己栈上引用的对象),因为垃圾回收器有可能要移动对象, 修改对象的内存地址。

因此当垃圾回收器开始回收时,所有执行托管代码的线程必须挂起。运行时有几种不同的机制可以安全的挂起线程来执行垃圾回收。这一块的内部机制我不打算详细说明。但是微软会持续修改垃圾回收的机制来降低垃圾回收带来的性能损耗。

下面几段描述了垃圾回收器在多线程情况下是如何工作的:
完全中断代码执行 当垃圾回收开始执行时,挂起所有应用程序线程。垃圾回收器随后将线程挂起的位置记录到一个just-in-time(JIT)编译器生成的表中,垃圾回收 器负责将线程挂起的位置记录在表中,记录当前正在访问的对象,以及对象存放的位置(变量中,CPU寄存器中,等等)
劫持:垃圾回收器可以修改线程的栈让返回地址指向一个特殊的方法,当当前执行的方法返回时,这个特殊的方法将会执行,挂起线程,这种改变线程执行路径的方式称为劫持线程。当垃圾回收完成之后,线程会重新返回到之前执行的方法上。

安全点: 当JIT编译器编译一个方法时,可以在某个点插入一段代码判断GC是否挂起,如果是,线程就挂起等待垃圾回收完成,然后线程重新开始执行。JIT编译器插入检查GC代码的位置被称作“安全点”

请注意,线程劫持允许正在执行非托管代码的线程在垃圾回收过程中执行。如果非托管代码不访问托管堆上的对象时这是没有问题的。如果这个线程当前执行非托管代码然后返回执行托管代码,这个线程将会被劫持,直到垃圾回收完成之后再继续执行。

除了我刚提到的集中机制之外,垃圾回收器还有其他改进来增强多线程程序中的对象内存分配和回收。

同步释放分配(Synchronization-free Allocations):在一个多线程系统中,0代堆被分成几个区域,一个线程使用一个区域。这允许多线程同时分配对象,并不需要一个线程独占堆。

可伸缩回收(Scalable Collections):在多线程系统中运行执行引擎的服务器版本(MXSorSvr.dll).托管堆会被分成几个不同的区域,一个CPU一个区域。 当回收初始化时,每个CPU执行一个回收线程,各个线程回收各自的区域。而工作站版本的执行引擎(MXCorWks.dll)不支持这个功能。

大对象回收

监视垃圾回收

如果你安装了.Net framework你的性能计数器(开始菜单—管理工具—性能 进入)中就会有.Net CLR Memory一项,你可以从实例列表中选择某个程序进行观察,如下图所示。



这些性能指标的具体含义如下:

性能计数器
说明
# Bytes in all Heaps(所有堆中的字节数)
显示以下计数器值的总和:“第 0 级堆大小”计数器、“第 1 级堆大小”计数器、“第 2 级堆大小”计数器和“大对象堆大小”计数器。此计数器指示在垃圾回收堆上分配的当前内存(以字节为单位)。
# GC HandlesGC 处理数目)
显示正在使用的垃圾回收处理的当前数目。垃圾回收处理是对公共语言运行库和托管环境外部的资源的处理。
# Gen 0 Collections(第 2 级回收次数)
显示自应用程序启动后第 0 级对象(即最年轻、最近分配的对象)被垃圾回收的次数。
当第 0 级中的可用内存不足以满足分配请求时发生第 0 级垃圾回收。此计数器在第 0 级垃圾回收结束时递增。较高级的垃圾回收包括所有较低级的垃圾回收。当较高级(第 1 级或第 2 级)垃圾回收发生时此计数器被显式递增。
此计数器显示最近的观察所得值。_Global_ 计数器值不准确,应该忽略。
# Gen 1 Collections(第 2 级回收次数)
显示自应用程序启动后对第 1 级对象进行垃圾回收的次数。
此计数器在第 1 级垃圾回收结束时递增。较高级的垃圾回收包括所有较低级的垃圾回收。当较高级(第 2 级)垃圾回收发生时此计数器被显式递增。
此计数器显示最近的观察所得值。_Global_ 计数器值不准确,应该忽略。
# Gen 2 Collections(第 2 级回收次数)
显示自应用程序启动后对第 2 级对象进行垃圾回收的次数。此计数器在第 2 级垃圾回收(也称作完整垃圾回收)结束时递增。
此计数器显示最近的观察所得值。_Global_ 计数器值不准确,应该忽略。
# Induced GC(引发的 GC 的数目)
显示由于对 GC.Collect 的显式调用而执行的垃圾回收的峰值次数。让垃圾回收器对其回收的频率进行微调是切实可行的。
# of Pinned Objects(钉住的对象的数目)
显示上次垃圾回收中遇到的钉住的对象的数目。钉住的对象是垃圾回收器不能移入内存的对象。此计数器只跟踪被进行垃圾回收的堆中的钉住的对象。例如,第 0 级垃圾回收导致仅枚举第 0 级堆中钉住的对象。
# of Sink Blocks in use(正在使用的接收块的数目)
显示正在使用的同步块的当前数目。同步块是为存储同步信息分配的基于对象的数据结构。同步块保留对托管对象的弱引用并且 必须由垃圾回收器扫描。同步块不局限于只存储同步信息;它们还可以存储 COM interop 元数据。该计数器指示与同步基元的过度使用有关的性能问题。
# Total committed Bytes(提交字节的总数)
显示垃圾回收器当前提交的虚拟内存量(以字节为单位)。提交的内存是在磁盘页面文件中保留的空间的物理内存。
# Total reserved Bytes(保留字节的总数)
显示垃圾回收器当前保留的虚拟内存量(以字节为单位)。保留内存是为应用程序保留(但尚未使用任何磁盘或主内存页)的虚拟内存空间。
% Time in GCGC 中时间的百分比)
显示自上次垃圾回收周期后执行垃圾回收所用运行时间的百分比。此计数器通常指示垃圾回收器代表该应用程序为收集和压缩内存而执行的工作。只在每次垃圾回收结束时更新此计数器。此计数器不是一个平均值;它的值反映了最近观察所得值。
Allocated Bytes/second(每秒分配的字节数)
显示每秒在垃圾回收堆上分配的字节数。此计数器在每次垃圾回收结束时(而不是在每次分配时)进行更新。此计数器不是一段时间内的平均值;它显示最近两个样本中观测的值的差除以取样间隔时间所得的结果。
Finalization Survivors(完成时存留对象数目)
显示因正等待完成而从回收后保留下来的进行垃圾回收的对象的数目。如果这些对象保留对其他对象的引用,则那些对象也保留下来,但此计数器不对它们计数。“从第 0 级提升的完成内存”和“从第 1 级提升的完成内存”计数器表示因完成而保留下来的所有内存。
此计数器不是累积计数器;它在每次垃圾回收结束时由仅在该特定回收期间存留对象的计数更新。此计数器指示由于完成应用程序可能导致系统开销过高。
Gen 0 heap size(第 2 级堆大小)
显示在第 0 级中可以分配的最大字节数;它不指示在第 0 级中当前分配的字节数。
当自最近回收后的分配超出此大小时发生第 0 级垃圾回收。第 0 级大小由垃圾回收器进行微调并且可在应用程序执行期间更改。在第 0 级回收结束时,第 0 级堆的大小是 0 字节。此计数器显示调用下一个第 0 级垃圾回收的分配的大小(以字节为单位)。
此计数器在垃圾回收结束时(而不是在每次分配时)进行更新。
Gen 0 Promoted Bytes/Sec(从第 1 级提升的字节数/秒)
显示每秒从第 0 级提升到第 1 级的字节数。内存在从垃圾回收保留下来后被提升。此计数器是每秒创建的在相当长时间保留下来的对象的指示符。
此计数器显示在最后两个样本(以取样间隔持续时间来划分)中观察到的值之间的差异。
Gen 1 heap size(第 2 级堆大小)
显示第 1 级中的当前字节数;此计数器不显示第 1 级的最大大小。不直接在此代中分配对象;这些对象是从前面的第 0 级垃圾回收提升的。此计数器在垃圾回收结束时(而不是在每次分配时)进行更新。
Gen 1 Promoted Bytes/Sec(从第 1 级提升的字节数/秒)
显示每秒从第 1 级提升到第 2 级的字节数。在此计数器中不包括只因正等待完成而被提升的对象。
内存在从垃圾回收保留下来后被提升。不会从第 2 级进行任何提升,因为它是最旧的一级。此计数器是每秒创建的非常长时间保留下来的对象的指示符。
此计数器显示在最后两个样本(以取样间隔持续时间来划分)中观察到的值之间的差异。
Gen 2 heap size(第 2 级堆大小)
显示第 2 级中当前字节数。不直接在此代中分配对象;这些对象是在以前的第 1 级垃圾回收期间从第 1 级提升的。此计数器在垃圾回收结束时(而不是在每次分配时)进行更新。
Large Object Heap size(大对象堆大小)
显示大对象堆的当前大小(以字节为单位)。垃圾回收器将大于 20 KB 的对象视作大对象并且直接在特殊堆中分配大对象;它们不是通过这些级别提升的。此计数器在垃圾回收结束时(而不是在每次分配时)进行更新。
Promoted Finalization-Memory from Gen 0(从第 1 级提升的完成内存)
显示只因等待完成而从第 0 级提升到第 1 级的内存的字节数。此计数器不是累积计数器;它显示在最后一次垃圾回收结束时观察到的值。
Promoted Finalization-Memory from Gen 1(从第 1 级提升的完成内存)
显示只因等待完成而从第 1 级提升到第 2 级的内存的字节数。此计数器不是累积计数器;它显示在最后一次垃圾回收结束时观察到的值。如果最后一次垃圾回收就是第 0 级回收,此计数器则重置为 0。
Promoted Memory from Gen 0(从第 1 级提升的内存)
显示在垃圾回收后保留下来并且从第 0 级提升到第 1 级的内存的字节数。此计数器中不包括那些只因等待完成而提升的对象。此计数器不是累积计数器;它显示在最后一次垃圾回收结束时观察到的值。
Promoted Memory from Gen 1(从第 1 级提升的内存)
显示在垃圾回收后保留下来并且从第 1 级提升到第 2 级的内存的字节数。此计数器中不包括那些只因等待完成而提升的对象。此计数器不是累积计数器;它显示在最后一次垃圾回收结束时观察到的值。如果最后一次垃圾回收就是第 0 级回收,此计数器则重置为 0。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: