在本文中,您将一次找到两个信息源:
- 俄语完整的垃圾收集器课程:CLRium#6( 当前的研讨会在此处 )
- 毛妮·史蒂文斯(Maoni Stevens)的BOTR“垃圾收集器”文章翻译。

1. CLRium#5:完整的垃圾收集器课程

注意:要全面了解有关垃圾收集的更多信息,请参阅《垃圾收集手册》 ; Pro .NET Memory Management一书中提供了有关CLR中垃圾收集器的专业信息。 到这两个资源的链接都在文档末尾给出。
组件架构
垃圾收集与两个组件相关:分配器和收集器。 分配器负责分配内存,并在必要时调用收集器。 收集器收集垃圾或程序不再使用的对象的内存。
还有其他方法可以调用收集器,例如,使用GC.Collect手动调用。 此外,终结器线程可能会收到异步通知,指出内存即将用尽(这将导致收集器)。
分配器设备
通过运行时的辅助组件使用以下信息来调用分发器:
- 分配地块的必要大小;
- 执行线程的内存分配上下文;
- 指示例如对象是否可终结的标志。
垃圾收集器没有为不同类型的对象提供特殊的处理方法。 它从运行时接收有关对象大小的信息。
根据大小,收集器将对象分为两类:小(<85,000字节)和大(> = 85,000字节)。 通常,大小对象的组装可以相同的方式进行。 但是,收集器按大小将它们分开,因为压缩大对象需要大量资源。
垃圾回收器根据分配上下文将内存分配给分配器。 分配上下文的大小由分配的内存块确定。
大对象不使用上下文和块。 一个大对象可能大于这些小内存。 此外,使用这些区域(如下所述)的好处只有在处理小物体时才显而易见。 大对象的空间直接在堆段中分配。
分配器设计为:
必要时调用垃圾收集器:当为对象分配的内存量超过阈值(由收集器设置),或者如果分配器无法再在此段中分配内存时,分配器将调用垃圾收集器 。 阈值和受控段将在后面详细描述。
保存对象的位置:位于堆的一部分中的对象存储在彼此靠近的虚拟地址处。
有效地使用缓存:分配器以块为单位分配内存,而不是为每个对象分配内存。 它会将太多的内存清零以准备处理器缓存,因为某些对象将直接放置在其中。 分配的内存块通常为8 KB。
有效地限制了分配给执行线程的区域:为该线程分配的上下文和内存块的接近程度确保只有一个线程将数据写入分配的空间。 结果,在当前分配上下文中的空间结束之前,无需限制内存的分配。
确保内存完整性:垃圾回收器始终将新分配对象的内存清零,以使它们的链接不会指向内存的任意部分。
确保堆连续性:分配器从每个分配的块中的剩余内存中创建一个空闲对象。 例如,如果块中剩余30个字节,并且需要40个字节容纳下一个对象,则分配器会将这30个字节转换为空闲对象并请求一个新的内存块。
API
Object* GCHeap::Alloc(size_t size, DWORD); Object* GCHeap::Alloc(alloc_context* acontext, size_t size, DWORD);
使用这些功能,您可以为小型和大型对象分配内存。 有一个函数可以在大对象堆(LOH)上分配空间:
Object* GCHeap::AllocLHeap(size_t size, DWORD);
集热器
垃圾收集器任务
GC设计用于高效的内存管理。 编写托管代码的开发人员可以毫不费力地使用它。 善治意味着:
- 垃圾回收应该经常发生,以免乱扔大量分配给内存的未使用对象(垃圾)的托管堆(按比例或绝对数量);
- 垃圾收集应尽可能少地发生,以免浪费可用的处理器时间,即使更频繁的收集将减少内存使用量;
- 垃圾回收应该是有效率的,因为如果由于程序集而只释放了一小块内存,那么程序集和所花费的处理器时间都是徒劳的;
- 垃圾收集应该很快,因为许多工作量需要较短的延迟时间;
- 编写托管代码的开发人员无需了解垃圾回收方面的知识即可有效地使用内存(与其工作量相比);
- 垃圾收集器必须适应内存使用的不同性质。
托管堆的逻辑描述
CLR垃圾收集器收集在逻辑上按代分离的对象。 在组装了第N代中的对象之后,其余对象被标记为属于第N + 1代。 此过程称为跨代对象提升。 在此过程中,有必要将对象转移到较低的世代或根本不提前它的情况下会有例外。
对于小对象,堆分为三代:gen0,gen1和gen2。 对于大型物体,只有第三代-gen3。 Gen0和gen1称为短暂世代(对象在其中短暂生存)。
对于一堆小物体,世代号表示它们的年龄。 例如,gen0是最年轻的一代。 这并不意味着gen0中的所有对象都比gen1或gen2中的对象年轻。 下文描述了一些例外情况。 组装一代意味着在这一代以及所有年轻一代中组装物品。
从理论上讲,大型对象和小型对象的组装可以相同的方式进行。 但是,由于大型对象的压缩需要大量资源,因此它们的组装以不同的方式进行。 大对象仅包含在gen2中,并且出于性能原因,仅在此代的垃圾回收期间收集大对象。 gen2和gen3都可能很大,并且在临时世代(gen0和gen1)中构建对象不应太耗费资源。
对象被放置在最年轻的一代中。 对于小对象,这是gen0,对于大对象,这是gen3。
托管堆的物理描述
托管堆由一组段组成。 段是操作系统传递给垃圾收集器的连续内存块。 堆段分为小部分和大部分,以容纳大小对象。 每个堆的段连接在一起。 加载CLR时,至少保留一个小对象段和一个大对象段。
在每个小对象堆中,只有一个临时段,即gen0和gen1所在的世代。 此段可能包含也可能不包含gen2生成对象。 除了临时段以外,还可能存在一个或多个其他段,它们将是gen2段,因为它们包含第2代的对象。
一堆大物体由一个或多个段组成。
堆段从低地址到高地址填充。 这意味着位于段较低地址的对象比位于高级地址的对象要旧。 还有一些例外情况,如下所述。
根据需要分配堆段。 如果它们不包含使用过的对象,则将删除段。 但是,堆上的初始段始终存在。 每个堆一次分配一个段。 对于小对象,这发生在垃圾回收期间,而对于大对象,则在为其分配内存期间发生。 由于大型对象仅在第2代中组装(这需要大量资源),因此这种方案提高了生产率。
堆分段在选择中连接在一起。 链中的最后一个部分总是短暂的。 收集了所有对象的段可以重复使用,例如,短暂使用。 段重用仅适用于小对象的堆。 为了每次都考虑整个大对象时都容纳大对象。 小物件仅放置在临时区域中。
分配内存的阈值
这是与每一代的大小有关的逻辑概念。 如果超过该限制,则生成将开始垃圾收集。
特定世代的阈值取决于该世代中幸存对象的数量。 如果该量高,则阈值变高。 预期在下一代垃圾回收会话期间,已使用和未使用对象的比率会更好。
垃圾收集的世代选择
激活后,收集器必须确定要建立哪一代。 除了阈值之外,其他因素也会影响此选择:
- 一代的碎片化-如果一代是高度碎片化的,则其中的垃圾回收很有可能会产生成果;
- 如果机器的内存太忙,则收集器可以执行更深的清理,如果这种清理更有可能释放空间并避免不必要的页面交换(整个机器的内存);
- 如果临时段空间不足,则收集器可以对此段进行更深层的清理(收集更多的第1代对象),以避免分配新的堆段。
垃圾收集流程
标记阶段
在此阶段,CLR应该找到所有有生命的物体。
具有世代支持的收集器的优势在于其仅在堆的一部分中清除垃圾的能力,而不是不断观察所有对象的能力。 在临时世代中收集垃圾时,收集器应从运行时环境接收有关程序仍在使用这些世代中的对象的信息。 另外,老年人代的对象可以使用年轻人代的对象来指代它们。
为了标记引用新对象的旧对象,垃圾收集器使用特殊位。 在分配操作期间,由JIT编译器机制设置位。 如果对象属于临时代,则JIT编译器将设置包含指示初始位置的位的字节。 在临时世代收集垃圾时,收集器可以将这些位用于剩余的整个堆,并仅查看这些位所对应的对象。
规划阶段
在这一点上,对压缩进行建模以确定其有效性。 如果结果令人满意,则收集器将开始实际压缩。 否则,他只做清洁工作。
移动舞台
如果收集器执行压缩,这将导致对象移动。 在这种情况下,必须更新到这些对象的链接。 在移动阶段,收集器必须找到所有发生垃圾收集的代中指向对象的链接。 相反,在标记阶段,收集器仅标记活动对象,因此不需要考虑薄弱环节。
压缩阶段
此阶段非常简单,因为收集者已经在计划阶段确定了用于移动对象的新地址。 压缩后,对象将被复制到这些地址。
清洁阶段
在此阶段,收集器搜索活动对象之间未使用的空间。 他创建了自由对象,而不是这个空间。 附近的未使用对象变成一个自由对象。 所有自由对象都放置在自由对象列表中 。
代码流
条款:
- WKS GC:工作站模式下的垃圾收集
- SVR GC:服务器模式垃圾收集
功能行为
WKS GC无并行垃圾回收
- 用户线程使用了为其分配的所有内存,并调用垃圾回收器。
- 收集器调用
SuspendEE
来挂起所有托管线程。 - 收集器选择一代进行清洁。
- 开始标记对象。
- 收集器将进入计划阶段,并确定是否需要压缩。
- 如有必要,收集器将移动对象并执行压缩。 在另一种情况下,它只是进行清洁。
- 收集器调用
RestartEE
重新启动托管线程。 - 用户线程继续工作。
具有并行垃圾收集的WKS GC
该算法描述了后台垃圾回收。
- 用户线程使用了为其分配的所有内存,并调用垃圾回收器。
- 收集器调用
SuspendEE
来挂起所有托管线程。 - 收集器确定是否运行后台垃圾收集。
- 如果是这样,则激活后台垃圾回收线程。 该线程调用
RestartEE
以恢复托管线程。 - 为托管进程分配的内存与后台垃圾回收同时进行。
- 用户线程可以使用为其分配的所有内存,并启动临时垃圾收集(也称为高优先级垃圾收集)。 它的运行方式与工作站模式相同,没有并行垃圾回收。
- 后台垃圾回收
SuspendEE
再次调用RestartEE
以完成标记,然后调用RestartEE
以在运行用户线程的情况下开始并行清理。 - 后台垃圾收集完成。
没有并行垃圾回收的SVR GC
- 用户线程使用了为其分配的所有内存,并调用垃圾回收器。
- 服务器模式垃圾回收线程被激活,并导致
SuspendEE
暂停托管线程的执行。 - 服务器模式下的垃圾收集流执行与工作站模式下相同的操作,而没有并行垃圾收集。
- 服务器模式垃圾回收线程调用
RestartEE
来启动托管线程。 - 用户线程继续工作。
具有并行垃圾回收的SVR GC
该算法与工作站模式下的并行垃圾收集相同,只是在服务器线程中执行非声子收集。
物理架构
本节将帮助您了解代码流。
当用户线程的内存不足时,它可以使用try_allocate_more_space
函数获得可用空间。
需要启动垃圾收集器时, try_allocate_more_space
函数将调用GarbageCollectGeneration
。
如果工作站模式下的垃圾回收不是并行的,则GarbageCollectGeneration
在垃圾回收器调用的用户线程中执行。 代码流如下:
GarbageCollectGeneration() { SuspendEE(); garbage_collect(); RestartEE(); } garbage_collect() { generation_to_condemn(); gc1(); } gc1() { mark_phase(); plan_phase(); } plan_phase() { // , // if (compact) { relocate_phase(); compact_phase(); } else make_free_lists(); }
如果在工作站模式下(默认情况下)执行并行垃圾收集,则后台垃圾收集的代码流如下所示:
GarbageCollectGeneration() { SuspendEE(); garbage_collect(); RestartEE(); } garbage_collect() { generation_to_condemn(); // // do_background_gc(); } do_background_gc() { init_background_gc(); start_c_gc (); // . wait_to_proceed(); } bgc_thread_function() { while (1) { // // gc1(); } } gc1() { background_mark_phase(); background_sweep(); }
资源链接