第三章:GC Heap管理
这是《设计.Net Compact Framework CLR》的第三部分。在前面两章中,我们讨论了CLR如何管理内存和JIT编译器的基本设计原则。
Part I, Overview and Background
Part II, Jit Compiler Design Considerations
这一章我们主要讨论垃圾收集设计中如何管理GC heap的问题。
---------
讨论.NET平台如何管理内存,垃圾收集肯定是第一个被提及的话题。不必惊讶,Compact Framework的垃圾收集和桌面版本有很多地方不同,原因是运行环境的不同。就像Compact Framework CLR中的其他大部分子系统一样,垃圾收集被设计成,在设备可用内存较低时,释放所有能够释放的内存。
对更高层次,GC提供了两个基本功能:分配内存保存引用类型的实例,在这些实例不再使用时收集他们。
Allocating Reference Types
通过第一章的讨论,我们知道GC heap存在于进程独有的32M虚拟地址空间中。和JIT堆一样,GC heap开始时很小,随着增长需要更多的空间。然而,比较JIT堆,GC heap的增长有两个重要的不同:GC heap以一个固定的增量增长,另外,它不会增长到很大。
GC Heap的增量通常是6K。当一个新的64K“内存段”被创建时,内存分配将从该段中被分配,直到没有足够的空间,然后一个新的“内存段”将被创建。Compact Framework GC使用标准Win32 API VirtualAlloc来分配新的内存段。分配一个内存段是很快的。在.NET Compact Framework 2.0的性能测试显示,每秒可以分配750万个小型引用类型的实例。分配器之所以如此高效,是因为它使用了简单的算法。在GC堆中用一个指针指向下一个有效空间。当分配一个新实例的空间时,分配器简单地将指针移动该对象所需要的字节数就可以了,如图4所示:
Figure 4
分配一个新的引用类型只是简单地移动“next object”指针
GC heap默认的增量值是64K,在分配大于64K的对象时,分配器将会以比较大的增量来增加Heap的尺寸。例如,如果一个程序尝试创建一个70K的对象,分配器将创建一个新的70K的内存段来存储这个大对象。
Gc Heap会像我们描述得这样一直增加,直到达到1MB。这时一个垃圾回收机制将被启动(更多地回收将在以后出现)。在这以后,GC Heap也许会继续增长,但是从上一次回收后,只要分配了1MB的对象,垃圾回收就会被启动。
Collecting Reference Types
现在我们已经了解GC Heap是如何增长的,让我们来看看它是如何收缩的。此外,在内存压力下减少GC heap尺寸的能力是帮助应用程序更好地运行在内存受限设备上的关键。在这一节中,我们将考虑那些原因会触发一次垃圾回收和内存何时被释放并被返回给操作系统。
有一些原因会触发垃圾回收机制。这些原因中的一个就是分配了1MB的托管对象。其他原因包括分配资源失败,例如,无法分配更多的内存,创建更多的Windows句柄等。请查看An Overview of the .Net Compact Framework Garbage Collector中对这些引发收回原因的详细讨论。
当GC发生时,内存不是每次都会被释放并返回给操作系统的。为了理解从操作系统角度来看内存何时被释放,让我们来看一下当垃圾回收运行时到底发生了什么?
Unreferenced Objects
在每次垃圾回收的过程中,GC会检索整个Heap以发现哪些对象不再被引用。这些不再使用对象的内存将被释放回GC Heap,使GC heap有更多可用空间。在释放不再引用的类型后,一些64K的内存段将有可能是完全空着的。如果内存段是完全空的并且GC仍然有1MB以上的分配空间,空的64K内存段将通过调用VirtualFree被归还给操作系统。除了完整GC(原文为”full” GC,将在下面讨论)外,收集器将会保持总数1MB的64K内存段的缓存,即使其中的一些内存段是空的。通过缓存代码段的方式,我们通过减少调用VirtualAlloc和VirtualFree的次数来改善性能。
Compacting the GC Heap
在垃圾回收的过程中,GC将随意地压缩堆。当一个堆充满碎片时,收集器将通过将所有活动对象移动到一起的方式,来“压实”整个堆。压实堆的主要目的是产生大量的有效内存块,以分配更多的对象。图5表现了GC Heap中的内容在压缩前后的状态。
Figure 5
The contents of a sample GC heap before and after compaction
通过”简单”对象回收,GC将归还1MB内存段缓存之外的空64K内存段给操作系统。
A "Full" GC
在正常的事态发展下,GC将按照下面描述的方式来工作:在每次分配1MB之后,周期性的垃圾回收将被启动;如果Heap充满碎片,它将被压缩;如果收集器有1MB的缓存,多余的空内存段将被返还给操作系统。
事实上,在这三个设定之外,为了获取更多的可用内存,还会使用哪些更激烈地做法。这些做法和发生的时机密切相关,就像第二章中JIT在这些时候减小它的Heap尺寸:当分配内存或其他资源失败时发生,当一个应用程序被切换到后台时,或者当应用程序接收到操作系统的WM_HIBERNATE消息时。在这些时候,GC将不再保持在1MB的内存段缓存上。(译注:原文为the GC will hot hold onto its 1MB segment cache.严重怀疑是the GC will not hold onto its 1MB segment cache.否则意思不通,故改之)它将回收所有的不在引用的对象,压缩堆,并且释放所有可以释放的内存。
Pulling it All Together
现在我们已经了解了GC如何分配和释放内存,让我们看一下GC heap的尺寸在应用程序的生命周期中的变化。
Figure 6
The size of the GC heap over the lifetime of an application.
图6跟踪了两部分的数据,黄色线表示在应用程序的生命周期中,垃圾收集被请求分配的累计byte数。该数将持续增长,不会跳跃,就像应用程序持续分配新对象。
图6中的蓝色线表示每个时间GC heap的尺寸。有几个时间点值得注意。首先,当应用程序开始并持续运行时,我们看到GC heap的尺寸的增长和前面所见是一致的。图中的每一步和新创建的64K GC段是一致的。第二,我们可以看到有时蓝色线水平偏离。就像你在前边关于分配和回收算法的讨论中所了解的那样,在1MB的时候heap尺寸变成水平。接下来,你可以看到heap尺寸戏剧性地下降。下降的发生在我将应用程序切换到后台时。在应用程序切换到前台后,我们开始看到heap再次以64K开始增长。
这个系列现在覆盖了.Net Compact Framework如何管理内存的基础,和构建JIT编译器和垃圾回收时的设计决定。在我的下一篇中,我们将讨论在内存受限设备中,CLR class loader如何运行得更有效率。
This posting is provided "AS IS" with no warranties, and confers no rights.
Aawolf: Steven老兄最近好象对这个系列文章不感兴趣了,没有再更新。不过他的文章还是很有意思的,不多的文字就解释了一些隐藏在背后的运行机制。期待他的下一篇。
这是《设计.Net Compact Framework CLR》的第三部分。在前面两章中,我们讨论了CLR如何管理内存和JIT编译器的基本设计原则。
Part I, Overview and Background
Part II, Jit Compiler Design Considerations
这一章我们主要讨论垃圾收集设计中如何管理GC heap的问题。
---------
讨论.NET平台如何管理内存,垃圾收集肯定是第一个被提及的话题。不必惊讶,Compact Framework的垃圾收集和桌面版本有很多地方不同,原因是运行环境的不同。就像Compact Framework CLR中的其他大部分子系统一样,垃圾收集被设计成,在设备可用内存较低时,释放所有能够释放的内存。
对更高层次,GC提供了两个基本功能:分配内存保存引用类型的实例,在这些实例不再使用时收集他们。
Allocating Reference Types
通过第一章的讨论,我们知道GC heap存在于进程独有的32M虚拟地址空间中。和JIT堆一样,GC heap开始时很小,随着增长需要更多的空间。然而,比较JIT堆,GC heap的增长有两个重要的不同:GC heap以一个固定的增量增长,另外,它不会增长到很大。
GC Heap的增量通常是6K。当一个新的64K“内存段”被创建时,内存分配将从该段中被分配,直到没有足够的空间,然后一个新的“内存段”将被创建。Compact Framework GC使用标准Win32 API VirtualAlloc来分配新的内存段。分配一个内存段是很快的。在.NET Compact Framework 2.0的性能测试显示,每秒可以分配750万个小型引用类型的实例。分配器之所以如此高效,是因为它使用了简单的算法。在GC堆中用一个指针指向下一个有效空间。当分配一个新实例的空间时,分配器简单地将指针移动该对象所需要的字节数就可以了,如图4所示:
Figure 4
分配一个新的引用类型只是简单地移动“next object”指针
GC heap默认的增量值是64K,在分配大于64K的对象时,分配器将会以比较大的增量来增加Heap的尺寸。例如,如果一个程序尝试创建一个70K的对象,分配器将创建一个新的70K的内存段来存储这个大对象。
Gc Heap会像我们描述得这样一直增加,直到达到1MB。这时一个垃圾回收机制将被启动(更多地回收将在以后出现)。在这以后,GC Heap也许会继续增长,但是从上一次回收后,只要分配了1MB的对象,垃圾回收就会被启动。
Collecting Reference Types
现在我们已经了解GC Heap是如何增长的,让我们来看看它是如何收缩的。此外,在内存压力下减少GC heap尺寸的能力是帮助应用程序更好地运行在内存受限设备上的关键。在这一节中,我们将考虑那些原因会触发一次垃圾回收和内存何时被释放并被返回给操作系统。
有一些原因会触发垃圾回收机制。这些原因中的一个就是分配了1MB的托管对象。其他原因包括分配资源失败,例如,无法分配更多的内存,创建更多的Windows句柄等。请查看An Overview of the .Net Compact Framework Garbage Collector中对这些引发收回原因的详细讨论。
当GC发生时,内存不是每次都会被释放并返回给操作系统的。为了理解从操作系统角度来看内存何时被释放,让我们来看一下当垃圾回收运行时到底发生了什么?
Unreferenced Objects
在每次垃圾回收的过程中,GC会检索整个Heap以发现哪些对象不再被引用。这些不再使用对象的内存将被释放回GC Heap,使GC heap有更多可用空间。在释放不再引用的类型后,一些64K的内存段将有可能是完全空着的。如果内存段是完全空的并且GC仍然有1MB以上的分配空间,空的64K内存段将通过调用VirtualFree被归还给操作系统。除了完整GC(原文为”full” GC,将在下面讨论)外,收集器将会保持总数1MB的64K内存段的缓存,即使其中的一些内存段是空的。通过缓存代码段的方式,我们通过减少调用VirtualAlloc和VirtualFree的次数来改善性能。
Compacting the GC Heap
在垃圾回收的过程中,GC将随意地压缩堆。当一个堆充满碎片时,收集器将通过将所有活动对象移动到一起的方式,来“压实”整个堆。压实堆的主要目的是产生大量的有效内存块,以分配更多的对象。图5表现了GC Heap中的内容在压缩前后的状态。
Figure 5
The contents of a sample GC heap before and after compaction
通过”简单”对象回收,GC将归还1MB内存段缓存之外的空64K内存段给操作系统。
A "Full" GC
在正常的事态发展下,GC将按照下面描述的方式来工作:在每次分配1MB之后,周期性的垃圾回收将被启动;如果Heap充满碎片,它将被压缩;如果收集器有1MB的缓存,多余的空内存段将被返还给操作系统。
事实上,在这三个设定之外,为了获取更多的可用内存,还会使用哪些更激烈地做法。这些做法和发生的时机密切相关,就像第二章中JIT在这些时候减小它的Heap尺寸:当分配内存或其他资源失败时发生,当一个应用程序被切换到后台时,或者当应用程序接收到操作系统的WM_HIBERNATE消息时。在这些时候,GC将不再保持在1MB的内存段缓存上。(译注:原文为the GC will hot hold onto its 1MB segment cache.严重怀疑是the GC will not hold onto its 1MB segment cache.否则意思不通,故改之)它将回收所有的不在引用的对象,压缩堆,并且释放所有可以释放的内存。
Pulling it All Together
现在我们已经了解了GC如何分配和释放内存,让我们看一下GC heap的尺寸在应用程序的生命周期中的变化。
Figure 6
The size of the GC heap over the lifetime of an application.
图6跟踪了两部分的数据,黄色线表示在应用程序的生命周期中,垃圾收集被请求分配的累计byte数。该数将持续增长,不会跳跃,就像应用程序持续分配新对象。
图6中的蓝色线表示每个时间GC heap的尺寸。有几个时间点值得注意。首先,当应用程序开始并持续运行时,我们看到GC heap的尺寸的增长和前面所见是一致的。图中的每一步和新创建的64K GC段是一致的。第二,我们可以看到有时蓝色线水平偏离。就像你在前边关于分配和回收算法的讨论中所了解的那样,在1MB的时候heap尺寸变成水平。接下来,你可以看到heap尺寸戏剧性地下降。下降的发生在我将应用程序切换到后台时。在应用程序切换到前台后,我们开始看到heap再次以64K开始增长。
这个系列现在覆盖了.Net Compact Framework如何管理内存的基础,和构建JIT编译器和垃圾回收时的设计决定。在我的下一篇中,我们将讨论在内存受限设备中,CLR class loader如何运行得更有效率。
This posting is provided "AS IS" with no warranties, and confers no rights.
Aawolf: Steven老兄最近好象对这个系列文章不感兴趣了,没有再更新。不过他的文章还是很有意思的,不多的文字就解释了一些隐藏在背后的运行机制。期待他的下一篇。