[CLR via C#]21. 自动内存管理(垃圾回收机制)
目录
一、理解垃圾回收平台的基本工作原理
- 值类型(含所有枚举类型)、集合类型、String、Attribute、Delegate和Event所代表的资源无需执行特殊的清理操作。
- 如果一个类型代表着或包装着一个非托管资源或者本地资源(比如数据库连接、套接字、mutex、位图等),那么在对象的内存准备回收时,必须执行资源清理代码。
- CLR要求所有的资源都从托管堆分配。
- 进程初始化时,CLR要保留一块连续的地址空间,这个地址空间最初没有对应的物理存储空间。这个地址空间就是托管堆。托管堆还维护着一个指针,可以称为NextObjPtr。它指向下一个对象在堆中的分配位置。刚开始时,NextObjPtr设为保留地址空间的基地址。IL指令使用newobj创建一个对象。newobj指令将导致CLR执行以下步骤:
- 计算类型(及其所有基类型)所需要的字节数。
加上对象的额外开销的字节数——“类型对象指针”和“同步块索引”。
- CLR检查保留区域是否能分配出相应的字节数。如果托管堆有足够的可用空间,对象将被放入。注意对象这在NextObjPtr指针指向的地址放入的,并且为它分配的字节会被清零。接着,调用类型的实例构造函数(为this参数传递NextObjPtr),IL指令newobj将返回对象的地址。就在地址返回之前,NextObjPtr指针的值会加上对象占据的字节数,这样就会得到一个新的NextObjPtr值,它指向下一个对象放入托管堆时的地址。
- 托管堆之所以能这么做,是因为它做了一个相当大胆的假设——地址空间和存储是无限的。这个假设显然是荒谬的。所以,托管堆必须通过某种机制来允许它做这样的假设。这种机制就是垃圾回收。
- 对象不断的被创建,NextObjPtr也在不断的增加,如果NextObjPtr超过了地址空间的末尾,表明托管堆已满,就必须强制执行一次垃圾回收。
二、 垃圾回收算法
- 每个应用程序都包含一组根。每个根都是一个存储位置,其中包含指向引用类型对象的指针。该指针要么引用托管堆中的一个对象,要么为null。只有引用类型的变量才会被认为是根;值类型的变量永远不被认为是根。
- 垃圾回收开始执行时,它假设堆中所有对象都是垃圾。
- 第一个阶段为标记阶段。这个阶段,垃圾回收器沿着线程栈向上检查所有根。如果发现一个根引用了一个对象,就进行”标记”。该标记具有传递性。标记好根和它的字段引用的对象之后,垃圾回收器会检查下一个根,并继续标记对象。如果垃圾回收期试图标记先前已经标记了的根,就会停止沿着这个路径走下去。检查好所有根之后,堆中将包含一组已标记和未标记的对象。已标记的对象是通过应用程序的代码可以到达的对象,而未标记的对象是不可达的。不可达的对象就是垃圾,它们的内存是可以回收的。
- 第二个阶段为压缩(可以理解成"内存碎片整理")阶段。在这个阶段中,垃圾回收器线性遍历堆,以寻找未标记对象的连续内存块。如果这个内存块较小,垃圾回收器会忽略它们。反之,垃圾回收器会把非垃圾的对象移动到这里已压缩堆,其实在这是内存碎片整理或许更会适用。自然的,包含那些”指向这些对象的指针”的变量和CPU寄存器现在都会变得无效。所以,垃圾回收器必须重新访问应用程序的所有根,并修改它们来指向对象的新内存位置。堆内存压缩之后,托管堆的NextObjPtr指针将指向紧接在最后一个非垃圾回收对象之后的位置。
- 所以,垃圾回收器会造成显著的损失,这是使用托管堆的主要缺点。当然,垃圾回收只在第0代满的时候才会发生。在此之前,托管堆性能远远高于C运行时堆。
三、垃圾回收与调试
- 当JIT编译器将方法的IL代码编译成本地代码时,JIT编译器会检查两点:定义方法的程序集在编译时没有优化;进行当前在一个调试器中执行。如果这两点都成立,JIT编译器在生成方法的内部根表时,会将变量的生存期手动延长至方法结束。
四、使用终结操作来释放本地资源
- 终结是CLR提供的一种机制,允许对象在垃圾回收器回收其内存之前执行一些得体的清理工作。
- 任何包装了本地资源的类型都必须支持终结操作。简单的说,类型实现了一个命名为Finalize的方法。当垃圾回收期判断一个对象是垃圾时,会调用对象的Finalize方法。
- C#团队认为,Finalize方法是编程语言中需要特殊语法的一种方法。在C#中,必须在类名前加一个~符号来定义Finalize方法。
Internal sealed class SomeType { ~SomeType(){ //这里的代码会进入Finalize方法 } }
5. 编译上述代码,会发现C#编译器实际是在模块的元数据中生成一个名为Finalize的protected override方法。方法主体被放到try块中,finally块放入了一个对base.Finalize的调用。
6.实现Finalize方法时,一般都会调用Win32 CloseHandle函数,并向该函数传递本地资源的句柄。
五、对托管资源使用终结操作
- 永远不要对托管资源使用终结操作,这是有一种非常好的编程习惯。因为对托管资源使用终结操作是一种非常高级的编码方式,只有极少数情况下才会用到。
- 设计一个类型时,处于以下几个性能原因,应避免使用Finalize方法:
- 可终结的对象要花费更长的时间来分配,因为指向它们的指针必须先放到终结列表中。("终结列表"在第七节会说到)
- 可终结对象会被提升到较老的一代,这会增加内存压力,并在垃圾回收器判定为垃圾时,阻止回收。除此之外,对该对象直接或间接引用的对象都会提升到较老的一代。("代"在第十三节会说到)
- 可终结的对象会导致应用程序运行缓慢,因为每个对象在进行回收时,需要对它们进行额外操作。
- 我们无法控制Finalize方法何时运行。CLR不保证各个Finalize的调用顺序。
六、是什么导致Finalize方法被调用
- 第0代满 只有第0代满时,垃圾回收器会自动开始。该事件是目前导致调用Finalize方法最常见的一种方式。("代"在第十三节会说到)
- 代码显式调用System.GC的静态方法Collect 代码可以显式请求CLR执行即时垃圾回收操作。
- Windows内存不足 当Windows报告内存不足时,CLR会强制执行垃圾回收。
- CLR卸载AppDomain 一个ApppDomain被卸载时,CLR认为该AppDomain不存在任何根,因此会对所有代的对象执行垃圾回收。
- CLR关闭 一个进程结束时,CLR就会关闭。CLR关闭会认为进程中不存在 任何根,因此会调用托管堆中所有的Finalize方法,最后由Windows回收内存。
七、终结操作揭秘
- 应用程序创建一个新对象时,new操作符会从堆中分配内存。如果对象的类型定义了Finalize方法,那么在该类型的实例构造器调用之前,会将一个指向该对象的指针放到一个终结列表(finalization list)中。
- 终结列表是由垃圾回收器控制的一个内部数据结构。列表中的每一项都指向一个对象,在回收该对象之前,会先调用对象的Finalize方法。
- 下图1展示了包含几个对象的一个托管堆。有的对象从应用程序的根可达,有的不可达(垃圾)。对象C,E,F,I,J被创建时,系统检测到这些对象的类型定义来了Finalize方法,所有指向这些对象的指针要添加到终结列表中。
- 垃圾回收开始时,对象B,E,G,H,I和J被判定为垃圾。垃圾回收器扫描终结列表以查找指向这些对象的指针。找到一个指针后,该指针会从终结列表中移除,并追加到freachable队列中。freachable队列(发音是“F-reachable”)是垃圾回收器的内部数据结构。Freachable队列中的每个指针都代表其Finalize方法已准备好调用的一个对象。图2展示了回收完毕后托管堆的情况。
- 从图2中我们可以看出B,E和H已经从托管堆中回收了,因为它们没有Finalize方法,而E,I,J则暂时没有被回收,因为它们的Finalize方法还未调用。
- 一个特殊的高优先级的CLR线程负责调用Finalize方法。使用专用的线程可避免潜在的线程同步问题。freachable队列为空时,该线程将睡眠。当队列中有记录项时,该线程就会被唤醒,将每一项从freachable队列中移除,并调用每一项的 Finalize方法。
- 如果一个对象在freachable队列中,那么意味这该对象是可达的,不是垃圾。
- 原本,当对象不可达时,垃圾回收器将把该对象当成垃圾回收了,可是当对象进入freachable队列时,有奇迹般的”复活”了。然后,垃圾回收器压缩(内存脆片整理)可回收的内存,特殊的CLR线程将清空freachable队列,并调用其中每个对象的Finalize方法。
- 垃圾回收器下一次回收时,发现已终结的对象成为真正的垃圾,因为应用程序的根不再指向它,freachhable队列也不再指向它。所以,这些对象的内存会直接回收。
- 整个过程中,可终结对象需要执行两次垃圾回收器才能释放它们占用的内存。可在实际开发中,由于对象可能被提升到较老的一代,所以可能要求不止两次进行垃圾回收。图3展示了第二次垃圾回收后托管堆中的情况。
八、Dispose模式:强制对象清理资源
- Finalize方法非常有用,因为它确保了当托管对象的内存被释放时,本地资源不会泄漏。但是,Finalize方法的问题在于,他的调用时间不能保证。另外,由于他不是公共方法,所以类的用户不能显式调用它。
- 类型为了提供显式进行资源清理的能力,提供了Dispose模式。
- 所有定义了Finalize方法的类型都应该同时实现Dispose模式,使类型的用户对资源的生存期有更多的控制。
九、使用实现了Dispose模式的类型
- 调用Dispose或Close只是为了能在一个确定的时间强迫对象执行清理;这两个方法并不能控制托管堆中的对象所占用的内存的生存期。这意味着即使一个对象已完成了清理,仍然可在它上面调用方法,但会抛出ObjectDisposedException异常。
- 建议只有在以下两种情况下才调用Dispose或Close:
- a) 确定必须清理资源
- b) 确定可以安全的调用Dispose或Close,并希望将对象从终结列表中删除,禁止对象提升到下一代,从而提升性能。
十、C#的using语句
- 如果决定显式地调用Dispose和Close这两个方法之一,强烈建议把它们放到一个异常处理finally中。这样可以保证清理代码得到执行。
- Using语句就是一种对第1点进行简化的语法。
十一、手动监视和控制对象的生存期
- CLR为每一个AppDomain都提供了一个GC句柄表。该表允许应用程序监视对象的生存期,或手动控制对象的生存期。
- 在一个AppDomain创建之初,该句柄表是空的。句柄表中的每个记录项都包含以下两种信息:一个指针,它指向托管堆上的一个对象;一个标志(flag),它指出你想如何监视或控制对象。
- 为了在这个表中添加或删除记录项,应用程序要使用如下所示的System.Runtime.InteropServices.GCHandle类型。
十二、对象复活
- 前面说过,需要终结的一个对象被认为死亡时,垃圾回收器会强制是该对象重生,使它的Finalize方法得以调用。Finalize方法调用之后,对象才真正的死亡。
- 需要终结的一个对象会经历死亡、重生、在死亡的”三部曲”。一个死亡的对象重生的过程称为重生。
- 复活一般不是一件好事,应避免写代码来利用CLR这个”功能”。
十三、代
- 代是CLR垃圾回收器采用的一种机制,它唯一的目的就是提升应用程序的性能。
- 一个基于代的垃圾回收器做出了以下几点假设:
- 对象越新,生存期越短。
- 对象越老,生存期越长。
- 回收堆的一部分,速度快于回收整个堆。
- 代的工作原理:
- 托管堆在初始化时不包含任何对象。添加到堆的对象称为第0代对象。第0代对象就是那些新构造的对象,垃圾回收器从未检查过它们。图4展示了一个新启动的应用程序,它分配了5个对象。过会儿,对象C和E将变得不可达。
- CLR初始化时,它会为第0代对象选择一个预算容量,假定为256K(实际容量可能有所不同)。所以,如果分配一个新对象造成第0代超过预算,就必须启动一次垃圾回收。假定对象A到E刚好占用256K内存。对象F分配时,垃圾回收器必须启动。垃圾回收器判定对象C和E为垃圾,因为会压缩(内存碎片整理)对象D,使其与对象B相邻。之所以第0代的预算容量为256K,是因为所有这些对象都能装入CPU的L2缓存,使之压缩(内存碎片整理)能以非常快的速度完成。在垃圾回收中存活的对象(A、B和D)被认为是第1代对象。第1代对象已经经历垃圾回收的一次检查。此时的对如图5所示。
- 一次垃圾回收后,第0代就不包含任何对象了。和前面一样,新对象会分配到第0代中。在图6中,应用程序继续运行,并新分配了对象F到对象K。另外,随着应用程序继续运行,对象B、H和J变得不可达,它们的内存将在某一个回收。
- 现在,假定分配新对象L会造成第0代超过256KB的预算。由于第0代达到预算,所以必须启动垃圾回收器。开始一次垃圾回收时,垃圾回收器必须决定检查哪些代。
- 前面说过,当CLR初始化时,他为第0代对象选择了一个预算。同样的,它还必须为第1代选择一个预算。假定为第1代选择的预算为2MB。
- 垃圾回收开始时,垃圾回收器还会检查第1代占据了多少内存。由于在本例中。第一代占据的内存远远小于2MB,所以垃圾回收器只检查第0代。因为此时垃圾回收器只检查第0代,忽略第1代,所以大大加快了垃圾回收器的速度。但是,对性能最大的提升就是现在不必遍历整个托管堆。如果一个对象引用了一个老对象,垃圾回收器就可以忽略那个老对象的所有内部引用,从而能更快的构造好可达对象的图。
- 如图7所示,所有幸存下来的第0代对象变成了第1代的一部分。由于垃圾回收器没有检查第1代,所以对象B的内存并没有被回收,即使它在上次垃圾回收时变得不可达。在一次垃圾回收后,第0代不包含任何对象,等着分配新对象。
- 假定程序继续运行,并分配对象L到对象O。另外,在运行过程中,应用程序停止使用对象G,I,M,是它们变得不可达。此时的托管堆如图8所示。
- 假设分配对象P导致第0代超过预算,垃圾回收发生。由于第1代中所有对象占据的内存仍小于2MB,所以垃圾回收器再次决定只回收第0代,忽略第1代不可达的垃圾(对象B和G)。回收后,堆的情况如图9所示。
- 从图9中可以看到,第1代正在缓慢增长。假定第1代的增长导致它所有对象占据的内存刚好达到2MB。这时,随着应用程序的运行,并分配了对象P到对S,使第0代对象达到了它的预算容量。这是的堆如图10所示。
- 应用程序试图分配对象T时,由于第0代已满,所以必须开始垃圾回收。但是,这次垃圾回收器发现第1代占据的内存超过了2MB。所以垃圾回收器这次决定检查第1代和第0代中的所有对象。两代都被回收之后,托管堆情况如图11所示。
4. 像前面一样,垃圾回收后,第0代的幸存者被提升到了第1代,第1代的幸存者被提升到了第2代,第0代再次空出来,准备迎接新对象的到来。第2代中的对象会经过2次或更多次的检查。只有在第1代到达预算容量是才会检查第1代中的对象。而对此之前,一般已经对第0代进行了好几次垃圾回收。
5. CLR的托管堆只支持三代:第0代、第1代和第2代。第0代的预算约为256KB,第1代的预算约为2MB,第2代的预算容量约为10MB。
十四、 线程劫持
- 前面讨论的垃圾回收算法有一个很大的前提就是:只在一个线程运行。
- 在现实开发中,经常会出现多个线程同时访问托管堆的情况,或至少会有多个线程同时操作堆中的对象。一个线程引发垃圾回收时,其它线程绝对不能访问任何线程,因为垃圾回收器可能移动这些对象,更改它们的内存位置。
- CLR想要进行垃圾回收时,会立即挂起执行托管代码中的所有线程,正在执行非托管代码的线程不会挂起。然后,CLR检查每个线程的指令指针,判断线程指向到哪里。接着,指令指针与JIT生成的表进行比较,判断线程正在执行什么代码。
- 如果线程的指令指针恰好在一个表中标记好的偏移位置,就说明该线程抵达了一个安全点。线程可在安全点安全地挂起,直至垃圾回收结束。如果线程指令指针不在表中标记的偏移位置,则表明该线程不在安全点,CLR也就不会开始垃圾回收。在这种情况下,CLR就会劫持该线程。也就是说,CLR会修改该线程栈,使该线程指向一个CLR内部的一个特殊函数。然后,线程恢复执行。当前的方法执行完后,他就会执行这个特殊函数,这个特殊函数会将该线程安全地挂起。
- 然而,线程有时长时间执行当前所在方法。所以,当线程恢复执行后,大约有250毫秒的时间尝试劫持线程。过了这个时间,CLR会再次挂起线程,并检查该线程的指令指针。如果线程已抵达一个安全点,垃圾回收就可以开始了。但是,如果线程还没有抵达一个安全点,CLR就检查是否调用了另一个方法。如果是,CLR再一次修改线程栈,以便从最近执行的一个方法返回之后劫持线程。然后,CLR恢复线程,进行下一次劫持尝试。
- 所有线程都抵达安全点或被劫持之后,垃圾回收才能使用。垃圾回收完之后,所有线程都会恢复,应用程序继续运行,被劫持的线程返回最初调用它们的方法。
- 实际应用中,CLR大多数时候都是通过劫持线程来挂起线程,而不是根据JIT生成的表来判断线程是否到达了一个安全点。之所以如此,原因是JIT生成表需要大量内存,会增大工作集,进而严重影响性能。
十五、大对象
- 任何85000字节或更大的对象都被自动视为大对象。
- 大对象从一个特殊的大对象堆中分配。这个堆中采取和前面小对象一样的方式终结和释放。但是,大对象永远不压缩(内存碎片整理),因为在堆中下移850000字节的内存块会浪费太多CPU时间。
- 大对象总是被认为是第2代的一部分,所以只能为需要长时间存活的资源创建大对象。如果分配短时间存活的大对象,将导致第2代被更频繁地回收,进而会损害性能。
[CLR via C#]21. 自动内存管理(垃圾回收机制)的更多相关文章
- python内存管理&垃圾回收
python内存管理&垃圾回收 引用计数器 环装双向列表refchain 在python程序中创建的任何对象都会放在refchain连表中 name = '张三' age = 18 hobby ...
- Java 类加载机制 ClassLoader Class.forName 内存管理 垃圾回收GC
[转载] :http://my.oschina.net/rouchongzi/blog/171046 Java之类加载机制 类加载是Java程序运行的第一步,研究类的加载有助于了解JVM执行过程,并指 ...
- [Python之路] 内存管理&垃圾回收
一.python源码 1.准备源码 下载Python源码:https://www.python.org/ftp/python/3.8.0/Python-3.8.0.tgz 解压得到文件夹: 我们主要关 ...
- 21 re正则模块 垃圾回收机制
垃圾回收机制 不能被程序访问到的数据,就称之为垃圾 引用计数 引用计数:用来记录值的内存地址被记录的次数的:当一个值的引用计数为0时,该值就会被系统的垃圾回收机制回收 每一次对值地址的引用都可以使该值 ...
- 内存管理 垃圾回收 C语言内存分配 垃圾回收3大算法 引用计数3个缺点
小结: 1.垃圾回收的本质:找到并回收不再被使用的内存空间: 2.标记清除方式和复制收集方式的对比: 3.复制收集方式的局部性优点: https://en.wikipedia.org/wiki/C_( ...
- JVM | 第1部分:自动内存管理与性能调优《深入理解 Java 虚拟机》
目录 前言 1. 自动内存管理 1.1 JVM运行时数据区 1.2 Java 内存结构 1.3 HotSpot 虚拟机创建对象 1.4 HotSpot 虚拟机的对象内存布局 1.5 访问对象 2. 垃 ...
- JVM内存管理及GC机制
一.概述 Java GC(Garbage Collection,垃圾收集,垃圾回收)机制,是Java与C++/C的主要区别之一,作为Java开发者,一般不需要专门编写内存回收和垃圾清理代码,对内存泄露 ...
- JavaScript作用域链和垃圾回收机制
作用域链 基本概念: 在了解作用域链和内存之前,我们先了解两个概念,分别是执行环境和变量对象. 执行环境:定义变量或者函数有权访问的其他数据,决定了它们各自的行为.每个对象都有自己的执行环境. 变量对 ...
- Java垃圾回收机制分析
Java的堆是一个运行时数据区,类的实例从中分配空间,堆中存储着正在运行的应用程序所建立的所有对象.垃圾回收是一种动态存储管理技术.它按照特定的垃圾回收算法,自动释放掉不再被引用的对象.堆内存里垃圾的 ...
随机推荐
- QQ空间直播秒开优化实践[读]
http://mp.weixin.qq.com/s?__biz=MzI1MTA1MzM2Nw==&mid=2649796799&idx=1&sn=42061b7d021b8d8 ...
- Maven3路程(二)Eclipse集成Maven
我的环境: Eclipse:eclipse-jee-juno-SR2-win32 Maven:Maven3.0.5 1.Help->Eclipse Marketplace 2.选中要安装的插件, ...
- 关于u-boot中的.balignl 16,0xdeadbeef的理解
.globl _start //不占内存_start: b start_code //占4字节内存 ldr pc, _undefined_instruction //占4字节内存 ldr ...
- OpenGL cubeMap
glsl 的reflect(I,N)其中I是 眼睛(camera)位置到顶点位置的方向向量,N为顶点法线,必须要归一化 橙宝书里给出的计算过程是这样的:reflect(I,N) = I - 2 *do ...
- C primer plus 练习题 第三章
5. #include <stdio.h> int main() { float you_sec; printf("请输入你的年龄:"); scanf("%f ...
- sencha cmd 更新日志
http://cdn.sencha.com/cmd/6.2.1.29/release-notes.html
- java.lang.NullPointerException
你妹的这是什么错误啊? Errors occurred during the build. Errors running builder 'Android Resource Manager' on p ...
- 个性化EDM数据营销的三大提醒
EDM数据营销行业已经进入个性化时代.但是怎样实现个性化仍然是一个重大课题.国内领先的智能化邮件营销服务商Focussend一直致力于探索和研究邮件营销领域的先进知识和做法,旨在为企业和个人提供更多有 ...
- 【资源下载】Ext4.1.0_Doc中文版_V1.0.0_Beta正式提供下载!
*************************************************重要提示: 在2014年1月1日前一天,历时两年左右的时间,翻译小组终于完成了该API的翻译.可喜可贺 ...
- ecslipe cdt lib link
项目属性-> settings -> mingw c linker 1.libs search 填写lib路径 2.lib 填写文件名,不要后缀