浅析CLR的GC(垃圾回收器)

文章目录:

  1. 了解托管堆和GC
  2. GC高效的处理方式—代
  3. 特殊类型的清理
  4. 手动监控和控制对象生命周期

1、了解托管堆和GC

  在面向对象环境中,每一个类型都代表了一种资源。我们要使用这些资源,就要为这些代表资源的类型分配内存。在C#中,我们一般使用new关键字来完成。访问资源包括以下几步:

    • 使用new操作符为类型分配内存(这个过程调用了IL指令newobj)
    • 初始化内存,设置资源的初始状态,来让这个资源可用(类型的实力构造器负责初始化类型状态)
    • 访问类型成员使用资源
    • 摧毁资源状态进行清理
    • 释放内存

  在C#中,我们的操作时基于CLR来完成的,我们所有对象都是从托管堆对来分配内存。当进程初始化(我们的程序)时,CLR会画出一个地址空间区域来作为托管堆。同时,CLR会维护一个指针NewObjPtr,这个指针指向下一个对象在托管堆中分配的地址。当这个区域被非垃圾的对象填满后,CLR会分配更多的区域,这个过程将一直重复,直至整个进程的地址空间都被填满。32位进程的地址空间为1.5GB,64位进程为8TB。(这里顺便提一下值类型的生命周期,值类型对象分配在线程栈上,当离开作于域时,自动销毁。)

  C# new 操作符,会让CLR执行以下步骤:

    • 计算类型字段的所需字节数(这里的字段是所有字段,包括基类继承的)
    • 添加创建对象的额外所需字节数 (每一个对象被初始化时,会创建类型对象指针和同步块索引,32位程序为8字节,64位程序16字节)
    • CLR检查区域中空间是否足够,假如足够,在NewObjPtr指针位置放入对象。这时候,对象分配的字节会被清零,然后调用类型的构造器(计算字节,NewObjPtr指针会指向旧的位置加上这个字节的位置,为下一个对象分配空间时候的位置),new操作符返回对象的引用。(例如在托管堆已经由A,B的情况下新构建了一个C)

  关于GC

  当程序调用new操作符创建对象时候,假如没有足够的地址空间来分配该对象,CLR就会执行垃圾回收(调用GC)。CLR采用引用跟踪算法,这种算法只关心引用类型的变量,避免了类型循环导致对象不能被回收的问题。引用类型包括类的静态和实例字段,方法的局部变量和参数。所有的引用类型被称之为根。

  CLR开始执行GC时候,会暂停进程中所有线程(防止CLR执行检查期间对象的状态被更改);然后CLR遍历托管堆中的所有对象,将同步块索引中字段的一位设置为0(标识所有对象都应该删除),然后CLR检查所有活动根,这些根引用了哪些对象。

  任何一个根引用了堆的对象,CLR会将对象的同步块索引的位设置位1。然后再检查对象中的跟,标记它们引用的对象。假如在遍历过程中发现对象被标记,就跳过这个对象,不再重新检查这个对象的字段,这样避免了循环引用。

  应用程序中所有的活动跟都检查完毕以后,这时候堆中的对象要么被标记了(称之为可达,由活动跟在引用),要么没有被标记(相应称为不可达)。CLR将不可达的对象内存回收。将可达对象进行内存整理,使对象内存在托管堆中是连续的(压缩过程中CLR要从每个根减去所引用对象在堆中的偏移字节数),如下图所示:

2、GC高效的处理方式—代

  CLR的垃圾回收基于代。

  同时GC回收垃圾时,做出了下面几点假设(可以先记下,从下文中体会)

  • 对象越新,生存期越短
  • 对象越来,生存期越长
  • 回收堆一部分内存,比回收整个堆要快   

  解释下代:托管进程中有两种内存堆,分别是本机堆托管堆,CLR在托管堆上面为.net 的所有对象分配内存(托管堆又称为GC堆)。托管堆又分为两种,小对象堆大对象堆(LOH),小对象堆用来分配常用的资源对象内存(如类,数组等等),小对象堆的内存段进一步划分为3代,0代,1代,2代。(大对象堆用来分配一些大对象和非托管资源,我们后文中专门来解释)

   托管堆初始化时,不包含任何对象,当我们声明一个对象时,这个对象称为第0代对象。也就是说,第0代对象就是那些新构造的对象,而且垃圾回收器没有检查过的对象。例如下图中,托管堆中分配了A,B,C,D,E5个对象,它们就是第0代对象。

  接下来随着我们不停地分配对象,第0代的堆内存使用完毕,且这随着程序的流转,C和E变得不可达,当我们分配下一个内存F时,CLR就会执行一次垃圾回收。此时,C,E对象内存被回收掉,我的的ABD对象从第0代对象变为第1代对象。这时候,垃圾回收结束,第0代不包含任何对象。如下图所示:

  接下来随着程序的运行,又在0代中分配了对象F G H I J K,1代对象中B变得不可达。接下来给对象L分配内存时内存不足,将执行垃圾回收。CLR会为第0代对象和第 1代对象选择预算,由于第一代中的占用内存远少于预算,所以垃圾回收期只检查第0代的对象(基越新的对象获得越短),因为第0代对象包含更多的垃圾可能性更大,可以回收更多的内存。忽略了第一代中的对象,所以加快了垃圾回收速度。

  随着垃圾回收的不断进行,第1代的内存将不断增加,当第1代对象的内存增长到占用了占用了全部预算(0代给新对象分配内存就要进行GC),此时,会进行第1代的垃圾回收,幸存下来的对象被分配的第2代中去。托管堆只支持3代(0,1,2)。超过85000字节的对象称之为大对象,直接由第2代分配内存。

  代给GC带来的性能提升主要体现在不必遍历托管堆中的每一个对象。如果根或者对象引用了老一代的某个对象,垃圾回收期就可以忽略老对象内部所有引用(CLR的特征,引用跟踪算法,同步索引块中的一位标识),在更短的时间内构造好可达对象图。假如老对象字段引用了新对象,则由JIT编辑器内部的一个机制(单独解释)让垃圾回收期跳过。微软官方性能测试,0代执行一次GC,花费时间不少过1毫秒。

  • JIT的机制是在对象引用字段发生变化时候,设置一个对应位标志。这样,下一次GC回收资源内存时候,会知道上一次GC过后,哪些老对象被写入位标志,这样,只有位标志发生变化(也就是老对象字段发生变化)时候,才检查老对象是否引用第0代对象。

3、特殊类型的清理

  特殊类型:大多数对象只要分配内存就可以使用。但是,还有部分对象需要分配本机资源(例如文件,网络连接,套接字,互斥体),我们称这部分对象为特殊类型的资源。

  特殊类型的回收过程和特点:包含本机资源的类型被GC时,GC在回收内存之前,需要将本机资源终结(Finalization)。当CLR判定一个特殊类型的对象不可达时,对象将终结自己,释放包裹的本机资源,然后由GC回收其内存。

  Object基类型定义了虚方法Finalize,GC判定对象时垃圾后,调用对象的Finalize方法,这个方法一般以析构函数的形式出现。(ILSpy 反编译后的析构函数代码为protected override Finalize)。

   特殊类型注意事项:

    1. Finalize执行在GC之后,所以特殊类型的对象不是马上被GC回收,因为Finalize方法可能要访问对象字段。这可能使对象提升到另一级别的代,增加内存耗用。所以,尽量避免引用类型的字段定义为可终结对象。
    2. Finalize方法执行时无顺序的。所以不要在Finalize方法中访问定义了其他Finalize方法的类型,因为另一个类型对象可能已被终结。
    3. CLR用一个特殊的、高优先级专用线程调用Finalize方法避免死锁。
    4. 自定义包含了本机资源的托管类型时要继承自SafeHandle(派生自它保证本机资源在GC时被释放)。

  控制包装了本机资源类型对象的生存期:

      例如这里我们要往D盘的1.txt中写入一部分文本,然后写完后想把这个文件删除,此时就会报 “System.IO.IOException:“文件“d:\1.txt”正由另一进程使用,因此该进程无法访问此文件。”这样一个异常,这是因为本机资源未被释放(Finalize)。假如我们想控制包装本机资源的类型对象的生命周期,就要实现IDispose接口。(如果类型对象的其中一个字段实现了这个接口,那么这个类型也就实现了Dispose模式。)然后我们修改我们的代码,成功删除文件。

  终结的内部实现原理:

    包装了本机资源的对象被回收时,会调用Finalize方法。

    包装了本机资源的对象创建的时候(定义了Finalize方法),在从堆中分配内存前,会将这个对象的指针添加到一个终结列表(由GC控制的内部数据结构)中。这个列表中的每一项,都指向一个定义了Finalize方法的对象,回收这些对象内存之前应该先调用它的Finalize方法(这里注意,虽然Object也定义了Finalize方法,但是CLR会忽略它,只有重写了Finalize方法的类型对象才会加入到终结列表)。如下图所示,C,E,F,I,J是定义了Finalize方法的类型对象,指向它们的指针被加入到终结列表中:

    

    垃圾回收开始进行,B,E,G,H,I,J被判定为垃圾,这时候垃圾回收器会扫描终结列表来查找这些对象的引用(这里找到了E,I,J),然后把这些引用从终结列表中移除,附加到freachable队列(也是GC的一个内部数据结构)。在freachable队列中的每一个引用都代表即将进行Finalize调用的对象。经历过一轮GC后,堆内存如下所示:

    

    CLR使用一个高优先级的,专用的线程来调用Finalize方法,这个线程避免潜在的线程同步问题。当freachable队列为空时候,这个线程将休眠,freachable队列出现记录项,将唤醒这个线程。这样来看,包装了本机资源的托管对象至少要进行两次GC才能回收它的内存,第一次由专用线程来执行Finalize方法,第二次才由GC回收这个对象的内存(大于2次是因为这些对象可能被提高到老的一代)。

4、手动监控和控制对象生命周期

  CLR为每一个AppDomain都提供了一个GC Handle table,允许程序监视或者控制对象的生命周期。这个表中的每一条记录项都包含托管堆中一个对象的引用监视控制对象标志。这里注意一个类GCHandle和一个枚举对象 GCHandleType。

  GCHandle调用Alloc方法时候,会扫描AppDomain的GC Handle table,查找一个可用的记录项存储对象的生命周期并且传回给对象引用。GChandle的Target属性,返回句柄表示的对象,如下图所示:

  GC发生时候会使用GC Handle table,首先,GC将所有对象标识为将要回收,扫描GC Handle table,所有GCHandleType为Normal和Pinned对象标识为根;然后查找GCHandleType为Weak的项,如果引用了未标记的对象,那么这个对象就是垃圾,且把这个项赋值为null;GC继续扫描中介列表,将无引用标识对象的引用放入freachable队列;GC再扫描GC Handle table,查找GCHandleType 为WeakTrackResurrection的记录想,这些记录想引用了未标记的对象(freachable队列中)变为垃圾,这些记录项赋值为Null。最后GC对内存进行压缩。

原文地址:https://www.cnblogs.com/liumengchen-boke/p/9123833.html

时间: 2024-08-10 14:36:05

浅析CLR的GC(垃圾回收器)的相关文章

Java GC 垃圾回收器的类型小结

阅读了java paper的垃圾回收器类型文章,在此做一个小结,文章部分翻译自java paper gc collector,部分自己做的总结,图片来自网络,在此仅用作理解表达之用. 一.JVM GC 垃圾回收器类型 JVM的垃圾回收器大致分为四种类型: (图片来自网络) 1.串行垃圾回收器  Serial Garbage Collector 串行垃圾回收器在进行垃圾回收时,它会持有所有应用程序的线程,冻结所有应用程序线程,使用单个垃圾回收线程来进行垃圾回收工作. 串行垃圾回收器是为单线程环境而

一篇文章让你了解GC垃圾回收器

简单了解GC垃圾回收器 了解GC之前我们首先要了解GC是要做什么的?顾名思义回收垃圾,什么是垃圾呢? GC回收的垃圾主要指的是回收堆内存中的垃圾对象. 从根对象出发,所有被引用的对象,都是存活对象 其他对象,都是垃圾对象. 根对象:  栈中的引用变量,所引用的对象. 方法区经静态变量所引用的对象. GC回收的三种基本方式 一.标记-清除 标记存活对象,清理其他垃圾对象(阴影为存活对象,空白为垃圾对象) 优点:效率高 缺点:产生碎片,使内存分布碎片化,造成内存空间不连续.若出现大的对象,内存空间不

[Java基础]-- Java GC 垃圾回收器的分类和优缺点

https://blog.csdn.net/high2011/article/details/80177473?utm_source=blogxgwz2 所属专栏: Java 版权声明:尊重原创,转载请标明,本文转自 https://blog.csdn.net/high2011/article/details/80177473 关于Java的垃圾回收器,一直是个头疼的问题,这里简要说明下分类和优缺点,供选择使用. 一.JVM GC 垃圾回收器类型 JVM的垃圾回收器大致分为六种类型: 1.串行:

Java虚拟机笔记(二):GC垃圾回收

为什么要了解GC 我们都知道Java开发者在开发过程中是不需要关心对象的回收的,因为Java虚拟机的原因,它会自动回收那些失效的垃圾对象.那我们为什么还要去了解GC和内存分配呢? 答案很简单:当我们需要排查各种内存溢出.内存泄漏时,当垃圾收集器成为系统达到更高并发量的瓶颈时,我们就需要对这些"自动化"的技术实施必要的监控和调节. 回收哪些对象 我们知道在Java内存运行时数据区域中,虚拟机栈.本地方法栈和程序计数器是线程隔离的数据区,随线程而生,随线程而灭:栈中的栈帧随着方法的进入和退

Android内存优化1 了解java GC 垃圾回收机制3

引言 接App优化之内存优化(序), 作为App优化系列中内存优化的一个小部分. 由于内存相关知识比较生涩, 内存优化中使用到的相关工具, 也有很多专有名词. 对Java内存管理, GC, Android内存管理, Dalvik/ART等知识有一个理论的认识, 可以让我们更好的使用这些工具, 分析内存问题. 据此, 我们就先从理论入手, 聊聊GC那些事儿. 1, 何为GC GC 是 garbage collection 的缩写, 垃圾回收的意思. 也可以是 Garbage Collector,

C# 托管堆和垃圾回收器GC

这里我们讨论的两个东西:托管堆和垃圾回收器,前者是负责创建对象并控制这些对象的生存周期,后者负责回收这些对象. 一.托管堆分配资源 CLR要求所有的对象都从托管堆分配.进程初始化时,CLR划出一个地址空间区域作为托管堆.CLR还要维护一个指针P,该指针指向下一个对象在堆中的分配位置. 那么我们进一步深入看看创建一个对象(也就是new 一个对象)时CLR做了哪些工作呢. 1.计算类型字段需要的字节数. 2.加上对象开销所需要的字节数,每个对象都有两个开销:类型对象指针和同步块索引 3.CLR检查区

CLR中垃圾回收器模式

垃圾回收器有2种不同的工作模式,分别为工作站模式(Workstation)和服务器模式(Server),按照GC线程的工作方式,又可以分成并发方式(Concurrent),非并发方式(Non-concurrent). 在工作站模式上,可以运行并发方式和非并发方式,而在服务器模式上,只能运行非并发方式.在.NET4.0后,工作站模式和服务器模式上都引入了另一种新模式,后台GC模式. 无论是在工作站模式还是在服务器模式上,只要是非并发方式都称为阻塞式GC.因为这种方式下,GC运行的时候,都会挂起对应

Java GC系列(3):垃圾回收器种类

本文由 ImportNew - 好好先生 翻译自 javapapers. 目录 垃圾回收介绍 垃圾回收是如何工作的? 垃圾回收的类别 垃圾回收监视和分析 在这篇教程中我们将学习几种现有的垃圾回收器.在Java中,垃圾回收是一个自动的进程可以替代程序员进行内存的分配与回收这些复杂的工作.这篇是垃圾回 收教程系列的第三篇,在前面的第2部分我们看到了在Java中垃圾回收是如何工作的,那是篇有意思的文章,我推荐你去看一下.第一部分介绍了Java的垃 圾回收,主要有JVM体系结构,堆内存模型和一些Java

JVM之GC算法的实现(垃圾回收器)

上一节:<JVM之GC算法> 知道GC算法的理论基础,我们来看看具体的实现.只有落地的理论,才是真理. 一.JVM垃圾回收器的结构 JVM虚拟机规范对垃圾收集器应该如何实现没有规定,因为没有最好的垃圾收集器,只有最适合的场景. 图中展示了7种作用于不同分代的收集器,如果两个收集器之间存在连线,则说明它们可以搭配使用.虚拟机所处的区域则表示它是属于新生代还是老年代收集器. 7种:serial收集器.parnew收集器.parallel scavenge收集器.serial  old 收集器.pa