当前位置:编程学习 > asp >>

.Net 垃圾回收机制原理(二)

 

上一篇文章介绍了.Net 垃圾回收的基本原理和垃圾回收执行Finalize方法的内部机制;这一篇我们看下弱引用对象,代,多线程垃圾回收,大对象处理以及和垃圾回收相关的性能计数器。

让我们从弱引用对象说起,弱引用对象可以减轻大对象带来的内存压力。

弱引用(Weak References)

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

 

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

 

 

void Method() {

//创建对象的强引用

Object o = new Object(); 

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

WeakReference wr = new WeakReference(o);

 

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

 

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

if (o == null) {

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

} else {

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

}

}

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

 

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

 

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

 

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

 

 

WeakReference(object target);

WeakReference(object target, bool trackResurrection);

 

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。然而在托管堆上,连续分配的对象的内存地址是连续的。

前面的假设中还提到,新对象之间更可能存在相互引用关系。因此新对象分配到连续的内存

补充:Web开发 , ASP.Net ,
CopyRight © 2022 站长资源库 编程知识问答 zzzyk.com All Rights Reserved
部分文章来自网络,