Garbage Collection Part 2: Automatic Memory Management in the Microsoft .NET Framework
垃圾回收:在微软NET框架自动内存管理 (二)
原文地址:http://msdn.microsoft.com/en-us/magazine/bb985011.aspx
概要:
本文的第一部分解释了垃圾收集算法的工作原理,当垃圾收集器决定清除资源的内存时如何正确地清理,以及当对象被释放时如何强制清理对象。 该系列我们将解释了强,弱引用如何帮助大对象的内存管理,以及如何使用代来改善性能。 此外,应用程序使用的方法和属性,用于控制垃圾收集,监控收集资源的性能,还包括多线程的垃圾收集。
过去的一个月,我描述了垃圾收集的环境,原因:以简化开发人员的内存管理。 我还讨论了由公共语言运行库(CLR)的使用的一般算法以及对本算法的一些内部工作机制的描述。 此外,我解释了如何开发显式handle资源管理,实现Finalize,Close和/或Dispose方法来清理这些handle资源。 这个月,我将结束我的CLR的垃圾收集器的讨论。
我将首先探讨所谓的弱引用功能,您通过使用如引用来降低大对象对托管堆产生的内存压力。 然后,我将讨论垃圾收集器如何使用代来提高性能。 最后,我将总结垃圾收集器提供的其他一些增加性能的手段,比如多线程收集以及由CLR公开的允许您监视垃圾收集器的实时性能的计数器。
弱引用
当根指向一个对象,该对象不能被收集,因为应用程序的代码可以达到这个对象。 当根指向一个对象,它被称为强引用该对象。 然而,垃圾收集器还支持弱引用。 弱引用允许垃圾收集器收集的对象,但也允许应用程序访问的对象。 怎么会这样呢? 这一切都归结为时间。
只要垃圾收集器运行时存在弱引用的对象,该对象会被被收集,当应用程序试图访问对象,访问将失败。 另一方面,要访问一个弱引用的对象,应用程序必须获取对象的强引用。 如果应用程序在垃圾处理器处理对象之前获得强引用,那么垃圾回收器无法收集的对象,因为对象有一个强引用存在。 我知道这一切听起来有点混乱,所以让我们清晰的代码,在它的研究Figure 1 。
//Figure 1 Strong and Weak References
Void Method()
{
Object o = new Object(); // Creates a strong reference to the
// object.
// Create a strong reference to a short WeakReference object.
// The WeakReference object tracks the Object.
WeakReference wr = new WeakReference(o);
o = null; // Remove the strong reference to the object
o = wr.Target;
if (o == null)
{
// A GC occurred and Object was reclaimed.
} else {
// a GC did not occur and we can successfully access the Object
// using o
}
}
你为什么会使用弱引用? 嗯,有一些数据结构创建起来很容易,但需要大量的内存。 例如,您可能有一个应用程序需要知道用户的硬盘驱动器下所有的目录和文件。 您可以轻松地构建一个树,在应用程序运行时反映了这一信息,你去引用内存中的树,而不是实际访问用户的硬盘。 此过程大大提高了应用程序的性能。
问题是,树可能会非常大,需要相当多的存储空间。 如果用户开始访问你的应用程序的不同部分,树可能不再需要了,这样是在浪费宝贵的内存。 您可以删除树,但是如果用户切换到应用程序的原来部分,你又需要重建树。 弱引用可以使你简单有效的处理这些情况。
当用户从应用程序的第一部分中切换到其他部分,您可以创建一个树的弱引用并销毁所有强引用。 如果应用程序的其他部分内存负载比较低,则垃圾回收器不会回收该树的对象。 当用户切换到应用程序的第一部分,应用程序试图获取一个树强引用。 如果成功的话,应用程序不必遍历用户的硬盘驱动器了。
该WeakReference的类型提供了两个构造函数:
WeakReference(Object target);
WeakReference(Object target, Boolean trackResurrection);
参数target对象是WeakReference要跟踪的对象。 trackResurrection参数表明WeakReference对象应在Finalize方法调用前还是调用后跟踪的对象。 通常情况下trackResurrection参数设置为False和第一个构造函数创建一个WeakReference的对象不跟踪复活。
为方便起见,弱引用不跟踪复活称为短弱引用,跟踪复活的弱引用被称为长弱引用。 如果对象的类型不提供Finalize方法,短弱引用和长弱引用的行为相同。 我们强烈建议您避免使用长弱引用。 长弱引用允许你复活一个已经Finalized的对象后,并且复活的对象状态是不可预知的。
一旦你已经创建了一个弱引用的对象,通常设置到对象的强引用为null。 如果任何强引用仍然存在,垃圾收集器将无法收集该对象。
要使用这个对象,你必须将弱引用变成一个强引用。 您只需通过调用完成WeakReference对象的Target属性和分配返回的结果到应用程序的一个root上。 如果Target属性返回null,那么对象被收集。 如果该属性不返回null,然后根是一个对象的强引用,可以用代以操纵他。 只要存在强引用,该对象将不能被收集。
弱引用内部机制
从前面的讨论,这应该是显而易见的WeakReference的对象与其他对象类型有区别。 通常,如果你的应用程序有一个根,包含一个对象A的引用,该对象A又引用另一个对象,那么这两个对象都是可达的和垃圾回收器无法回收他们中任一对象使用的内存。 但是,如果应用程序有一个根,是指向一个WeakReference对象,那么WeakReference对象将不被视为reachable,并可能被收集。
为了充分了解弱引用如何工作,让我们再次进入托管堆内部。 托管堆包含两个专门为了管理弱引用的内部数据结构:短弱引用表和长弱引用表。 这两个表只包含托管堆中分配对象的指针。
最初,这两个表是空的。 当你创建一个WeakReference对象,一个不是从托管堆中分配的对象。 在弱引用表中的一个空Slot将被设置。短弱引用使用短弱引用表和长弱引用使用长弱引用表。
一旦一个空Slot被发现,在空Slot的值将被设置为你想追踪的那个对象的地址。这个对象的指针将被传递到的WeakReference的构造函数。New操作符返回的地址将是弱引用表中对应Slot存储的地址。显然,这两个弱引用表不被视为一个应用程序的根, 垃圾回收器将无法收回指向弱引用表的对象。
现在,这里所发生的事情时,垃圾收集(GC)运行:
1. 垃圾收集器生成的所有可到达对象图。 第1部分讨论如何工作的这篇文章。
2. 垃圾收集器扫描短弱引用表。 如果在表的指针指向一个对象,它不是图的一部分,那么指针标识了一个不可达对象,在短弱引用表将Slot设置为null。
3. 垃圾收集器扫描finalization队列。 如果队列中的某个指针指向一个对象,它不是图的一部分,那么指针标识了一个不可及的对象,指针将从finalization队列移动到freachable队列。 在这一点上,该对象被添加到图因为对象目前被认为是可访问的。
4. 垃圾收集器扫描长弱引用表。 如果在表的某个指针指向一个对象,它是不是图的一部分(现在它包含在freachable队列条目所指向的对象中),然后将指针标识了一个不可及的对象和Slot设置为null。
5. 垃圾收集压缩内存,收集不可达对象所造成的内存空白段。
一旦你理解了垃圾收集过程的逻辑,很容易理解弱引用如何工作。 访问的WeakReferenced对象的Target属性使系统返回在弱引用表中适当位置的值。 如果Slot是空的,收集这个对象。
短弱引用不跟踪复活。 这意味着一旦垃圾收集器确定该对象是不可访问的,就会立即设置短弱引用表中的指针为空。 如果对象有Finalize方法,该方法还没有被调用所以对象仍然存在。 如果应用程序访问的WeakReference对象的Target属性,将返回空但是对象实际上仍然存在。
长弱引用跟踪复活。 这意味着垃圾收集器的指针在对象的存储空间可回收时,设置长弱引用表中的指针对应为空。 如果对象有Finalize方法,Finalize方法被调用,对象没有复活。
代
当我第一次开始在垃圾收集的环境中工作,我对性能非常担心。 毕竟,我是一个已经超过15年工作经验的C / C + +程序员,我理解的分配和释放的堆内存块从头顶。 当然,每个Windows版本,每个版本的C运行时调整了内部的堆算法,以提高性能。
嗯,就像Windows的开发和C运行时,GC的开发商都调整垃圾收集器,以改善其性能。 其中一个垃圾收集器存在纯粹是为了提高性能,功能被称为一代。代垃圾收集器(也称为一个临时垃圾收集器)作如下假设:
• 较新的对象,其生命周期越短。
• 老的对象,其生命周期越长。
• 较新的对象往往有强烈的相互关系,经常在同一时间访问。
• 压缩了一部分堆比压缩整个堆得更快。
当然,许多研究表明,这些假设是一大套现有的应用非常有效。 因此,让我们讨论如何将这些假设都影响了垃圾收集器实现。
初始化时,托管堆包含任何对象。 对象添加到堆的说是0代,你可以看到在图2。 简单地说,第0代对象是从来没有被垃圾回收器检查的年轻的对象。
图2 0代
现在,如果有更多的对象添加到堆,堆填充和垃圾收集必须发生。 当垃圾收集器分析了堆,它建立了垃圾图(显示在这里紫色)和非垃圾对象。 任何在垃圾收集后还存在的对象,被压缩到堆的最左边。 经历过这此垃圾回收后存在的对象,是老对象,现在被认为是1代( 见图3)。
图3 0和1代
即使多个对象添加到堆,这些新的,年轻一代的对象被放置在0代。 如果 0代被填满,一个GC执行。 这一次,第1代在这生存的所有对象都将被压缩,被认为是在第2( 见图4)。 0代中所有的幸存者现在压缩,被认为是在1代。 目前包含0代没有对象,但所以新的对象都将进入0代。
图4 代0,1和2
目前,运行库垃圾回收器支持的最高带仅仅是第2代。 等到将来的GC发生,目前在第2代任何尚存的对象将仍然停留在第2代。
代的GC性能优化
正如我前面所说,代垃圾收集可以提高性能。 当堆填充和回收时,垃圾收集器可以选择只检查第0代中的对象,而忽略任何更大的几代人的对象。 毕竟,新的对象,其生命周期预期会越短。 因此,收集和压缩0代对象很可能从堆中回收了大量空间,比收集所有代的对象更快。
这是可从代的GC获得的最简单的优化。 代收集器可以通过不遍历在托管堆中的每个对象来获取更多的优化。 如果根或对象引用一个老一代的对象,垃圾收集器可以忽略的老对象的任何内部引用,以减少建立可到达对象图的时间。 当然,它可能是一个旧的对象引用一个新的对象。 因此,收集器可以利用系统的write-watch来研究这些对象(由Win32-GetWriteWatch在Kernel32.dll功能提供)。 这种支持让收集器知道哪个老对象(如有)自从上次收集已经被写入。 这些特定的老对象可以有自己的引用检查,看看他们是否涉及任何新的对象。
如果0代收集没有提供出足够的存储空间,则可以垃圾回收器尝试回收世代1和0的对象。 如果这些方法都失败,则收集器可以收集所有代2,1和0的对象。用于确定区域中的哪些代会被收集器收集的准确算法,微软一直在调整。
大多数堆(如C运行时堆)分配对象在任何发现的可用空间地方。 因此,如果我连续创建几个对象,这是很可能这些对象将被兆字节的地址空间分隔。 但是,在托管堆中连续几个对象分配,该对象在内存是连续的。
假设之一是,如前所述新的对象往往有强烈的相互关系,经常在同一时间访问。 由于新的对象被分配在内存中是连续的,你便从引用性质得到更好的优化。 更具体地说,它极有可能,所有的对象可以驻留在CPU的缓存。 当CPU将能够通过缓存执行大部分操作,而不需要使用RAM时,您的应用程序将能用惊人的速度访问这些对象。
微软的性能测试表明,托管堆分配是由Win32快于HeapAlloc函数执行的标准分配。 这些测试还表明,它需要一个200MHz奔腾小于1毫秒执行第0代完全GC。 这是微软的目标是使一次垃圾处理比一个普通页错误耗时更少。
直接控制System.GC
System.GC类型允许应用程序在垃圾收集进行一些直接控制。 首先,你可以通过阅读GC.MaxGeneration属性查询托管堆最大支持的代数。 目前,GC.MaxGeneration属性总是返回2。
通过调用下面两个函数中的一个来使垃圾回收器执行一次垃圾回收:
void GC.Collect(Int32 Generation)
void GC.Collect()
第一种方法允许你指定收集哪个代。 你可以传递任何整数从0到GC.MaxGeneration。 传递0将导致0代收集,传递1将导致1和0代被收集和传递2导致第2代,1和0被收集。 该收集方法不带参数的版本强制各代全额征收,相当于调用:
GC.Collect(GC.MaxGeneration);
在大多数情况下,你应该避免调用任何方法的收集,这是最好还是让垃圾收集器上自行运行。 然而,由于您的应用程序比运行时更了解它执行的意义,你可以明确的执行回收(运行时只会认为自己在空间不足的时候执行清理空间垃圾的操作,但是我们可以在某些操作执行后,立即清理对象,以避免某些问题的产生)。 例如,在用户保存文件之后,做一次完全的收集可能会很有意义。 互联网浏览器在页面关闭时执行一次全面的收集。 您可能还需要在你的应用程序做了一次冗长做错后执行一次强制收集,这隐藏了一个事实,在用户与应用程序交互时,回收可能获取了操作时间,但阻止了回收执行。
GC的类型还提供了一个WaitForPendingFinalizers方法。 这种方法简单地挂起调用线程,直到freachable队列清空队列,调用每个对象的Finalize方法。 在大多数应用中,它是不可能的,你永远不会有调用此方法。
最后,垃圾收集器提供了两种方法,使您能够确定一个对象目前在那一代:
Int32 GetGeneration(Object obj);
Int32 GetGeneration(WeakReference wr);
第一个版本的GetGeneration的参数是一个对象的引用,第二个版本接受一个参数WeakReference的引用。 当然,返回值将介于0和GC.MaxGeneration(包括GC.MaxGeneration)之间。
在代码Figure 5可以帮助您了解代工作。
//Figure 5 GC Methods Demonstration
private static void GenerationDemo() {
// 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 = new GenObj("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
Collect();
obj.DisplayGeneration(); // Displays 1
Collect();
obj.DisplayGeneration(); // Displays 2
Collect();
obj.DisplayGeneration(); // Displays 2 (max generation)
obj = null; // Destroy the strong reference to this object
Collect(0); // Collect objects in generation 0
WaitForPendingFinalizers(); // We should see nothing
Collect(1); // Collect objects in generation 1
WaitForPendingFinalizers(); // We should see nothing
Collect(2); // Same as Collect()
WaitForPendingFinalizers(); // Now, we should see the Finalize
// method run
Display(-1, "Demo stop: Understanding Generations.", 0);
}
多线程应用程序性能
在上一节中,我解释了GC算法和优化。 然而,这个讨论我们都是在一个假设的前提下:只有一个线程正在运行。 事实上,它很有可能将要多个线程访问托管堆,或至少操纵在托管堆中分配的对象。 当一个线程触发了一次收集,其他线程不能访问任何(包括它自己的堆栈对象引用)的对象,因为收集器可能会移动这些对象,改变他们的内存位置。
所以,当垃圾收集器要开始收集时,所有线程执行托管代码必须暂停。 运行时有一个安全暂停线程的机制以便于垃圾器可以回收资源。 究其原因有多种机制,是保持线程运行尽可能长,尽可能减少开销。 我不希望把所有的细节放在这里,但我只想说,微软已经做了很多工作,以减少参与收集与执行的开销。 微软将随着时间的推移继续修改,以确保这些机制有效的执行垃圾收集。
以下各段描述的机制,垃圾收集器采用多线程应用程序时有几个:
完全中断代码
当一个收集开始,收集器挂起所有的应用程序线程。 然后回收器使用(JIT)编译器产生的表确定在何处收集器暂停线程,收集器可以告诉线程在一个非方法的某处停止,当在当前访问中引用了这些代码,这些引用将会被保留(一个变量,CPU寄存器等)。
劫持
收集器可以修改线程的堆栈,使返回地址指向一个特殊函数。 当目前执行的方法返回时,线程会挂起,这个特殊方法将执行。 偷了线程的执行路径这种方式被称为劫持线程。 当收集完成后,该线程将恢复并返回到原来调用的方法。
Safe Points
编译器编译一个方法时,它可以插入到一个特殊的功能要求,检查是否一个GC是否将要执行。 如果是这样,线程被挂起,到GC行完成,线程被恢复。 编译器插入的这些方法调用的位置被称为一个GC安全点。
请注意,线程劫持允许线程正在执行非托管代码继续执行,同时进行垃圾回收。因为非托管代码不访问的托管堆上的对象,除非不包含引用对象并且对象被固定。 垃圾收集器不能在内存中移动一个固定的对象。 如果一个线程正在执行非托管代码,然后返回到托管代码,该线程被劫持挂起,直到GC完成。
除了我刚才提到的机制,垃圾收集器提供了一些额外的改进,提高多线程应用程序中对象的分配和回收。
同步-释放分配(Synchronization-free Allocations)
在多处理器系统,托管堆的0代被分成很多内存块,每个线程使用一块。 这允许多个线程同时进行分配,使独占访问堆并不是必需的。
可扩展收集(Scalable Collections)
在多处理器中,服务器版本的操作系统(MSCorSvr.dll),托管堆将被分成几个部分,每个CPU一个。 当开始收集,每个CPU启动一个线程,所有线程收集自己的部分并同时进行。 工作站版本(Mscorwks.dll)不支持此功能。
大对象垃圾收集
还有一个你可能想知道的更高的性能改善。 大对象(那些20,000字节或更大)是从一个特殊的大对象堆分配。 在这个堆离得对象想之前讨论的小对象一样的finalized 和释放。 然而,大型对象不用压缩,因为在堆中转移20000字节内存块下来会浪费太多的CPU时间。
请注意,所有这些机制对你应用程序代码是透明的。 对你来说,开发人员,它看起来像只有一个托管堆,这些机制存在只是为了提高应用程序性能。
监控垃圾回收
微软运行时团队已经建立了一个性能计数器,提供了关于运行时的业务很多的实时统计信息。 您可以通过Windows 2000系统ActiveXÂ ®监控控制器观察这些统计数字。访问系统监测控制器最简单的方式是运行PerfMon.exe和选择+工具栏按钮,使Add Counter对话框显示( 见图6)。
图6添加性能计数器
为了监控运行时的垃圾收集器,选择COM +内存性能对象。 然后,您可以从实例列表中选择具体应用程序。 最后,你还可以选择您感兴趣的应用程序集监测,按添加按钮,关闭按钮。 此时,系统监视器将选定实时统计数据绘图表示出来。 Figure 7描述了每个计数器的功能。
Figure 7 Counters to Monitor
Counter | Description |
# Bytes in all Heaps | Total bytes in heaps for generations 0, 1, and 2 and from the large object heap. This indicates how much memory the garbage collector is using to store allocated objects. |
# GC Handles | Total number of current GC handles. |
# Gen 0 Collections | Number of collections of generation 0 (youngest) objects. |
# Gen 1 Collections | Number of collections of generation 1 objects. |
# Gen 2 Collections | Number of collections of generation 2 (oldest) objects. |
# Induced GC | Total number of times the GC was run because of an explicit call (such as from the Classlibs) instead of during an allocation. |
# Pinned Objects | Not yet implemented. |
# of Sink Blocks in use | Synchronization primitives use sink blocks. Sink block data belongs to an object and is allocated on demand. |
# Total committed Bytes | Total committed bytes from all heaps. |
% Time in GC | Total time since the last sample spent performing garbage collection, divided by total time since the last sample. |
Allocated Bytes/sec | Rate of bytes per second allocated by the garbage collector. This is only updated at a garbage collection, not at each allocation. Since it is a rate, time between GCs will be 0. |
Finalization Survivors | Number of garbage-collected classes that survive because their finalizer creates a reference to them. |
Gen 0 heap size | Size of generation 0 (youngest) heap in bytes. |
Gen 0 Promoted Bytes/Sec | Bytes per second that are promoted from generation 0 (youngest) to generation 1. Memory is promoted when it survives a garbage collection. |
Gen 1 heap size | Size of generation 1 heap in bytes. |
Gen 1 Promoted Bytes/Sec | Bytes per second that are promoted from generation 1 to generation 2 (oldest). Memory is promoted when it survives a garbage collection. Nothing is promoted from generation 2, since it is the oldest. |
Gen 2 heap size | Size of generation 2 (oldest) heap in bytes. |
Large Object Heap size | Size of the Large Object heap in bytes. |
Promoted Memory from Gen 0 | Bytes of memory that survive garbage collection and are promoted from generation 0 to generation 1. |
Promoted Memory from Gen 1 | Bytes of memory that survive garbage collection and are promoted from generation 1 to generation 2. |
结论
以上所有是关于垃圾回收的说明。 上个月,我提供了有关资源如何分配,如何自动垃圾收集工作,如何使用finalization功能允许一个GC后清理,以及复活的功能如何恢复对对象的访问。 这个月我解释了弱,强引用实现,如何将对象划分几代来提高性能,以及如何可以手动控制System.GC垃圾收集。 我也涵盖了垃圾收集机制在多线程应用程序中使用时如何提高性能,当对象大于20000字节时会发生什么,最后,你可以使用Windows 2000系统监视器来跟踪垃圾收集的性能。 有了这些信息,你应该能够简化存储管理,并提高应用程序的性能。
|
From the December 2000 issue of MSDN Magazine