.Net 垃圾回收和大对象处理

英文原文:Maoni Stephens,编译:赵玉开(@玉开Sir)

CLR垃圾回收器根据所占空间大小划分对象。大对象和小对象的处理方式有很大区别。比如内存碎片整理 —— 在内存中移动大对象的成本是昂贵的,让我们研究一下垃圾回收器是如何处理大对象的,大对象对程序性能有哪些潜在的影响。

大对象堆和垃圾回收

在.Net 1.0和2.0中,如果一个对象的大小超过85000byte,就认为这是一个大对象。这个数字是根据性能优化的经验得到的。当一个对象申请内存大小达到这个阈值,它就会被分配到大对象堆上。这意味着什么呢?要理解这个,我们需要理解.Net垃圾回收机制。

如大多人所知道的,.Net GC是按照“代”来回收的。程序中的对象共有3代,0代、1代和2代,0代是最年轻的对象,2代对象存活的时间最长。GC按代回收垃圾也是出于性能考虑的;通常的对象都会在0代是被回收。例如,在一个asp.net程序中,和每一个请求相关的对象都应该在请求结束时回收掉。而没有被回收的对象会成为1代对象;也就是说1代对象是常驻内存对象和马上消亡对象之间的一个缓冲区。

从代的角度看,大对象属于2代对象,因为只有在2代回收时才会处理大对象。当某代垃圾回收执行时,会同时执行更年轻代的垃圾回收。比如:当1代垃圾回收时会同时回收1代和0代的对象,当2代垃圾回收时会执行1代和0代的回收.

代是垃圾回收器区分内存区域的逻辑视图。从物理存储角度看,对象分配在不同的托管堆上。一个托管堆(managed heap)是垃圾回收器从操作系统申请的内存区(通过调用windows api VirtualAlloc)。当CLR载入内存之后,会初始化两个托管堆,一个大对象堆(LOH –large object heap)和一个小对象对(SOH – small object heap)。

内存分配请求就是将托管对象放到对应的托管堆上。如果对象的大小小于85000byte,它会被放置在SOH;否则会被放在LOH上。

对于SOH,对象在执行一次垃圾回收之后,会进入到下一代。也就是说如果在第一次执行垃圾回收时,存活下来的对象会进入第二代,如果在第2次垃圾回收之后该对象仍然没有被当作垃圾回收掉,它就会成为2代对象;2代对象就是最老的对象不会在提升代数。

当触发垃圾回收时,垃圾回收器会在小对象堆做碎片整理,将存活下来的对象移动到一起。而对于大对象堆,由于移动内存的开销很大,CLR团队选择只是清除它们,将回收掉的对象组成一个列表,以便满足下次有大对象申请使用内存,相邻的垃圾对象会被合并成一块空闲的内存块。

需要时时留意的是,直到.Net 4.0中也不会对大对象堆做碎片整理操作,将来也许会做。因此如果你要分配大对象并不想他们被移动,你可以使用fixed语句。

如下小对象堆SOH的回收示意图

上图中第一次垃圾回收之前有四个对象obj0-3;在第一垃圾回收之后obj1和obj3被回收了,同时obj2和obj0移动到一起了;在第二次垃圾回收之前有分配了三个对象obj4-6;在第二次执行垃圾回收之后obj2和obj5被回收了,obj4和obj6被移动到obj0旁边。

下图是大对象堆LOH回收示意图

可以看到在未执行垃圾回收之前,一共有四个对象obj0-3;第一次二代垃圾回收之后obj1和obj2被回收掉了,回收掉之后obj1和obj2所占空间被合并到了一起,在obj4申请分配内存时就把obj1和obj2回收后释放的空间分配给它了;同时留下了一块内存碎片。如果这个碎片的大小小于85000byte,那么这个碎片就在这个程序的生命周期中永远不能被再次利用了。

如果大对象堆上没有足够的空闲内存容纳要申请的大对象空间,CLR首先会尝试向操作系统申请内存,如果申请失败,就会触发一次二代回收来尝试释放一些内存。

在2代垃圾回收时,可以将不需要的内存通过VirtualFree交还给操作系统。交还的过程参见下图:

什么时候回收大对象呢?

在讨论什么时候回收大对象之前先来看下普通的垃圾回收操作什么时机执行吧。垃圾回收在下列情况下发生:

1. 申请的空间超过0代内存大小或者大对象堆的阈值,多数的托管堆垃圾回收在这种情况下发生

2. 在程序代码中调用GC.Collect方法时;如果在调用GC.Collect方法是传入GC.MaxGeneration参数时,会执行所有代对象的垃圾回收,包括大对象堆的垃圾回收

3. 操作系统内存不足时,当应用程序收到操作系统发出的高内存通知时

4. 如果垃圾回收算法认为做二代回收是有收效时会触发二代垃圾回收

5. 每一代对象堆的都有一个所占空间大小阈值的属性,当你分配对象到某一代,你增长了内存总量接近了该代的阈值,或者分配对象导致这一代的堆大小超过了堆阈值,就会发生一次垃圾回收。因此当你分配小对象或者大对象时,会对应消耗0代堆或者大对象堆的阈值。当垃圾回收器将对象代数提升到1代或者2代时,会消耗1、2代的阈值。在程序运行中这些阈值是动态变化的。

大对象堆性能影响

让我们先看下分配大对象的代价。 CLR为每个新对象分配内存时都要保证这些内存清空的,是没有被其他对象使用的(I give out is cleared)。这就意味着分配的代价完全被清理(clearing)的代价控制着(除非在分配时触发了一次垃圾回收)。如果清空1byte需要2个周期(cycles),就意味着清除一个最小的大对象需要170,000个周期。通常情况下人们不会分配超大的对象,比如说在2GHz的机器上分配16M大小的对象,大约需要16ms来清空内存。这代价太大了。

让我们在看下回收的代价。前面提到过,大对象和2代龄对象一起回收。如果大对象或者2代对象占用空间超过其阈值时,就会触发2代对象的回收。如果2代回收因为大对象堆超过阈值被触发,2代对象堆本身没有多少对象可以做回收。如果在2代堆上没有多少对象,这问题不大。但是如果2代堆很大对象很多,过多的2代回收就会导致性能问题。如果是临时性的分配大对象,就需要很多的时间来运行垃圾回收;也就是说如果你持续的使用大对象然后又释放大对象对性能会有很大的负面影响。

大对象堆上的巨大对象通常是数组(很少有一个对象很大的情况)。如果对象中的元素是强引用,代价会很高;如果元素之间没有相互引用,垃圾回收时就不需要遍历整个数组。例如:用一个数组来保存二叉树的节点,一种方法是在节点中强引用左右节点:

class Node
{
Data d;
Node left;
Node right;
}

Node[] binaryTree = new Node[num_nodes];

如果num_nodes是一个很大的数字,就意味着每个节点都至少需要查看二个引用元素。一种替代方案是在节点中保存左右节点元素的数组索引号

class Node
{
Data d;
uint left_index;
uint right_index;
}

这样的话,元素之间的引用关系去掉了;可以通过binaryTree[left_index]来获得引用的节点。垃圾回收器在做垃圾回收时也不需要看相关的引用元素了。

为大对象堆收集性能数据

有几种方法可以收集大对象堆相关的性能数据。在我解释这些方法之前,让我们先谈一下为什么需要收集大对象堆相关的性能数据。

在你开始上搜集某个方面的性能数据时,有可能你已经找到这方面造成性能瓶颈的证据;或者你已经没有找遍了所有方面都没有发现问题。

在查找性能问题时.Net CLR Memory 性能计数器通常是应该先考虑使用的工具。和LOH相关的计数器有generation 2 collectioins(2代堆收集次数)和large object heap size大对象堆大小。Generation 2 collections显示的是进程启动之后2代垃圾回收操作发生的次数。Large object heap size计数器显示的是当前大对象堆的大小值,包括空闲空间;这个计数器是在每次垃圾回收操作之后做更新,并非每次分配内存都做更新。

可以参考下图在windows性能计数器中观察.Net CLR Memory相关性能数据



你也可以通过程序查询这些计数器的值;很多人通过程序的方式收集性能计数器来帮助查找性能瓶颈。

当然也可以使用调试器winddbg观察大对象堆。

最后提示一下:到目前为止,大对象堆作为垃圾回收的一部分是不做内存碎片整理的,但是这个只是一个clr的实现细节,程序代码不应该依赖这个特点。如果要确保对象不会被垃圾回收器移动,就要使用fixed语句。

原文地址:http://blog.jobbole.com/31459/

时间: 2024-10-28 22:06:22

.Net 垃圾回收和大对象处理的相关文章

.Net 垃圾回收和大对象处理 内存碎片整理

CLR垃圾回收器根据所占空间大小划分对象.大对象和小对象的处理方式有很大区别.比如内存碎片整理 -- 在内存中移动大对象的成本是昂贵的,让我们研究一下垃圾回收器是如何处理大对象的,大对象对程序性能有哪些潜在的影响. 大对象堆和垃圾回收 在.Net 1.0和2.0中,如果一个对象的大小超过85000byte,就认为这是一个大对象.这个数字是根据性能优化的经验得到的.当一个对象申请内存大小达到这个阀值,它就会被分配到大对象堆上.这意味着什么呢?要理解这个,我们需要理解.Net垃圾回收机制. 如大多人

.Net 垃圾回收机制原理(二)

英文原文:Jeffrey Richter 编译:赵玉开 链接http://www.cnblogs.com/yukaizhao/archive/2011/11/25/dot_net_GC_2.html 上一篇文章介绍了.Net 垃圾回收的基本原理和垃圾回收执行Finalize方法的内部机制:这一篇我们看下弱引用对象,代,多线程垃圾回收,大对象处理以及和垃圾回收相关的性能计数器. 让我们从弱引用对象说起,弱引用对象可以减轻大对象带来的内存压力.弱引用(Weak References) 当程序的根对象

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

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

asp.net C#基础知识之垃圾回收机制介绍

第一节 垃圾回收机制早期的C/C++开发中,一个对象的生命周期大概像这样:计算对象大小--查找可用内存--初始化对象--使用对象--摧毁对象.如果在上面的过程中,开发人员忘记了"摧毁对象"这一步骤,则很有可能导致内存泄露!这是一个非常可怕的事情!幸好,CLR的开发人员为我们解决了这一问题,在.NET Framework中引入了垃圾回收机制,使得开发人员不需要再过多地关注内存释放的问题,CLR会在合适的时候进行执行垃圾回收来释放不再使用的内存.这里就像一个邪恶的男人所说的话:给我一个女人

Java 的垃圾回收机制(转)

先看一段转载,原文出自 http://jefferent.iteye.com/blog/1123677 虚拟机中的共划分为三个代:年轻代(Young Generation).年老点(Old Generation)和持久代(Permanent Generation).其中持久代主要存放的是Java类的类信息,与垃圾收集要收集的Java对象关系不大.年轻代和年老代的划分是对垃圾收集影响比较大的. 年轻代: 所有新生成的对象首先都是放在年轻代的.年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象.

浅谈Python的垃圾回收机制_python

一.垃圾回收机制 Python中的垃圾回收是以引用计数为主,分代收集为辅.引用计数的缺陷是循环引用的问题. 在Python中,如果一个对象的引用数为0,Python虚拟机就会回收这个对象的内存. #encoding=utf-8 __author__ = 'kevinlu1010@qq.com' class ClassA(): def __init__(self): print 'object born,id:%s'%str(hex(id(self))) def __del__(self): pr

.net垃圾回收和CLR 4.0对垃圾回收所做的改进之二

A survey of garbage collection and the changes CLR 4.0 brings in Part 2 - series of what is new in CLR 4.0 接前篇Continue the previous post .net垃圾回收和CLR 4.0对垃圾回收所做的改进之一 CLR4.0所带来的变化仍然没有在这篇,请看下篇. 内存释放和压缩 创建对象引用图之后,垃圾回收器将那些没有在这个图中的对象(即不再需要的对象)释放.释放内存之后, 出

垃圾回收机制与引用类型

 Java语言的一个重要特性是引入了自动的内存管理机制,这样一来,开发人员就不需要自己来管理应用中的内存了.C/C++开发人员需要通过malloc/free和new/delete等函数来显式的分配和释放内存.这对开发人员提出了比较高的要求,因为这些方法使用不当很容易造成内存访问错误和内存泄露等严重问题.一个最常见的问题是 "悬挂引用(dangling references)",即一个引用或指针所指向的内存区块已经被错误的回收并重新分配给新的对象了,如果程序继续使用这个引用或指针的话会,

各位大神有空的来探讨下java的垃圾回收机制吧

问题描述 1.java的对象不一定会被gc回收.问下不用new关键字创造的特殊对象有哪几种?Strings="ss";算吗?字符串池不在gc的回收范围吗?2.finalized()并不是c++的析构方法?为什么调用System.gc()不一定要调用finalized()?在这里对象的终结条件怎么理解?怎么自己重写finalized()?将将回收的对象的引用置为null?将流引用的文件关闭?3.为啥说java的gc不能完全代替c的析构?能探讨下gc的两种工作"暂停-复制&quo