• CLR via C#-托管堆和垃圾回收


    目的:
    如何构造新对象,托敢对如何控制这些对象的生存期,以及如何管理这些对象的内存


    托管堆基础
    每个程序都需要使用资源,比如:文件、内存缓冲区、屏幕空间、网络连接、数据库资源等等
    面向对象编程中,每个类型都是一种资源,如何访问资源?
    a.调用 IL 指令 newobj,为代表资源的类型分配内存
    b.初始化内存,设置资源的初始化状态并使用(每个类型实例构造器负责设置初始状态)
    c.访问类型的成员来使用资源
    d.摧毁资源的状态进行清理(unsafe资源需要)
    e.释放内存。垃圾回收器负责

    如何从托管堆分配内存?
    CLR 要求所有的对象都从托管堆分配,进程初始化时 CLR 划出一个地址空间区域作为托管堆
    CLR 维护一个指针(NextObjPtr),指向下一个对象在堆中分配位置
    (32位进程最多能分配1.5G,64位进程最多能分配8TB)
    c# 的 new 操作:
    a.计算类型的字段(包括基类继承的)需要的字节数
    b.加上对象的开销所需的字节数,每个对象有两个开销字段:类型对象指针和同步块索引
    c.CLR 检查区域中是否有分配对象所需的字节数,如果有的话就控制 NextObjPtr 指针指向的地址放入对象,为对象分配的字节会被清零。接着调用类型的构造器,new 操作符返回对象的引用。
    同时 NextObjPtr 会加上对象所占用的字节数,给下一个对象做初始位置(连续分配可以提高缓存命中)
    d.如果没有就进行垃圾回收

    垃圾回收的算法是啥?
    微软用的是 组件对象模型(Component Object Model,COM)也是引用计数
    堆上的每个对象都维护者一个内存字段统计程序中多少地方引用这个对象,当不需要这个对象的时候就会递减计数,直到0的时候,就可以从内存中删除了
    注意:循环引用问题,会导致两个对象永远不会删除

    CLR 改为使用一种引用跟踪算法,值关心引用类型的变量,只有这种变量才可以引用堆上的对象
    (而值类型变量直接包含值类型实例)
    将引用类型的变量成为根

    当CLR 开始 GC 时,流程如下:
    a.暂停进程中的所有线程(防止线程在 CLR 检查期间访问对象并改期状态)
    b.进行 GC 的标记阶段,会遍历堆中的所有对象,将同步块索引字段中的一位设置为0
    c.检查活动根,查看它们引用了那些对象(引用跟踪)

    • 如果根包含 null ,就忽略这个
    • 如果根引用了堆上的对象,CLR 就会标记那个对象,将该对象同步块索引中的为设置为1。当某个对象被标记后,CLR 会检查那个对象中的跟,标记他们引用的对象。如果发现对象已经标记,就不重复检查,防止循环引用造成死循环
    • 检查完所有后,已标记的对象不能被垃圾回收,叫可达的(reachable),未标记的对象是不可达的(unreachable)

    d.进入 GC 压缩(compact)阶段,CLR 对堆中已标记的对象占用连续的内存空间,解决内存碎片问题。还要从每个根对象减去所引用的对象的偏移字节数,保证和之前的引用是一样的
    e.NextObjPtr 指向最后一个幸存对象之后的位置
    f.恢复所有的线程

    注意:静态字段引用的对象将一直存在,直到加载类型的 AppDomain 卸载位置。


    下面这段代码,TimerCall 只会输出一次

    public static class Program
    {
    	public static void Main()
    	{
    		Timer t = new Timer(TimerCallback, null, 0, 2000);
    		Console.ReadLine();
    	}
    
    	static void TimerCallback(Object o)
    	{
    		Console.WriteLine("In TimerCallback: " + DateTime.Now);
    		GC.Collect();
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    因为没有任何变量引用 Timer
    切换到 debug 编译器模式,编辑器会将所有根的方法生存期延长至方法结束


    如果切换到 -ebug编译模式,下面这段代码也只会输出一次
    因为 t=null 被JIT 编译成等价于不引用该变量,会把整行代码注释掉

    public static class Program
    {
    	public static void Main()
    	{
    		Timer t = new Timer(TimerCallback, null, 0, 2000);
    		Console.ReadLine();
    
    		t = null;
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    如何不开启 debug 编译选项,还可以等待程序结束时再释放?
    通过 t.Dispose() 这样会标记为 t 引用的对象必须存活才可以调用 Dispose()

    public static class Program
    {
    	public static void Main()
    	{
    		Timer t = new Timer(TimerCallback, null, 0, 2000);
    		Console.ReadLine();
    
    		t.Dispose();
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10


    CLR 的 GC 是基于代的垃圾回收器(generational garbage collector)
    大部分项目都有如下特征:
    a.对象越新,生存期越短
    b.对象越老,生存期越长
    c.回收堆的一部分,速度快于回收整个堆

    托管堆在初始化时不包含对象,第一批添加到堆的对象成为第0代对象
    CLR 初始化的时候为第0代对象选择一个预选容量,如果分配一个新对象造成的第0代超过预算,就必须启动一次垃圾回收

    所有没被回收第0代的对象在被压缩之后变成1代对象,此时第0代就没有对象了
    之后在申请内存就会被分配到第0代中,再选择新的预算空间(动态分配的)

    之后再回收的时候,只会检测第0代的对象,其他代的对象采用标志位,如果修改了标识位就发生变化了,才会和第0代一起检测

    所以,
    第X代,就是经历过几次检查存活下的那一堆对象
    托管堆只支持3代:第0代、第1代、第2代。没有第3代

    注意:CLR 对大对象(超过85000字节)的对象特殊处理
    a.不在小对象的地址空间分配,有专门的地方
    b.不压缩大对象,因为移动的代价比较高
    c.大对象始终是第2代,一般都是需要长时间存活的对象

    垃圾回收的触发条件
    a.第0代超过预算时
    b.主动调用 GC.Collect
    c.Windows 低内存
    d.CLR 卸载AppDomain
    e.CLR 正在关闭

  • 相关阅读:
    HTTP 接口测试的流程
    钉钉漏洞通知脚本dingtalkBot—配套Automated_bounty_Hunter
    Vue整合Markdown组件+SpringBoot文件上传+代码差异对比
    文件系统系列专题之 Ext2/3/4
    振弦采集仪应用于隧道安全监测
    Unity 下载Zip压缩文件并且解压缩
    [JS] canvas 详解
    求职简历这样写,轻松搞定面试官
    TikTok平台的两种账户有什么区别?
    OpenFeign讲解+面试题
  • 原文地址:https://blog.csdn.net/qq_33064771/article/details/128204605