.NET中的垃圾回收(下)

垃圾回收性能的优化

l 弱引用( WeakReference )

l 代( Generations )

弱引用( WeakReference )

弱引用( WeakReference )是提高性能的一种方式,用于减少托管堆中大对象的压力。

当一个根指向一个对象时,它被称为这个对象的一个强引用并且这个对象不能被回收,因为应用程序能遍历到这个对象。

当一个对象有一个指向它的弱引用( WeakReference )时,基本上是指如果有内存请求并且 GC 启动时,这个对象可以被回收,当应用程序再次尝试去访问这个对象时,访问将会失败。另一方面,为了能访问一个被弱引用( WeakReference )的对象,应用程序必须获得一个对这个对象的强引用。如果应用程序在垃圾回收器回收这个对象之前获得了它的强引用, GC 将不能回收这个对象,因为有这个对象的强引用存在。

托管堆包含两个管理弱引用( WeakReference )的内部数据结构:短弱引用表和长弱引用表。

两种类型的弱引用:

l 短弱引用不追踪复苏。

也就是说,一个有短弱引用的对象会被立即收回,而不用等到运行 Finalize 方法。

l 长弱引用追踪复苏。

也就是说,只有当长弱引用表中的对象的存储空间可收回的时候 GC 才回收这个对象。如果对象有 Finalize 方法,是在 Finalize 方法被调用了之后并且对象不能复活了。

这两个表简单的存放着分配在托管堆中对象的指针。最初,两个表均为空。当你创建一个弱引用 (WeakReference) 对象时,对象不从托管堆中分配。而是在一个弱引用表中分配一个空的存储位置;短弱引用使用短弱引用表,长弱引用使用长弱引用表。

让我们看一个例子,看看 GC 运行时会发生些什么。下面的图(图 1 和图 2 )显示了 GC 运行前和运行后所有内部数据结构的状态。

** 图 1 ** : GC 运行前

** 图 2 ** : GC 运行后

以下是 GC 运行时进行的操作:

1. GC 为所有可遍历的对象建一张图。在上面的例子中,图包含对象 B,C,E,G 。

2. GC 扫描短弱引用表。如果表中指针指向的对象不在图中,那么这个指针标识的是一个不可遍历的对象,短弱引用表中的这个位置置为 null 。在上面的例子中,对象 D 的位置置为 null ,因为它不是图的一部分。

3. GC 扫描 Finalization 队列。如果队列中的指针所指的对象不在图中,那么这个指针标识一个不可遍历的对象,指针从 Finalization 队列中移到 FReachable 队列中。这时,对象被认为是可遍历的,所以加到图中。在上面的例子中,对象 A,D,F 是不包含在图中但看作是可遍历的对象,因为它们属于 Finalization 队列。进而 Finalization 队列被清空。

4. GC 扫描长弱引用表。如果表中的指针指的对象不在图中(现在图包括 FReachable 队列中指针所指的对象),那么指针标识一个不可遍历的对象,所在位置置为 null 。由于对象 C 和 F 都包含在图中,都不置 null 。

5. GC 整理 (Compact) 内存,挤出不可遍历对象留下的空隙。在上面的例子中,对象 H 是唯一从堆中删除的对象,它所分配的内存被收回。

代( Generations )

由于垃圾回收要在停止整个程序的情况下才能完成,它们可能会在程序执行期间进行任意长时间的中断。 GC 也有可能中断为满足实时系统的需求而要求及时响应的事件。

GC 中有一个特征叫代 (Generations) ,就是专门为提高性能而设计的。一个多代的 GC 是通过对观察用各种语言编写的大部分程序而得到两个事实进行仔细分析而得到的:

1. 新创建的对象拥有更短的生命周期。

2. 越老的对象,存活的越久。

多代的回收器通过对象的年龄把它分成若干组,并且年轻的对象比年老的对象回收的更频繁。初始化时,托管堆不含任何对象。所有新的对象都被添加到第 0 代堆中,直到堆装满了并触发垃圾回收。由于大部分对象存活的时间很短,只有一小部分年轻的对象在第一次回收时存活下来。一旦一个对象在第一次回收后存活下来后,它就被提升到第 1 代。在垃圾回收后可以说新的对象都在第 0 代堆中。只有当第 0 代的堆装满时垃圾回收才会再次被触发。所有第 0 代存活下来的对象被整理提升到第 1 代中。然后第 0 代不含任何对象了,但是所有新的对象都进入了第 0 代。

因此,作为当前代中“成熟”(存活于多代回收器)的对象,它们都会被移到下一级更老的代中。第 2 代是 CLR 的 GC 所支持的最大的代。以后回收时,第 2 代存活的对象将只是简单的停留在第 2 代。

因此,把堆划分成对象的代并且回收和整理更年轻的代中的对象提高了垃圾回收算法的效率,因为从堆中收回了大量的有意义的空间,同时比起回收器检查所有代中的所有对象要快得多。

一个能执行多代回收的 GC ,每次回收都要确保(至少要尽可能)所需时间小于某个最大时间,以帮助为实时环境能做一些配套的实时操作,同时也防止出现让用户明显感觉到的中断现象。

垃圾回收相关神话

** GC显然比手工内存管理要慢 **

** 对 ** ** 应解释 ** :不是一定的。现代垃圾回收器看起来运行时和手工存储分配( malloc/free 或 new/delete )一样快。在一些特殊的程序中,垃圾回收运行的可能不如为用户专门设计的自定义内存分配那么快。但从另一方面说,为了使手工内存管理正确的工作而添加的额外代吗(比如说,显示的引用计数)常常比 GC 所做的要昂贵的多。

** GC会使程序中断 **

** 对应解释: ** 由于垃圾回收器在查找和回收垃圾对象时通常停止整个应用程序,他们可能会导致中断时间过长而让用户觉察到。但是通过现在优化计数,这些可以感觉到的中断完全可以避免。

** 手工内存管理不会导致中断 **

** 对应解释: ** 手工内存管理并不能确保性能。它可能由于大量的分配或释放内存工作而导致中断。

** 使用 GC的程序很大并且臃肿;GC不适合小的程序或系统 **

** 对应解释: ** 尽管在复杂的系统中使用 GC 很有优势,也没有理由认为 GC 在其它尺寸的程序中会引入什么大的开销。

** 我已经听说了 GC会使用两次大量的内存 **

** 对应解释: ** 对于原来的 GC 这可能是个事实,但并不是垃圾回收器都是这样的。用于 GC 的数据结构比那些手工内存管理的要大的多。

Published At
Categories with Web编程
Tagged with
comments powered by Disqus