CLR 内存分配和垃圾收集 GC
目录
- 内存分配
- 垃圾收集
- 如何分析内存问题
- 非托管资源
- 参考文献
- 注释
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 代垃圾回收也称为完整垃圾回收。
如何分析内存问题:
首先理解几个内存指数概念:
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】虚拟内存有三种状态
-
可用。 该内存块没有引用关系,可用于分配。
-
保留。 内存块可供你使用,并且不能用于任何其他分配请求。 但是,在该内存块提交之前,你无法将数据存储到其中。
-
提交。 内存块已指派给物理存储。