CLR垃圾回收的设计

CLR垃圾回收的设计

作者: Maoni Stephens (@maoni0) - 2015

附: 关于垃圾回收的信息,可以参照本文末尾资源章节里引用的垃圾回收手册一书。

组件架构

GC包含的两个组件分别是内存分配器和垃圾收集器。内存分配器负责获取更多的内存并在适当的时候触发垃圾收集。垃圾收集器回收程序中不再使用的对象的内存。

有多种方法调用垃圾回收器,例如人工调用GC.Collect或者当终结线程在接收到表示低内存的异步通知时(调用)。

内存分配器的设计

内存分配器由执行引擎(EE)的内存分配辅助函数调用,并附上下列信息:

  • 请求的大小
  • 线程分配上下文
  • 一个说明该对象是否可终结的标识。

GC不会区别对待不同的对象。请通过执行引擎来获取对象的大小。

基于对象的大小,GC将其分成两类:小对象(< 85,000字节)和大对象(>= 85,000字节)。原则上,大小对象都可以同样处理,但是压缩大对象耗费更加昂贵所以GC才这样区分。

GC向内存分配器释放内存是通过内存分配上下文完成的。内存上下文的大小有分配额度定义:

  • 内存分配上下文(Allocation contexts)是线程专用的堆区(heap segment)上小一点的区域。在单处理器(即一个逻辑处理器)机器上,使用单上下文,也就是第0代内存分配器上下文。
  • 内存分配定额(Allocation quantum)是分配器在一个内存分配上下文中执行对象分配时要求更多内存时的分配定额。这个定额通常是8k,而托管对象的平均大小大约是35个字节,这样在一个分配额度里可以满足很多对象的分配请求。

大对象不使用分配上下文和定额。一个大对象本身就比这些小内存区域(8k的定额)大了。而且,这些区域的优点(下文讨论)直适用于小对象。大对象就直接在堆区上分配了。

分配器的设计目标如下:

  • 在适当的时候触发GC: 分配器在超出分配预算(由收集器设置的一个阈值)时,或者分配器无法在堆区上分配时触发GC。后文会详细介绍分配预算和托管堆区。
  • 保留对象的本地性: 在同一个堆区上分配的对象,保存它们的虚拟内存地址也是挨着的。
  • 提高缓存的效率: 分配器以 分配定额 为单位分配内存,而按不是一个个对象分配。其将这些内存置零来便CPU的缓存提前做准备,因为随后马上就有对象在这块内存中创建。分配定额通常是8k。
  • 提高锁的效率: 内存分配上下文的线程关联性和定额保证有且只有一个线程写入指定的定额分配的内存。结果就是只要当前的内存分配上下文没有用光的话,对象分配是不需要加锁的。
  • 内存完整性: 对于新创建的对象,GC总是将内存置零,以防止对象引用了随机的内存位置。
  • 保持堆的可遍历性: 分配器保证定额的剩余内存是一个空闲对象。例如,如果定额里只剩下30个字节,而下一个要分配的对象大小是40字节,分配器为这30个字节创建一个空闲对象并申请一个新的分配定额。

内存分配 APIs

 Object* GCHeap::Alloc(size_t size, DWORD flags);
 Object* GCHeap::Alloc(alloc_context* acontext, size_t size, DWORD flags);

上面的函数可以用来分配大对象和小对象。也有一个对象可以直接在大对象堆里分配内存:

 Object* GCHeap::AllocLHeap(size_t size, DWORD flags);

收集器的设计

GC的目标

GC将极其高效利用内存和尽量避免编写“托管代码”的程序员的人工干预作为奋斗目标。高效是指:

  • GC应该足够频繁发生,以避免托管堆上有大量(按比率或者绝对值)已分配的但无用的对象(垃圾),导致非必要的使用内存。
  • GC应该尽量不频繁发生,避免占有有用的CPU时间,哪怕在低内存导致的频繁GC。
  • GC应该有高效产出。如果GC只回收了一小部分内存,那么GC(包括其使用的CPU周期)都是浪费的。
  • 每次GC应该尽量快。很多工作负荷要求低延迟。
  • 托管代码程序员应该不需要知道GC的太多细节而能达到高效的内存使用率。
  • GC应该自我调整以满足不同的内存使用模式。

托管堆的逻辑形式

CLR GC是一个分代收集器,即对象是逻辑划分成几个代的。当第 N 代收集完毕后,剩下来的存活对象则被标识为第 N+1 代。这个过程被称作升级。也有异常情况我们决定降级或者不升级。

小对象堆被分成3代:gen0, gen1和gen2。大对象只有一代 - gen3。gen0和gen1被称为短命代(对象存活的时间不长)。

对于小对象堆,代的数字表示它的年龄 - gen0属于最年轻的一代。这不是说gen0里所有的对象比gen1或gen2中任意一个对象年轻。后文会提到一些异常情形。收集一代是指收集这一代和所有比其年轻的代。

原则上大对象可以使用跟小对象相同的办法处理,但是压缩大对象的代价很高,才区别对待。出于性能的考量,大对象只有一代而且总是跟gen2一起收集。gen2和gen3可以很大,但是收集短命代(gen0和gen1)的成本有限制。

内存分配是在最年轻的代发生的 - 对小对象来说总是gen0,而对大对象来说是gen3,因为只有一代。

托管堆的物理形式

托管堆是一系列的托管堆区。一个托管堆区是GC从操作系统那里申请的一个连续的内存区域。堆区被分成大小对象区,对应大小对象。每个堆的堆区都链在一起。至少有一个小对象堆区和一个大对象堆区 - 用来为加载CLR而保留。

每个小对象堆总是只有一个短命区,用来保存gen0和gen1代。这个堆区有可能包含gen2的对象。除了短命区以外,有可能有零个、一个或多个额外的堆区,用来作为gen2堆区并保存gen2对象。

在大对象堆上有一个或多个堆区。

堆区的使用是从低地址开始到高地址,即堆区里低地址对象的时间比高地址对象久。同样下文也有一些异常情况。

堆区可以按需申请,如果其不包含存活对象就会被删除,但是堆上初始的第一个堆区一直都在。对于每个堆,一次申请一个堆区,这个在给小对象做垃圾回收时和创建大对象时发生。这样做有更好的性能,因为大对象只会跟gen2一起回收(执行起来代价更高)。

堆区按照申请的顺序链接在一起。链表上最后一个堆区永远是短命区。回收过的堆区(没有存活对象)会被复用而不是直接被删除,也就是变成新的短命区。堆区复用只发生在小对象堆。每当分配一个大对象,会考虑整个大对象堆。而小对象的分配只考虑短命区。

分配预算

分配预算是跟每个代关联的逻辑概念。这是代里的一个大小限制用来在超出时触发一个GC。

预算是设置在代上基于该代对象存活率的一个属性。如果存活率高,那么预算就会大一些,这样在下一次GC的时候销毁的对象和存活的对象有一个更好的比率。

确定回收哪一代

当触发一个GC时,GC必须决定回收哪一代。除了分配预算以外还要考虑以下几个因素:

  • 代上碎片情况 - 如果代上的内存碎片很严重,那么在这个代上回收产量可能很高。
  • 如果机器上内存负荷很大,那么GC会更积极的回收来产生更多的可用空间。这对避免不必要的页面调度很重要。
  • 如果短命堆区没有空间的话,GC会更积极的回收短命对象(更多的gen1回收)来避免申请一个新的堆区。

GC的流程

标注阶段

标注阶段的目标是找出所有存活的对象。

按代回收的好处是只需要考虑堆的一部分而不是每次都处理所有对象。当回收短命代时,GC只需要找到这一个代里存活的对象,这些信息由执行引擎上报。除了执行引擎可能引用对象以外,更老一代的对象也可能会引用新一代的对象。

对于GC使用卡片来标注更老的代。卡片是由JIT辅助函数在分配操作时设置的。如果JIT辅助函数看到一个对象在短命区的范围,然后设置包含卡片的字节来指示其来源位置。在收集短命区时,GC可以在看堆上设置过的卡片并依次处理卡片对应的对象即可。

计划阶段

计划阶段模拟压缩过程来决定最后的效果,如果压缩效果很好那么GC就会启动压缩,否则执行清理。

迁移阶段

如果GC决定压缩,其结果会移动对象,那么对这些对象的引用必须更新。迁移阶段需要处理所有指向所回收的代中的对象的引用。相比之下,而标注阶段只处理存活对象因此不需要考虑弱引用(weak reference)。

压缩阶段

这个阶段很直观,因为在计划阶段就已经计算对象应该移动的新地址,压缩阶段只需要将对象拷贝过去。

清理阶段

清理阶段会查看两个存活对象之间的空间。其为这些空间创建闲置对象。相邻的闲置对象会合并。它会将所有的闲置对象保存在 闲置对象列表(freelist)

代码流程

术语:

  • WKS GC: 工作站 GC.
  • SRV GC: 服务器 GC

功能行为

WKS GC并关闭了并行GC

  1. 用户线程用完了分配预算并触发一个GC。
  2. GC调用SuspendEE来暂停托管线程。
  3. GC决定回收哪一代。
  4. 执行标注阶段。
  5. 执行计划阶段并决定是否要执行压缩。
  6. 如果要压缩则执行迁移和压缩过程。否则执行清理过程。
  7. GC调用RestartEE来恢复托管线程。
  8. 用户线程恢复执行。

WKS GC并打开了并行GC

这些说明了一个后台GC是如何实施的:

  1. 用户线程用完了分配预算并触发一个GC。
  2. GC调用SuspendEE来暂停托管线程。
  3. GC决定是否需要后台GC运行。
  4. 如果需要后台GC,唤醒它。后台GC线程调用RestartEE来恢复托管线程的执行。
  5. 托管线程在后台GC执行的同时运行并分配内存。
  6. 用户线程可能会用完分配预算并触发一个短命代GC(我们称之为前台GC)。这个过程跟“WKS GC并关闭了并行GC”一样。
  7. 后台GC再次调用SuspendEE来完成标注并调用RestartEE来在用户线程运行的同时并行执行清理阶段。
  8. 后台GC处理完毕.

SVR GC并关闭了并行GC

  1. 用户线程用完了分配预算并触发一个GC。
  2. 服务器GC线程被唤醒冰调用SuspendEE来暂停托管线程。
  3. 服务器GC线程执行GC工作(与WKS GC并关闭了并行GC一样).
  4. 服务器GC线程调用RestartEE来恢复托管线程。
  5. 用户线程恢复执行。

SVR GC并打开了并行GC

这个场景跟WKS GC并打开了并行GC一样,除了在服务器GC线程上没有后台GC。

物理架构

这个章节用来帮助你理解代码过程。

用户线程用完定额之后,通过try_allocate_more_space申请新定额。

try_allocate_more_space在需要触发GC时调用GarbageCollectGeneration。

假如WKS GC并关闭了并行GC,GarbageCollectGeneration在触发GC的用户线程上执行,代码过程如下:

 GarbageCollectGeneration()
 {
     SuspendEE();
     garbage_collect();
     RestartEE();
 }

 garbage_collect()
 {
     generation_to_condemn();
     gc1();
 }

 gc1()
 {
     mark_phase();
     plan_phase();
 }

 plan_phase()
 {
     // actual plan phase work to decide to
     // compact or not
     if (compact)
     {
         relocate_phase();
         compact_phase();
     }
     else
         make_free_lists();
 }

假如WKS GC并打开了并行GC(默认情况),后台GC的代码过程如下:

 GarbageCollectGeneration()
 {
     SuspendEE();
     garbage_collect();
     RestartEE();
 }

 garbage_collect()
 {
     generation_to_condemn();
     // decide to do a background GC
     // wake up the background GC thread to do the work
     do_background_gc();
 }

 do_background_gc()
 {
     init_background_gc();
     start_c_gc ();

     //wait until restarted by the BGC.
     wait_to_proceed();
 }

 bgc_thread_function()
 {
     while (1)
     {
         // wait on an event
         // wake up
         gc1();
     }
 }

 gc1()
 {
     background_mark_phase();
     background_sweep();
 }

资料

时间: 2024-10-10 15:29:21

CLR垃圾回收的设计的相关文章

CLR 垃圾回收算法

c#相较于c,c++而言,在内存管理上为程序员提供了极大的方便,解放了程序员与内存地址打交道,提高了程序员的工作效率.比如c中分配的malloc堆空间没有释放导致的内存泄露,数组越界导致的踩内存错误,使用了已释放的内存空间错误等等.这些在C#中统统的都不存在,主要是由于clr提供的安全检查机制以及垃圾回收机制.本篇文章主要来介绍常用的垃圾回收算法以及CLR中使用的垃圾回收算法. 在通常的情况下当分配对象时发现内存堆空间不足时,此时GC会执行垃圾回收算法.默认情况下,进程启动,会被分配相应的堆空间

应用程序集和CLR 垃圾回收(二)

本篇是整理蒋金楠对CLR 内存管理的博文,蒋大神的博文中将CLR 的内存分管理分为三个逻辑部分,博文中详细讲述了哪些程序集要加载到系统程序域,哪些要加载到共享程序域,以及我们写的代码会被加载到默认程序域.下面是我整理后的思路,目的是加强一下对CLR 内存管理的概念. 程序集与应用程序域 程序集是一个托管应用的基本的部署单元.一个程序集是子描述的(通过元数据),能够实施版本策略和部署策略.从结构组成来看,一个程序集主要由三个部署组成:IL指令.元数据和资源. 应用程序域从功能上看是,通过应用程序域

垃圾回收机制GC知识再总结兼谈如何用好GC(其他信息: 内存不足)

来源 一.为什么需要GC 应用程序对资源操作,通常简单分为以下几个步骤: 1.为对应的资源分配内存 2.初始化内存 3.使用资源 4.清理资源 5.释放内存 应用程序对资源(内存使用)管理的方式,常见的一般有如下几种: 1.手动管理:C,C++ 2.计数管理:COM 3.自动管理:.NET,Java,PHP,GO- 但是,手动管理和计数管理的复杂性很容易产生以下典型问题: 1.程序员忘记去释放内存 2.应用程序访问已经释放的内存 产生的后果很严重,常见的如内存泄露.数据内容乱码,而且大部分时候,

垃圾回收机制GC知识再总结兼谈如何用好GC

一.为什么需要GC 应用程序对资源操作,通常简单分为以下几个步骤: 1.为对应的资源分配内存 2.初始化内存 3.使用资源 4.清理资源 5.释放内存 应用程序对资源(内存使用)管理的方式,常见的一般有如下几种: 1.手动管理:C,C++ 2.计数管理:COM 3.自动管理:.NET,Java,PHP,GO… 但是,手动管理和计数管理的复杂性很容易产生以下典型问题: 1.程序员忘记去释放内存 2.应用程序访问已经释放的内存 产生的后果很严重,常见的如内存泄露.数据内容乱码,而且大部分时候,程序的

.NET垃圾回收机制(二)

一.GC的必要性 1.应用程序对资源操作,通常简单分为以下几个步骤:为对应的资源分配内存 → 初始化内存 → 使用资源 → 清理资源 → 释放内存. 2.应用程序对资源(内存使用)管理的方式,常见的一般有如下几种: [1] 手动管理:C,C++ [2] 计数管理:COM [3] 自动管理:.NET,Java,PHP,GO- 3.但是,手动管理和计数管理的复杂性很容易产生以下典型问题: [1] 程序员忘记去释放内存 [2] 应用程序访问已经释放的内存 产生的后果很严重,常见的如内存泄露.数据内容乱

Clr Via C#读书笔记---垃圾回收机制

#1 垃圾回收平台的基本工作原理: 访问一个资源所需的具体步骤: 1)调用IL指令newobj,为代表资源的类型分配内存.在C#中使用new操作符,编译器就会自动生成该指令.2)初始化内存,设置资源的初始状态,使资源可用.类型的实例构造器负责设置该初始状态.3)访问类型的成员(可根据需要反复)来使用资源.4)摧毁资源的状态以进行清理.正确清理资源的代码要放在Finalize, Dispose和Close方法.5)释放内存.垃圾回收器独自负责这一步. 托管堆如何知道应用程序不再用一个对象? 托管堆

[CLR via C#]21. 自动内存管理(垃圾回收机制)

目录 理解垃圾回收平台的基本工作原理 垃圾回收算法 垃圾回收与调试 使用终结操作来释放本地资源 对托管资源使用终结操作 是什么导致Finalize方法被调用 终结操作揭秘 Dispose模式:强制对象清理资源 使用实现了Dispose模式的类型 C#的using语句 手动监视和控制对象的生存期 对象复活 代 线程劫持 大对象 一.理解垃圾回收平台的基本工作原理 值类型(含所有枚举类型).集合类型.String.Attribute.Delegate和Event所代表的资源无需执行特殊的清理操作.

重温CLR(十五) 托管堆和垃圾回收

本章要讨论托管应用程序如何构造新对象,托管堆如何控制这些对象的生存期,以及如何回收这些对象的内存.简单地说,本章要解释clr中的垃圾回收期是如何工作的,还要解释相关的性能问题.另外,本章讨论了如何设计应用程序来最有效地使用内存. 托管堆基础 每个程序都要使用这样或那样的资源,包括文件.内存缓冲区.屏幕空间.网络连接.数据库资源等.事实上,在面向对象的环境中,每个类型都代表可提供程序使用的一种资源.要使用这些资源,必须为代表资源的类型分配内存.以下是访问一个资源所需的步骤 1 调用IL指令newo

对CLR基本原理概念&amp;垃圾回收机制的简单理解

PS,之前有说过C语言的函数&变量的一些基本概念,说得可能不是很好,先也把C#的.里相关的也说下,已成一统. 而说函数变量,其实主要就是GC,而GC又是CLR的主要内容,故就有了此文. CLR基本原理: 把这几个概念一说基本就知道了,其实就是为了跨语言.跨平台,和JAVA的JVM类似 1.MSIL,中间语言,就是独立于所在平台系统的.net的特殊代码.里面含有相关元数据信息,常用的反编译工具想ILspy等就是靠他吃饭的. 2.CLR,公共语言运行时,其实就是把VB.VC.C#等不同语言编译成同一