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】虚拟内存有三种状态

  • 可用。 该内存块没有引用关系,可用于分配。
  • 保留。 内存块可供你使用,并且不能用于任何其他分配请求。 但是,在该内存块提交之前,你无法将数据存储到其中。
  • 提交。 内存块已指派给物理存储。
时间: 2024-08-03 18:14:44

CLR 内存分配和垃圾收集的相关文章

垃圾收集器与内存分配策略-垃圾收集器

(A).图中展示了7种不同分代的收集器: Serial.ParNew.Parallel Scavenge.Serial Old.Parallel Old.CMS.G1: (B).而它们所处区域,则表明其是属于新生代收集器还是老年代收集器:       新生代收集器:Serial.ParNew.Parallel Scavenge:       老年代收集器:Serial Old.Parallel Old.CMS:       整堆收集器:G1: (C).两个收集器间有连线,表明它们可以搭配使用:

JVM内存分配和垃圾收集策略

java内存区域 程序计数器 因为java可以多线程并发执行,因此,为了线程切换后能恢复到正确的执行位置,每个线程都需要一个独立的程序计数器.记录正在执行的虚拟机字节码指令的地址. 这个区域不会产生内存溢出异常. 栈 java虚拟机栈 栈中主要存放了编译期可知的四类八种基本数据类型存(逻辑型 boolean.文本型char.整数型byte.short.int.float.浮点数型double.long),对象引用类型,和对象引用类型(reference). 本地方法栈 本地方法栈和java虚拟机

垃圾收集器与内存分配策略

①对于java虚拟机来说,垃圾收集器主要关注的内存区域是 堆和方法区. ②垃圾收集器就是要收集那些已经“死了”的对象.如果判断一个对象是否存活? 对象引用计数法 对象引用增加一个,那么相应的计数器加1,否则,减1. 优点:实现简单 缺点:不能处理对象间的循环引用.a引用b,b同时引用a. 可达性分析 如果节点到root节点可达,则证明是存活的:否则,已死.所以对于下图的o5,o6,o7虽然他们是循环引用的,但是到root节点无可达,所以已死可清除. ③垃圾回收器对于不同类型引用的回收规则 强引用

垃圾收集器以及内存分配策略

垃圾回收 垃圾回收的三个问题: 哪些内存需要回收? 什么时候回收? 如何回收? 1.哪些对象需要回收? 判断对象是否存活的办法: 引用计数算法:给对象中添加一个引用计数器,有一个地方引用就+1,引用失效就-1.只要计数器为0则对象已死. 优点:简单易实现: 缺点:无法解决对象之间相互引用的问题.(JVM也因为此种原因没有使用它) 根搜索算法: 通过选取出一个GC Roots对象,已它作为起始点,如果对象不可达,则对象已死. GC Roots对象: 虚拟机栈中引用的对象 方法区中类静态属性引用的对

二、Java如何分配和回收内存?Java垃圾收集器如何工作?

线程私有的内存区域随用户线程的结束而回收,内存分配编译期已确定,内存分配和回收具有确定性.共享线程随虚拟机的启动.结束而建立和销毁,在运行期进行动态分配.垃圾收集器主要对共享内存区域(堆和方法区)进行垃圾收集回收. Java如何实现内存动态分配和内存垃圾的回收? 1.哪些内存需要回收(垃圾收集器内存回收的对象)?已经"死亡"的对象,那如何判定对象已经"死亡"了? Java堆回收的内存:已经"死亡"的对象 方法区回收的内存:废弃的常量和无用的类 2

深入理解JVM读书笔记二: 垃圾收集器与内存分配策略

3.2对象已死吗? 3.2.1 引用计数法 给对象添加一个引用计数器,每当有一个地方引用它的地方,计数器值+1:当引用失效,计数器值就减1;任何时候计数器为0,对象就不可能再被引用了. 它很难解决对象之间相互循环引用的问题. 3.2.2 可达性分析算法 这个算法的基本思路就是通过一系列的称为"GC Roots"的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(用图论的话来说,就是从GC

003 垃圾收集器与内存分配策略

1.概述 程序计数器.虚拟机栈.本地方法栈是线程私有的,内存分配和回收都具有确定性,不需要考虑垃圾回收的问题,方法结束或者线程结束,内存就自然回收了 java堆和方法区的内存的分配和回收都是动态的,垃圾收集器所关注的是这部分的内存 2.垃圾收集器处理的对象 垃圾收集器需要确定哪些对象还"存活"着,哪些已经"死去"(不可能再被任何途径使用的对象) ①引用计数算法 给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1:当引用失效时,计数器值就减1:任何时刻

Java虚拟机垃圾收集器与内存分配策略

Java虚拟机垃圾收集器与内存分配策略 概述 那些内存需要回收,什么时候回收,如何回收是GC需要完成的3件事情. 程序计数器,虚拟机栈与本地方法栈这三个区域都是线程私有的,内存的分配与回收都具有确定性,内存随着方法结束或者线程结束就回收了. java堆与方法区在运行期才知道创建那些对象,这部分内存分配是动态的,本章笔记中分配与回收的内存指的就是:java堆与方法区. 判断对象已经死了 引用计数算法:给对象添加一个引用计数器,每当有一个地方引用它,计数器+1;引用失败,计数器-1.计数器为0则改判

深入理解JAVA虚拟机 垃圾收集器和内存分配策略

引用计数算法 很多教科书判断对象是否存活的算法是这样的:给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1:当引用失效时,计数器值就减1:任何时刻计数器都为0的对象就是不可能再被使用的. 客观地说,引用计数算法(Reference Counting)的实现简单,判定效率也很高,在大部分情况下它都是一个不错的算法,也有一些比较著名的应用案例,例如微软的COM(Component Object Model)技术.使用ActionScript 3的FlashPlayer.Python语