zoukankan      html  css  js  c++  java
  • [学习笔记].NET中的内存分析

    参考:

    一:《你必须知道的.NET》电子工业出版社

    二:对.Net 垃圾回收Finalize 和Dispose的理解

     

    .NET中的内存分配

     

    几个基本概念:

    TypeHandle:类型句柄,指向对应的方法表。每个对象创建时都包含该附加成员。每个类型都对应于一个方法表,方法表创建于编译时,主要包含了类型的特征信息、实现的接口数等等。

     

    SyncBlockIndex:用于线程同步,每个对象创建时也包含该附加成员,它指向一块被称为SyncBlockIndex的内存块,用于管理同对象同步。

     

    NextObjPtr, 由托管堆维护的一个指针,用于标识下一个新建对象分配时在托管堆中所处的位置。CLR初始化时,NextObiPtr位于托管堆的基地址。

     

    托管堆

    对32位处理器来说,应用程序完成进程初始化后,CLR 将在进程的可用地址空间上分配一块保留的地址空间,它是进程(每个进程可使用 4GB)中可用地址空间上的一块内存区域,但并不对应于任何物理内存,这块地址空间即是托管堆。

    托管堆又根据存储信息的不同划分为多个区域,其中最重要的是垃圾回收堆(GC Heap)和加载堆(Loader Heap)。

    GC Heap 用于存储对象实例,受 GC管理;

    Loader Heap 用于存储类型系统,又分为High-Frequency Heap、Low-Frequency Heap和 Stub Heap,不同的堆上存储不同的信息。Loader Heap最重要的信息就是元数据相关的信息,也就是 Type 对象,每个Type 在Loader Heap上体现为一个Method Table(方法表),而Method Table 中则记录了存储的元数据信息,例如基类型、静态字段、实现的接口、所有的方法等等。Loader Heap 不受GC控制,其生命周期为从创建到AppDomain卸载。

     

    值类型一般创建在线程的堆栈上。

    引用类型的实例分配于托管堆上。

    引用类型内存分配过程

    示例如下:

    using System;
    
    public class UserInfo
    {
        private Int32 age = -1;
        private char level = 'A';
    }
    
    public class User
    {
        private Int32 id;
        private UserInfo user;
    }
    
    public class VIPUser : User
    {
        public bool isVip;
        public bool IsVipUser()
        {
            return isVip;
        }
    
        public static void Main()
        {
            VIPUser aUser;
            aUser = new VIPUser();
            aUser.isVip = true;
            Console.WriteLine(aUser.IsVipUser());
        }
    
    }
    

     

    分配过程剖析:

    首先,将声明一个引用类型变量 aUser:

    VIPUser aUser;

    它仅是一个引用(指针),保存在线程的堆栈上,占用 4Byte 的内存空间,将用于保存 VIPUser对象的有效地址,此时 aUser未指向任何有效的实例,因此被自行初始化为 null,试图对 aUser 的任何操作将抛出 NullReferenceException异常。

     

     

    接着,通过new操作执行对象创建:

    aUser = new VIPUser();

    其过程又可细分为以下几步:

    (a)CLR按照其继承层次进行搜索,计算类型及其所有父类的字段,该搜索将一直递归到 System.Object类型,并返回字节总数。以本例而言类型 VIPUser 需要的字节总数为15Byte,具体计算为:VIPUser类型本身字段 isVip(bool型)为1Byte;父类User 类型的字段 id(Int32 型)为 4Byte,字段user 保存了指向 UserInfo 型的引用,因此占 4Byte,而同时还要为 UserInfo 分配6Byte

    字节的内存。

    (b)实例对象所占的字节总数还要加上对象附加成员所需的字节总数,其中附加成员包括TypeHandle和SyncBlockIndex,共计 8字节(在 32位 CPU平台下)。因此,需要在托管堆上分配的字节总数为23 字节,而堆上的内存块总是按照4Byte的倍数进行分配,因此本例中将分配24字节的地址空间。

    (c)CLR 在当前 AppDomain对应的托管堆上搜索,找到一个未使用的24字节的连续空间,并为其分配该内存地址。事实上,GC使用了非常高效的算法来满足该请求,NextObjPtr指针只需要向前推进24 个字节,并清零原 NextObjPtr指针和当前 NextObjPtr 指针之间的字节,然后返回原NextObjPtr指针地址即可,该地址正是新创建对象的托管堆地址,也就是 aUser引用指向的实例地址。而此时的NextObjPtr 仍指向下一个新建对象的位置。

    注意,栈的分配是向低地址扩展,而堆的分配是向高地址扩展。

    在上述操作时,如果试图分配所需空间而发现内存不足时,GC将启动垃圾收集操作来回收垃圾对象所占的内存。

     

     

    最后,调用对象构造器,进行对象初始化操作,完成创建过程。

    该构造过程,又可细分为以下几个环节:

    (a)构造VIPUser 类型的 Type对象,主要包括静态字段、方法描述、实现的接口等,并将其分配在上文提到托管堆的 Loader Heap上。

    (b)初始化 aUser的两个附加成员:TypeHandle和SyncBlockIndex。将TypeHandle 指针指向Loader Heap上的MethodTable,CLR 将根据 TypeHandle 来定位具体的 Type;将 SyncBlockIndex 指针指向Synchronization Block 的内存块,用于在多线程环境下对实例对象的同步操作。

    (c)调用 VIPUser 的构造器,进行实例字段的初始化。实例初始化时,会首先向上递归执行父类初始化,直到完成System.Object类型的初始化,然后再返回执行子类的初始化,直到执行 VIPUser类为止。以本例而言,初始化过程首先执行 System.Object类,再执行 User类,最后才是VIPUser类。最终,newobj分配的托管堆的内存地址,被传递给 VIPUser 的this 参数,并将其引用传给栈上声明的aUser。

     

    上述过程,基本完成了一个引用类型创建、内存分配和初始化的整个流程,然而该过程只能看作是一个简化的描述,实际的执行过程更加复杂,涉及一系列细化的过程和操作。对象创建并初始化之后,内存的布局,可以表示为下图。

    clip_image001[4]

     

    其余种种

    对于值类型嵌套引用类型的情况,引用类型变量作为值类型的成员变量,在堆栈上保存该成员的引用,而实际的引用类型仍然保存在 GC堆上;

    对于引用类型嵌套值类型的情况,则该值类型字段将作为引用类型实例的一部分保存在 GC堆上。

    方法调用过程

    如上所言,MethodTable中包含了类型的元数据信息,类在加载时会在 Loader Heap上创建这些信息,一个类型在内存中对应一份 MethodTable,其中包含了所有的方法、静态字段和实现的接口信息等。对象实例的 TypeHandle在实例创建时,将指向MethodTable 开始位置的偏移处(默认偏移12Byte)。通过对象实例调用某个方法时,CLR 根据TypeHandle 可以找到对应的 MethodTable,进而可以定位到具体的方法,再通过 JIT Compiler 将IL 指令编译为本地 CPU指令,该指令将保存在一个动态内存中,然后在该内存地址上执行该方法,同时该 CPU指令被保存起来用于下一次的执行。

    静态字段的内存分配和释放

    静态字段也保存在方法表中,位于方法表的槽数组后,其生命周期为从创建到 AppDomain卸载。因此一个类型无论创建多少个对象,其静态字段在内存中也只有一份。静态字段只能由静态构造函数进行初始化,静态构造函数确保在任何对象创建前,或者在任何静态字段或方法被引用前执行,其详细的执行顺序在 7.8 节“动静之间:静态和非静态”有所讨论。

     

    .NET的垃圾回收机制:

    CLR管理内存的区域主要有三块:

    一:

    线程的堆栈 ,用于分配值类型实例。堆栈主要有操作系统管理,不受垃圾收集器的控制,当值类型实例所在的方法结束时,其存储单位自动释放。栈的执行效率高,但存储容量有限。

    二:

    GC堆,用于分配小对象实例。如果引用类型对象的实例小于85000字节,实例将被分配在GC堆上。当有内存分配或者回收时,垃圾收集器会对GC对进行压缩。

    三:

    LOH(Large object heap),用于分配大对象实例。LOH堆不会被压缩,而且只有在完全GC回收时才会被回收,这种设计方案是对垃圾回收性能的优化考虑。

    GC如何判断某个对象为垃圾

    每个应用程序有一组根(指针),根指向托管堆中的存储位置,由 JIT编译器和 CLR运行时维护根指针列表,主要包括全局变量、静态变量、局部变量和寄存器指针等。

    当垃圾收集器启动时,它假设所有对象都是可回收的垃圾,并开始遍历所有的根,将根引用的对象标记为可达对象添加到可达对象图中,在遍历过程中,如果根引用的对象还引用着其他对象,则该对象也被添加到可达对象图中,依次类推,垃圾收集器通过根列表的递归遍历,将能找到所有可达对象,并形成一个可达对象图。同时那些不可达对象则被认为是可回收对象,垃圾收集器接着运行垃圾收集进程来释放垃圾对象的内存空间。

    垃圾收集器何时启动:

    (1)内存不足溢出时,更确切地应该说是第 0代对象充满时。

    (2)调用 GC.Collect 方法强制执行垃圾回收。

    (3)Windows报告内存不足时,CLR 将强制执行垃圾回收。

    (4)CLR 卸载AppDomain 时,GC将对所有代龄的对象执行垃圾回收。

    (5)其他情况,例如物理内存不足,超出短期存活代的内存段门限,运行主机拒绝分配内存

    等等

    何为紧缩

    GC在垃圾回收之后,堆上将出现多个被收集对象的“空洞”,为避免托管堆的内存碎片,会重新分配内存,压缩托管堆,此时 GC可以看出是一个紧缩收集器,其具体操作为:GC找到一块较大的连续区域,然后将未被回收的对象转移到这块连续区域,同时还要对这些对象重定位,修改应用程序的根以及发生引用的对象指针,来更新复制后的对象位置。

    关于Generation(代龄)的详细情况请参考《你必须知道的.NET》第五章,P187

     

    非托管资源的回收

    GC全权负责了对托管堆的内存管理,然而除了内存,还有一些非托管资源比如数据库链接、文件句柄、网络链接、互斥体、COM对象、套接字、位图和GDI+对象等, 这些GC是不管的。

    非托管资源的清理,主要有两种方式:Finalize 方法和 Dispose 方法。

    Finalize方式

    其实就是用析构函数来释放资源

    例如

    class GCApp: Object 
    { 
    	~GCApp() 
    	{ 
    		//执行资源清理 
    	} 
    }

    该析构函数会被编译成一个Finalize()方法。

    Finalize方法调用时,会强制调用父类的FInalize方法。

    若某个类有析构函数,则当GC回收该对象之前,会自动执行其析构函数来清理非托管资源,否则直接将该对象从内存清除。

    若一个对象调用了GC.SuppressFinalize()方法,则GC在清理它之前不会再调用它的析构函数。

    Finalize方式存在很多弊端

    比如

     

    调用Finalize()的时间无法控制,只在GC决定要回收该对象时才调用其析构函数,这导致一些资源比如文件句柄长时间被占据。

    极大地损伤性能,GC使用一个终止化队列的内部结构来跟踪具有 Finalize方法的对象。当重写了Finalize方法的类型在创建时,要将其指针添加到该终止化队列中,由此对性能产生影响。

    因此一般情况下在自定义类型中应避免重写 Finalize 方法,而通过 Dispose 模式来完成对非托管资源的清理

     

    Dispose模式

    其实就是某个类实现 System.IDisposable接口。

    该接口中定义了一个公有无参的 Dispose 方法,用户在该方法中清理非托管资源。

    Dispose方法需要用户显式的调用才能被执行。这保证了用户能够及时地释放非托管资源,而不必等到GC回收该对象之前才释放资源。

    例子

    class MyDispose : IDisposable 
    { 
        //实现IDisposable接口 
        public void Dispose() 
        { 
            //释放资源
        } 
    } 

    派生类中实现Dispose模式,应该重写基类的受保护Dispose方法,并且通过base调用基类的Dispose方法,以确保释放继承链上所有对象的引用资源,在整个继承层次中传播 Dispose模式。

        protected override void Dispose(bool disposing)
        {
            if (!disposed)
            {
                try
                {
                    //子类资源清理 
                    //...... 
                    disposed = true;
                }
    
                finally
                {
                    base.Dispose(disposing);
                }
            }
        } 

    最佳策略

    最佳的资源清理策略,应该是同时实现 Finalize 方式和 Dispose方式。

    一方面,Dispose方法可以克服Finalize 方法在性能上的诸多弊端;另一方面,Finalize 方法又能够确保没有显式调用 Dispose 方法时,也自行回收使用的所有资源。

    例子

    我们模拟一个简化版的文件处理类 FileDealer,其中涉及对文件句柄的访问

    using System;
    using System.Runtime.InteropServices;
    
    class FileDealer : IDisposable
    {
        //定义一个访问文件资源的 Win32句柄 
        private IntPtr fileHandle;
    
        //定义引用的托管资源 
        private ManagedRes managedRes;
    
        //定义构造器,初始化托管资源和非托管资源 
        public FileDealer(IntPtr handle, ManagedRes res)
        {
            fileHandle = handle;
            managedRes = res;
        }
    
        //实现终结器,定义Finalize 
        ~FileDealer()
        {
            if (fileHandle != IntPtr.Zero)
            {
                Dispose(false);//确保当没有显式调用Dispose时,在对象被GC销毁前也会调用Dispose方法。
            }
        }
    
        //实现IDisposable接口 
    
        public void Dispose()
        {
            Dispose(true);
            //阻止GC调用Finalize方法
            GC.SuppressFinalize(this);
        }
    
        //实现一个处理资源清理的具体方法 
        protected virtual void Dispose(bool disposing)
        {
            if (disposing)
            {
                //清理托管资源 
                managedRes.Dispose();
            }
    
            //执行资源清理,在此为关闭对象句柄 
            if (fileHandle != IntPtr.Zero)
            {
                CloseHandle(fileHandle);
                fileHandle = IntPtr.Zero;
            }
        }
    
        public void Close()
        {
            //在内部调用Dispose来实现 
            Dispose();
        }
    
        //实现对文件句柄的其他应用方法 
        public void Write() { }
        public void Read() { }
    
        //引入外部Win32API 
        [DllImport("Kernel32")]
    
        private extern static Boolean CloseHandle(IntPtr handle);
    
    }
    

    这个例子的妙处在于既可以显式调用Dispose方法,又能够保证当我们没有显式地调用Dispose()方法时,GC回收该对象之前也会调用Dispose(false)方法来释放非托管资源。

     

    本例子的说明:

    Dispose方法中,应该使用 GC. SuppressFinalize防止GC调用Finalize方法,因为显式调用Dispose之后就没有必要再让GC调用Finalize()方法了。

     

    公有Dispose方法不能实现为虚方法,以禁止在派生类中重写。

     

    在该模式中,公有Dispose方法通过调用重载虚方法 Dispose(bool disposing)方法来实现,具体的资源清理操作实现于虚方法中。Dispose(true)在Dispose()中调用,Dispose(false)在析构函数中调用,两者的区别在于Dispose(true)表示要将托管资源和非托管资源一并清理掉,而Dispose(false)由于是GC调用finalize()时候被调用,GC已经准备清理这个对象了,所以就不需要再手动地清理托管资源了。

    总结

    Finalize是CLR提供的一个机制, 它保证如果一个类实现了Finalize方法,那么当该类对象被垃圾回收时,垃圾回收器会调用Finalize方法

     

    Dispose(bool disposing)不是CRL提供的一个机制, 而仅仅是一个设计模式。

     

    Finalize由GC自行调用,而Dispose由开发者强制执行调用。

    尽量避免使用Finalize方式来清理资源,必须实现Finalize时,也应一并实现 Dispose方法,来提供显式调用的控制权限。

     

    执行Finalize()的准确时间是不确定的,这取决于GC。

     

    Finalize方法和Dispose方法,只能清理非托管资源,释放内存的工作仍由GC负责。

     

    对象使用完毕应该立即释放其资源,最好显式调用 Dispose方法来实现

     

    using语句

    凡是实现了Dispose模式的类型,均可以 using语句来定义其引用范围.

     

    例如:

    public static void Main() 
    { 
    	using(FileDealer fd = new FileDealer(new IntPtr(), new ManagedRes())) 
    	{ 
    		fd.Read(); 
    	} 
    } 
    

    当执行流超过了using的代码块之后,fd对象会自动调用Dispose()方法释放资源。

  • 相关阅读:
    html5+css3中的background: -moz-linear-gradient 用法 (转载)
    CentOS 安装Apache服务
    Linux 笔记
    CURL 笔记
    Spring Application Context文件没有提示功能解决方法
    LeetCode 389. Find the Difference
    LeetCode 104. Maximum Depth of Binary Tree
    LeetCode 520. Detect Capital
    LeetCode 448. Find All Numbers Disappeared in an Array
    LeetCode 136. Single Number
  • 原文地址:https://www.cnblogs.com/ybwang/p/1765117.html
Copyright © 2011-2022 走看看