首页 > 代码库 > CLR 内存分配和垃圾收集

CLR 内存分配和垃圾收集

目录

  • 内存分配
  • 垃圾收集
  • 如何分析内存问题
  • 非托管资源
  • 参考文献
  • 注释

 

NET提供了一个运行时环境 CLR, 负责资源管理(内存分配和垃圾收集),通过垃圾回收器(Garbage Collector)—GC,对内存自动回收。

每当您创建新对象时,CLR都会从托管堆为该对象分配内存。 只要托管堆中有地址空间可用,运行时就会继续为新对象分配空间。但是,内存不是无限大的。 最终,垃圾回收器必须执行回收以释放一些内存。 垃圾回收器优化引擎根据正在进行的分配情况确定执行回收的最佳时间。 当垃圾回收器执行回收时,它检查托管堆中不再被应用程序使用的对象,并执行必要的操作来回收它们占用的内存。【1】

 

要更深入了解CLR 内存的管理,需要从内存分配垃圾收集这两方面进行学习。

内存分配:

CLR 初始化之后, 垃圾回收器会分配一段内存用于存储和管理对象。 此内存称为托管堆。【注1】

每当您创建新对象时,CLR都会从托管堆为该对象分配内存。

对象分为大型对象、小型对象两类。如果对象大于或等于 85,000 字节,将被视为大型对象,大型对象通常是字符串,数组

对象存在于托管堆栈段上,托管堆栈段是垃圾回收器通过调用 VirtualAlloc 代表托管代码在操作系统上保留的内存块。

加载 CLR 时,将分配两个初始堆栈段(一个用于小型对象,另一个用于大型对象),我将它们分别称为小型对象堆 (SOH) 和大型对象堆 (LOH)。

然后,通过将托管对象置于任一托管堆栈段上来满足分配请求。如果对象小于 85,000 字节,则将其放在 SOH 段上;否则将其放在 LOH 段上。随着分配到各段上的对象越来越多,会以较小块的形式提交这些段。【2】【4】

 

垃圾收集:

堆上的对象有三代:【4】

  • 第 0 代。 这是最年轻的代,其中包含短生存期对象。 短生存期对象的一个示例是临时变量。 垃圾回收最常发生在此代中。

    新分配的对象构成新一代的对象并且为隐式的第 0 代回收,除非它们是大对象,在这种情况下,它们将进入第 2 代回收中的大对象堆。

    大多数对象通过第 0 代中的垃圾回收进行回收,不会保留到下一代。

  • 第 1 代。 这一代包含短生存期对象并用作短生存期对象和长生存期对象之间的缓冲区。

  • 第 2 代。 这一代包含长生存期对象。 长生存期对象的一个示例是服务器应用程序中的一个包含在进程期间处于活动状态的静态数据的对象。

 

当满足以下条件之一时将发生垃圾回收:

    • 系统具有低的物理内存。

    • 由托管堆上已分配的对象使用的内存超出了每代可接受的阈值。 随着进程的运行,此阈值会不断地进行调整。

    • 调用 GC.Collect 方法。

从分代的角度来说,大型对象属于第 2 代,因为只有在第 2 代回收过程中才能回收它们。回收一代时,同时也会回收所有前面的代。执行第 0 代垃圾回收时,回收第 0代 。执行第 1 代垃圾回收时,将同时回收第 1 代和第 0 代。执行第 2 代垃圾回收时,将回收整个堆,包括大对象。因此,第 2 代垃圾回收也称为完整垃圾回收。

对于 SOH,垃圾回收未处理的对象将进入下一代;由此第 0 代回收未处理的对象将被视为第 1 代对象,依此类推。但是,最后一代回收未处理的对象仍会被视为最后一代中的对象。也就是说,第 2 代垃圾回收未处理的对象仍是第 2 代对象;LOH 未处理的对象仍是 LOH 对象(由第 2 代回收)。用户代码只能在第 0 代(小型对象)或 LOH(大型对象)中分配。只有垃圾回收器可以在第 1 代(通过提升第 0 代回收未处理的对象)和第 2 代(通过提升第 1 代和第 2 代回收未处理的对象)中“分配”对象。
触发垃圾回收后,垃圾回收器将寻找存在的对象并将它们压缩。不过对于 LOH,由于压缩费用很高,目前没有不会压缩 LOH。垃圾回收器选择扫过所有对象,列出没有被清除的对象列表以供以后重新使用,从而满足大型对象的分配请求。相邻的被清除对象将组成一个自由对象。【2】
 
下图是垃圾回收的过程:
SOH 分配和垃圾回收
在第一次第 0 代 GC 后形成了第 1 代,其中 Obj1 和 Obj3 被清除;在第一次第 1 代 GC 后形成了第 2 代,其中 Obj2 和 Obj5 被清除。
 
LOH 分配和垃圾回收
在第 2 代垃圾回收后,您将看到 Obj1 和 Obj2 被清除,内存中原来存放 Obj1 和 Obj2 的空间将成为一个可用空间,随后可用于满足 Obj4 的分配请求。从最后一个对象 Obj3 到此段末尾的空间仍可用于以后的分配请求。
垃圾回收期间在 LOH 上释放的已消除段
如果没有足够的可用空间来容纳大型对象分配请求,会先尝试从操作系统获取更多段。如果失败,将触发第 2 代垃圾回收以便释放一些空间。
在第 2 代垃圾回收期间,会把握时机将不包含任何活动对象的段释放回操作系统(通过调用 VirtualFree)。
从最后一个存在的对象到该段末尾的内存将退回。而且,尽管已重置可用空间,但仍会提交它们,这意味着操作系统无需将其中的数据重新写入磁盘。
下图  说明了一种情况,将一个段(段 2)释放回操作系统,并在剩下的段中退回了更多空间。如果需要使用该段末尾的已退回空间来满足新的大型对象分配请求,可以再次提交该内存

如何分析内存问题:

首先理解几个内存指数概念:

Total reserved Bytes:托管堆保留的字节数。当 GC 分配一个新堆段时,内存将保留给该段,保留内存不需要操作系统提供物理内存。只有在需要时才提供内存。因此保留字节的总数可以比提供的字节总数大。

Total committed Bytes:托管堆提供的字节数。在 GC(垃圾收集器)提供物理内存时,会真正分配物理内存。可以用来衡量托管堆的大小。略微大于实际的第 0 级堆大小 + 第 1 级堆大小 + 第 2 级堆大小 + 大型对象堆大小。

Gen 0 heap size :第 0 级中可以分配的最大字节数,并非第 0 代中使用的实际内存,而是其预算值。

Gen 1 heap size:第 1 级中的当前字节数。

Gen 2 heap size: 第 2 级中的当前字节数。

Large Object Heap size:大对象堆的当前字节数

Bytes in all Heaps:所有堆中的字节数。Framework 2.0版本中,是上面4个值的总和。Framework 4.0以后,等于Gen 1 heap size+Gen 2 heap size+Large Object Heap size

% Time in GC(GC 中时间的百分比):显示自上次垃圾回收周期后执行垃圾回收所用运行时间的百分比。如果这个值过高,可能 会引起系统性能下降,大部分时间都花在GC收集上面了。

                                                           10%以下是一个比较平稳的参考值。

Allocated Bytes/second:每秒在垃圾回收堆上分配的字节数。

以上的内存指数,可以在内存性能计数器上【5】,获取相关数据。

通常,先通过这些计数器,收集必要的数据以确定出现问题的准确位置。然后分析转储文件DUMP,定位哪些对象占了过多的空间,找到这个对象引用的根。

碎片是否过多。对于第 0 代,碎片不构成问题,因为 GC 可以在碎片空间中进行分配。对于第 1 代和第 2 代,碎片可能会造成问题。要在第 1 代和第 2 代中使用碎片空间,GC 必须收集和提升对象以填补这些间隙。但由于第 1 代的大小不会超过一个段,因此通常需要关注的是第 2 代。

 

非托管资源:

非托管资源有两种释放方式:

1:显式释放,代码中通过调用Dispose()方法显式释放非托管资源

2:开发人员,可能会忘记调用Dispose()方法。对实现了析构函数的对象,CLR会在一个名叫 终结器队列(Finalization Queue )的地方增加一个指向该对象的引用。

    GC时,将不活动动的对象,从Finalization Queue 移除,并加到另一个可终结对象队列中。

    有一个终结器线程,会处理可终结对象对列。下次GC 的时候(并不一定是下一次垃圾回收),调用对象的Finalize() 方法释放非托管资源,将此对象从可终结对象队列中移除。

    用windbg 可以用 !finalizequeue  查看准备终结的对象数 。排查是否有过多非托管资源没被释放。

    !threads-special 找到终结器线程,查看其状态是否正常。

 

参考文献:

【1】http://msdn.microsoft.com/zh-cn/library/0xy59wtx%28v=vs.110%29.aspx 垃圾回收

【2】http://msdn.microsoft.com/zh-cn/magazine/cc534993.aspx#id0070002 大型对象堆揭秘

【3】http://msdn.microsoft.com/zh-cn/magazine/cc163528.aspx 研究内存问题

【4】http://msdn.microsoft.com/zh-cn/library/ee787088%28v=vs.110%29.aspx 垃圾回收基础

【5】http://msdn.microsoft.com/zh-cn/library/x2tyfybc%28v=vs.100%29.aspx 内存性能计数器

 

注释:

【注1】 垃圾回收器为你分配和释放是托管堆上的虚拟内存。

【注2】虚拟内存有三种状态

  • 可用。 该内存块没有引用关系,可用于分配。

  • 保留。 内存块可供你使用,并且不能用于任何其他分配请求。 但是,在该内存块提交之前,你无法将数据存储到其中。

  • 提交。 内存块已指派给物理存储。

CLR 内存分配和垃圾收集