• .NET对象的创建、垃圾回收、非托管资源的手动处理


    本篇用来梳理对象的创建、垃圾的回收,以及非托管资源的手动处理。


    →首先运行应用程序,创建一个Windows进程。

     

    →CLR创建一块连续的虚拟地址空间,这个地址空间就是托管堆。而且,这个地址空间最初并没有对应的物理存储空间。

     

    虚拟地址空间分成2段。一个区段是普通堆,也叫GC堆,大小小于85000字节的引用类型对象的实例被分配在这里;另一个是大对象堆,大小大于等于85000字节的引用类型对象的实例被分配在这里。

     

    对于客户端应用程序,每个区段的大小大致是16MB;对于服务端应用程序,每个区段的大小大致是64MB。另外,每个区段的大小还会因CPU的数量,是否是32位或64位操作系统而各异。

     

    随着每个区段被装满对象,CLR会分配更多的区段,直到整个进程空间都满了为止,每个进程可使用4GB的内存。

     

    托管堆上维护着一个指针NextObjPtr,指向下一个对象在托管堆上分配的位置。

     

    托管堆根据存储信息的不同分为垃圾回收堆GC Heap和加载堆Loader Heap。垃圾回收堆GC Heap存储对象实例,受GC管理;加载堆Loader Heap存储AppDomain中的元数据信息,例如基类型、静态字段、静态方法、接口信息等,不受GC管理,它的生命周期从创建AppDomain开始到卸载AppDomain结束。此时,托管堆以及其它方面大致是这样分布的:

     

    1

    →在托管堆上创建对象

     

    □ 引用类型对象创建

     

    使用new关键字创建引用类型对象。

    FileStream fs = new FileStream(@"",FileMode.Open);

     

    以上代码经编译器编译,在生成的中间IL代码中实际上是一个newobj指令。IL相关的指令还包括:ldstr指令用于创建string类型对象,newarr用于分配新的数组对象,box指令用于在值类型转换为引用类型时,将值类型字段拷贝到托管堆上。

     

    通过以下代码来体会托管堆上创建对象的大致过程:
     

     

    public class Employee
    
    {
    
        private int _id;
    
        private Status _status;
    
        public Employee()
    
        {
    
            _id = 1;
    
            _status = new Status();
    
        }
    
    }
    
    public class Sales : Employee
    
    {
    
        public bool _isLive;
    
        public bool IsLive()
    
        {
    
            return _isLive;
    
        }
    
        public static void Main()
    
        {
    
            Sales _sales;
    
            _sales = new Sales();
    
            _sales._isLive = true;
    
            Console.WriteLine(_sales.IsLive());
    
        }
    
    }
    
    public class Status
    
    {
    
        private int _years;
    
        private char _level = "A";
    
    }
    

     

    1、执行Sales _sales,在线程堆栈上开辟4byte的内存空间,用于保存Sales对象的托管堆地址,此时为null。


    2、_sales = new Sales(),递归计算Sales实例对象的字节总数,递归从Sales本身开始,到父类Employee,一直到基类System.object,具体字节计算过程如下:

     

    Sales对象实例字节数=字段_isLive的字节数为1byte
    Sales对象实例的附加成员TypeHandler字节数=4byte
    Sales对象实例的附加成员SyncBlockIndex字节数=4byte
    Sales对象实例的父类Employee字节数=字段_id的字节数4byte + 字段_status保存指向Status对象实例的引用为4byte

    1+4+4+4+4=17byte,考虑到内存块总是按照4byte进行内存对齐,因此补齐到20byte。

     

    由于在Sales的父类Employee的构造函数中,实例化了一个Status类,所以还需要计算Status对象实例的字节数:
    Status对象实例字节数=字段_years的字节数4byte + 字段_level的字节数2byte
    Status对象实例的附加成员TypeHandler字节数=4byte
    Status对象实例的附加成员SyncBlockIndex字节数=4byte
    4+1+4+4=13byte,考虑到内存块总是按照4byte进行内存对齐,因此补齐到16byte。

     

    综上,创建Sales对象实例所需总字节数为36。

     

    3、在托管堆上向高地址扩展出36byte的连续空间,并为其分配内存地址。

     

    4、进行Sales对象实例的初始化工作。

     

    a:构造Sales类型的Type对象,并分配到托管堆中的Loader Heap上。
    b:初始化Sales对象实例的2个附加成员:TypeHandler和SyncBlockIndex。TypeHandler指向Load Heap上的Method Table,SyncBlockIndex指向Synchronization Block内存块,用于在多线程环境下对实例对象的同步操作。
    c:实例字段的初始化,先初始化System.Object的实例字段,然后是Employee,最后是Sales的实例字段。

     

    5、将Sales实例对象的托管堆地址赋值给线程栈变量_sales。


    □ 值类型对象创建

     

    对于值类型(不包括类中值类型实例成员),比如struct,通过new关键字创建,实际上是在线程栈上创建了对象,线程堆栈不受CLR的垃圾回收控制,而是由操作系统直接管理,当值类型实例所在方法结束时,其存储单位被自动释放。在线程栈上,操作系统维护者一个堆栈指针,指向下一个自由空间的地址。

     

    线程堆栈的内存地址是由高位向低位填充,也就是入栈时由高地址向低地址扩展,出栈时由低地址向高地址删除内存。通过以下代码来走一遍整个过程:

    public void SaySth()
    
    {
    
        int a = 1;
    
        char b = 'x';
    
    }

    1、方法执行之前,线程堆栈指针指向一个高位,比如100
    2、开始执行SaySth方法,SaySth方法的返回地址首先入栈,线程堆栈指针指向一个较低位置,比如99
    3、执行int a = 1,在线程堆栈上分配4byte的内存空间,将值1保存在该地址空间内,线程堆栈指针向下移动4个字节指向一个较低位置,比如98
    4、执行char b = 'x',在线程堆栈上分配2byte的内存空间,将值"x"保存在该地址空间内,线程堆栈指针向下移动4个字节指向一个较低位置,比如97
    5、方法执行结束,依次上次b和a的内存,线程堆栈指针回到返回地址,即99

     

    →垃圾回收

     

    在托管堆上,设计了一个叫作"GC roots",这些内存空间总是可到达的,存储着对象实例的引用,"GC roots"中的这些对象被标记为"live",所有被"GC root"引用的对象实例也被标记为"live",GC会按照这种方式一直循环遍历下去,直到某个对象实例没有引用对象实例。这就好像一颗"节点树",从根节点开始遍历,一直到没有子节点的节点循环遍历才结束。

     

    在垃圾回收的时候,所有没有被标记为"live"的对象实例都会回收,然后托管堆的内存空间会重新压缩,托管堆低位的内存空间准备迎接下一批实例对象。

     

    "GC root"有不同的类型:
    ● 当前方法内的局部变量被视作"GC root"
    ● 静态变量也被视作"GC root"
    ● 如果一个托管对象实例需要被传递给COM+库,也会被视作"GC root"
    ● 如果一个对象有析构器,在垃圾回收的时候是不会被回收的,只有调用finalizer的时候才对该对象进行回收,也就是说,带有析构器的对象也被视作"GC root"

     

    为了提高垃圾回收的效率,GC引入了一个"代龄"策略,将托管堆中的对象分为三代,分别为0,1和2。而且会为不同的代龄设置不同的阙值容量,第0代大约256KB,第1代大约2MB,第2代大约10MB。被新建的对象起初都是第0代,GC也总是回收第0代没有被标记为"live"的对象实例。具体运作过程如下:

     

    1、CLR初始化时,所有被添加到托管堆中的对象为第0代。
    2、当有垃圾回收时,未被回收的对象代龄提升1级,变为第1代。
    3、第1代对象如果没有达到阙值容量,就不会被回收,这样有效地提高了垃圾回收的效率。
    4、仅当第0代阙值容量不足以创建新的对象,同时第1代对象实例的大小超出了第1代阙值容量,GC会同时回收第0代和第1代的对象,还未被回收的第1代对象升级为2代对象,还未被回收的第0代对象升级为1代对象。
    5、如此反复。

     

    →非托管资源的清理

    对于一些托管资源,CLR的垃圾回收器能帮我们自动清理内存,而对于一些非托管资源,比如数据库连接、文件句柄、COM对象等,仍然需要开发者手动清理。具体请参考这里

     

     

  • 相关阅读:
    朴素贝叶斯分类器实现
    Puppeteer使用
    神经网络常用名词
    Mysql binlog的基本使用和数据库恢复步骤
    webpack之代码分割及页面缓存优化
    webpack之常用loader的配置和使用
    webpack之常用plugin的配置和使用
    第11章 面向对象
    第10章 面向对象
    第9章 模块与包
  • 原文地址:https://www.cnblogs.com/darrenji/p/4282718.html
Copyright © 2020-2023  润新知