c#基础-自动内存管理

摘要:
然而,在托管环境中,程序员不必担心这两个问题。在满足某些条件后,CLR垃圾收集器会自动释放不需要的内存。CLR只能管理内存。CLR不知道该类型中使用了哪些本地资源。如果该类型使用特殊资源,程序仍需要手动完成某些操作。正如稍后将介绍的,终止符或Dispose模式。压缩后,NextObjectPtr指针将移动到堆中最后一个非垃圾对象的后面。终结操作是CLR提供的一种机制,它允许您在回收对象的内存之前清理对象。

1.自动垃圾回收是什么?

    在非托管环境下程序员要自已管理内存,由疏忽的原因,通常会犯两种错误,请求内存后在不使用时忘记释放,或使用已经释放了的内存。但在托管环境下,程序员不用担心这两个问题,CLR的垃圾回收器在某种条件达到后自动释放已经不需要的内存,CLR能管理的只有内存,CLR并不知道类型中使用了什么本地资源,如果类型使用了特殊资源(数据库连接,文件,套接字,位图,图标等 ),还是需要程序自已手动完成些什么 的,这在后面会介绍到,终结器或 Disponse模式。
在托管环境下让程序不需要操心这些的就是垃圾回收器。
 
 
2.托管环境下新建对象及内存分配
托管代码申请内存只能 在托管堆中申请,应用程序启动时会为每个应用程序域初始化一个托管堆,托管堆的地址空间是连续的,但这个地址空间不会真的对应物理内存,托管堆还维护一个指针NextObjPtr,这个指针指向下一个对象要分配的内存地址,应用程序刚启动时NextObjPtr指向堆首。
C#中新建一个对象时用的是new关键词,编译器在编译方法时会将其转换成IL代码newobj,newobj会使CLR进行如下操作:
1.计算类型及基类的所有实例字段所需要的字节数。
2.计算出来的大小再加上两个字段的大小(对象类型指针和同步块索引),32位应用程序中每个字段需要32位即共需8个字节,64位应用程序中每个字段需要64位即共需16个字节。
3.在NextObjPtr分配计算出来大小的内存,提交并将其清0,然后调用类型的构造函数,为this传递NextObjPtr,返回newobj的调用,并把对象的地址返回至调用,在newobj返回之前NextObjPtr会加上计算出的对象大小,以备下次分配对象时使用。
 
C在堆中分配内存时会检索一个内部数据结构,发现可以容得下请求内存大小后,就会分配,之后修改内部数据结构保留对内存使用的有效性,如果对比下托管内存的申请会发现还是比较快的,因为内存分配只是在NextObjPtr分配然后将其后移。
 
在程序中,前后分配的对象代表着有可能有某种联系,可以会一起使用,如果分到一个连续的空间,可以同时加载到CPU缓存里提高程序速度,如:FileStream和StreamReader的使用。
 
3.垃圾回收算法
垃圾回收如何检测一个对象已经不在使用了?
应用程序中包含一组根,每一个根都是对对象的引用,只要被根引用垃圾回收就不认为对象是垃圾而回收它。根可能是局部变量,实参,或表态变量。如果一个对象被根对象的某个字段引用也不会认为是垃圾而被回收。只有引用类型的变量才会被认为是根,值类型永远不支被认为是根。
 
CLR在编译方法体时会创建一个内部表,内部表里的每一项包含方法的本地CPU指令中的有效偏移范围,每一项都包含根在内存中的一组地址和CPU寄存器
在进行垃圾回收的时候,CLR假定托管堆中的所有对象是没有被根引用的(即假定他们是垃圾),当一个对象被确认被某个根引用后,CLR会对它进行标记,也就是设置同步块索引块的某一位。
垃圾回收的步骤是:
1.CLR会沿线程栈上行遍历,检查每一个根,如果发象根引用了对象,那么就标记此对象(对所经过的每一个方法调用的内部表进行扫描,如果确定在范围内内部表对对象有引用就标记对象。)
2.如果对象有引用字段,那么将引用字段所引用的对象也进行标记,CLR会递归对象的所有可达字段引用,然后标记。如果CLR发现某一对象标记过则跳过。
3.标记所有类型 的静态引用字段的对象。
 
完成这一系统的动作后所有被标记的对象就是还有用的对象,未被标记的对象就是没被引用会被认定为垃圾,垃圾回收的下一步是压缩,CLR会线性扫描堆,如果发现连续的未被标记的垃圾块,如果找到的垃圾块比较小CLR会跳 过,如果找到比较大的,CLR会把标记过的非垃圾对象移动这里,以进行堆的压缩,压缩堆可以缩小应用程序 的工作 集,当然非垃圾对象移动后对它的所有引用都会失效,CLR在移动了一个变量后会遍历应用程序 的所有根,把他们的指值都指向对象的新地址。在压缩完成后NextObjectPtr指针会被移动堆内最后一个非垃圾对象的后面。
 
 
4.使用终结操作来释放本地资源
前面我们说过了,对象是否还在被使用CLR可以判断出来,并合适的释放掉,但如果对象里有本地资源那么CLR是不知道 怎么释放的,这就需要用到本节的终结操作。
终结操作是CLR提供的一个机制他允许在将对象的内存回收之前可以对对象进行一些清理工作。简单说来对象如果要支持终结操作就是在类内部实现 一个Finalize方法,在C#中定义这个方法 就是在类中定义一个前面有~号的同类名一个的方法。
反汇编后查看IL会发现这个方法被编 译为:protected override Finalize
在终结器方法中一般是关闭一些句柄,如果不关闭这些句柄可能会一直处于打开状态,另的应用或对象就无法使用这些本地资源,一直到应用程序结束为止。
    4.1CriticalFinalizerObject
        这个类位于System.Runtime.ConstrainedExecution命名空间下,它可以在一定程度上确保Finalize方法可以被执行,CLR会特殊的对待继承自这个抽象类的类,具体如下:
            1.首次构造一个CriticalFinalizerObject继承的对象时,JIT会编译所有继承层层次上的Finalize方法。
            2.CLR会先调用非CriticalFinalizerObject派生类的Finalize方法,这就确保了非派生自CriticalFinalizerObject的类可以安全的调用派生自CriticalFincalizerObject类而数据不会丢失,如FileStream可以把缓冲区的数据写入到磁盘
            3.Appdomain被强行中断也会调用派生自CriticalFinalizerObject的Finalize方法 
    4.2SafeHandle
            这个类位于System.Runtime.InteropServices命名空间,为了让编各更简单而存在的。
            继承自CriticalFinalizerObject,并且它是一个抽象类,你需要重写他的ReleaseHandle和IsInvalid.
            因为大多数windows句柄是0或-1都代表着无效,所以他还有一个派生的抽像类以简化I编写sInvalid,SafeHandleZeroOrMinusOneIsInvalid它重写了父类的IsInvalid当handle是0或-1时返回true.
 
    4.3什么导致Finalize方法被调用(什么时候进行垃圾回收)
    下面几中情况下会使CLR开始进行垃圾回收:
  1. 第0代满。
  2. 主动调用GC.Collect()方法
  3. Windows内存不足
  4. CLR卸载AppDomain
  5. CLR关闭
       CLR会使用一个单独的线程去调用每个对象的Finalize方法,所以一个Finalize进入死循环会拖住线程去执行其它对象的Finalize,从而导执内存泄露。
对于第五种情况,每个Finalize方法有2秒的时间 执行,如果大于2秒CLR会直接杀死进程,后面的Finalize也得不到执行,并且所有的Finalize方法执行的总时长也不能 大于40秒,否则进程也会被杀死。
代码可以通过AppDomain.IsFinalizingForUnload方法来判断Finalize方法被调用是否应用程序域被卸载引起的。
也可以调用Environment .HasShutdownStarted 来判断当前程序是否正在被关闭
 
 
5.终结器揭密
在对象被创建时即newobj时,计算对象占用字节数,分配空间后,如果clr发现对象类型中有Finalize方法,会将NextObjectPtr指针放到终结列表。
当垃圾回收时,对象被标记完成后,垃圾对象如果在终结列表里存里,那么对象被转移到freachedable列表里同时,对象被标记,即不再是垃圾对象了,freachedable列表中保存的即是对象的根。
CLR中有专门的线程去遍历freachedable队列,并依次调用对象的Finalize方法,再从freachedable队列中删除对象,这时对象又变成没有根的对象了,这样在下次垃圾回收时对象会被回收,同时因为在终结列表里没有此对象了,所以对象在被回收时不会再被放入freachedable表里而再次调用Finalize
 
控制终结器
GC.SuppressFinalize(Object obj);
这个方法告诉CLR不要再调用obj对象的终结器了。
 
GC.ReRegisterForFinalize(Object obj);
把对象放到终结列表里,在对象被确定为无引用对象时可以调用对象的Finalize方法
 
6.对象代的概念及大对象
代:CLR垃圾回收器采用的一种机制,使用这种机制主要是为了让垃圾回收更快,基于代的垃圾回收总是有以下假设:
  1. 对象越新生命周期越短。
  2. 对象越老生命周期越长。
  3. 回收堆的一部分总是快于回收全部。
新对象总是在第0代,创建,假设第0代内存可以预算为256KB,那么一直创建新对象,如果第0代内存够了256KB后CLR会启动一次垃圾回收,把标记对象,去除垃圾对象压缩内存,并把剩余的0代内存提升成第1代,第0代空出来接收新的对象。
 
随着程序的运行,新对象继续被创建,新创建的对象依然会被分配到第0代,当第0代满时垃圾回收器启动,,假设第1代内存预算为2M,垃圾回收器发现第1代内存没有满,所以会只清理第0代,然后第0代对象变成第1代,第1代的内存继续扩大。
 
 
假设现在第1代内存现在到达2M,程序继续运行,新对象被分配到第0代,当第0代满时,CLR发现第1代和第0代都满了,这时CLR会回收这两代,第1代会提升成第2代,第0代会提升至第1代。
 
目前已经发生了很多次垃圾回收,第0代满时只回收第0代,只有当第0代和第1代满时才会回收这两代的内存。CLR托管堆只支持三代,0,1,2。。每一代都有个初始预算大小,第一代256KB,第二代2M,第三代10M,但预算是会动态调节的并非是固定的,但预算越小意味着垃圾回收越频繁。
 
你可以通过GC.MaxGeneration得到最大代数,在CLR中这个属性返回2
对象大于85000(可能会变化)被认定为大对象,大对象将直接放到第2代上
    
 
 
7.Disponse模式,强制对象清理资源
Finalize方法固然可以清理资源,但这种清理只是被动的,资源只有在垃圾回收器运行后才会被清理,比如一个数据库连接,通常在查询完后要马上关闭,如果只是用终结器的话,数据库的连接可能要等 到下次垃圾回收时才能 关闭,如果程序创建对象较少垃圾回收可能要等很长时间才会运行。
这时Disponse模式就比较好用了,C#中实现了IDisponse接口的类就应用了Disponse模式。实现Disponse模式的类提供了一个Disponse方法,当资源不再使用时可以调用这个方法以实现对资源的清理,另外通常实现了Disponse的类,也会实现Close方法,其跟调用Disponse是一样的。
 
注意1:如果类型的资源已经被清理,再调用类中的成员应该抛出一个System.ObjectDisponsedException异常,但在多次调用Disponse方法时不应抛出这个异常 。
注意2:通常先建立FileStream后创建StreamWriter,来向文件写入数据,它他两个都有缓存,而StreamWriter依赖FileStream,它们两个都实现了终结器,但终结器没有先后调用顺序,如果先终结了FileStream而关闭了文件句柄,再终结StreamWriter它再向基础流写数据时就会报错。
注意3:在注意2中不必显式调用FileStream的Disponse或Close,因为StreamWriter的Disponse会帮你调用,不过这时你再调用Disponse也不会出错,而是直接返回。
 
8.监视和控制对象的生命周期
CLR为每个AppDomain维护一个GC句柄表,这个表允许程序可以监控对象的生命周期,或控制对象的生命周期。这个表里包括两个信息,1.一个引用对象的地址,2.一个标志,它指示是监控对象还是控制对象。
对象生命期的控制要用到GCHandle类这个类,这个类在System.Runtime.InteropServices空间下,定义大概如下:
 
    public struct GCHandle
    {
        public static bool operator !=(GCHandle a, GCHandle b);
        public static bool operator ==(GCHandle a, GCHandle b);
        public static explicit operator IntPtr(GCHandle value);
        public static explicit operator GCHandle(IntPtr value);
        public bool IsAllocated { get; }
        public object Target { get; set; }
        [SecurityCritical]
        public IntPtr AddrOfPinnedObject();
        [SecurityCritical]
        public static GCHandle Alloc(object value);
        [SecurityCritical]
        public static GCHandle Alloc(object value, GCHandleType type);
        [SecurityCritical]
        public void Free();
        [SecurityCritical]
        public static GCHandle FromIntPtr(IntPtr value);
        public static IntPtr ToIntPtr(GCHandle value);
    }
 
通过Alloc方法将一个对象的引用地址放入GC句柄表,GCHandleType的枚举值大概有以下几种类型:
  1. GCHandleType.Normal;
    控制对象的生命周期,调用Alloc时的默认值,这个标志告诉垃圾回收器即使没有根引用对象,该对象也必须保留在内存中,但对象的位置可以移动。
  2. GCHandleType.Pinned;
    控制对象的生命周期,调用Alloc时的默认值,这个标志告诉垃圾回收器即使没有根引用对象,该对象也必须保留在内存中,但对象的位置是固定的,不可移动,此标志常用来进行P/invoke时固定对象的地址。
  3. GCHandleType.Weak;
    监视对象的生命周期,可判断垃圾回收器什么 时候认为对象不可达,对象有可能执行了Finalize也有可能没执行。
  4. GCHandleType.WeakTrackResurrection
    监视对象的生命周期,可判断垃圾回收器什么 时候认为对象不可达,对象一定执行了Finalize。
CLR是如下应用GC句柄表的:
垃圾回收阶段,垃圾回收器标识可达对象。
垃圾回收器扫描GC句柄表,标志为Normal或Pinned的项会被认为是对象的根,并将对象进行标记。
垃圾回收器扫描句柄表,如果一个标记了Weak的项引用了一个未标记的对象,那么对这个句柄项会被设为null
垃圾回收器扫描终结列表,将其中未标记的对象移至freacheable列表中,并重新标记为对象可达。
垃圾回收器扫描GC句柄表,如果标志为Pinned的引用地址对象未被标记,那么对这个句柄项引用的地址设为null
 
通过GCHandleType.Weak标记可以实现弱引用,当GCHandle引用的一个对象即Target为null时此对象即已经被清理了
可以用GCHandle的Free方法,清理句柄表里的某一项。
 
 
9.其它
可以用GC.Collect(Int32 Generation)来手动进行垃圾回收。
GC.GetGeneration(Object obj)可以用来得到对象是第几代
GC.GetTotalMemory(bool forceFullCollection);
GC.CollectionCount(Int32 generation);返回指定代已经垃圾回收的次数
GC.WaitForPendingFinalizers();挂起调用线程,直到终结队列(freacheable队列)处理完成
 
 
10.疑问函数
GC.WaitForFullGCApproach();
GC.WaitForFullGCComplete()
GC.CancelFullGCNotification()
           
            
 
 
 
 

免责声明:文章转载自《c#基础-自动内存管理》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇Http请求报头设置(C#)Vba实现解析json数据。当中的关于Set oSC = CreateObject("MSScriptControl.ScriptControl") 不能创建对象的问题。下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

Windows录音API学习笔记--转

Windows录音API学习笔记 结构体和函数信息  结构体 WAVEINCAPS 该结构描述了一个波形音频输入设备的能力。 typedef struct {     WORD      wMid; 用于波形音频输入设备的设备驱动程序制造商标识符。     WORD      wPid; 声音输入设备的产品识别码。     MMVERSION vDrive...

Linux 内存工作机制

内存工作的概述 Linux 内核给每个进程都提供了一个独立的虚拟地址空间,并且这个地址空间是连续的。这样,进程就可以很方便地访问内存,更确切地说是访问虚拟内存。 虚拟地址空间的内部又被分为内核空间和用户空间两部分,不同字长(也就是单个 CPU 指令可以处理数据的最大长度)的处理器,地址空间的范围也不同。比如最常见的 32 位和 64 位系统 通过这里可以...

Android 内存管理介绍

极力推荐Android 开发大总结文章:欢迎收藏程序员Android 力荐 ,Android 开发者需要的必备技能 Android Runtime(ART)和Dalvik虚拟机使用 分页 和 内存映射 来管理内存。 这意味着应用程序修改的任何内存(无论是通过分配新对象通过映射页面)都将保留在RAM中,并且不能被分页。 应用程序释放内存的唯一方法是释...

C#获得窗口控件句柄

/*整个Windows编程的基础。一个句柄是指使用的一个唯一的整数值,即一个4字节(64位程序中为8字节)长的数值,来标识应用程序中的不同对象和同类中的不同的实例,诸如,一个窗口,按钮,图标,滚动条,输出设备,控件或者文件等。应用程序能够通过句柄访问相应的对象的信息,但是句柄不是指针,程序不能利用句柄来直接阅读文件中的信息。如果句柄不在I/O文件中,它是毫...

Windows核心编程句柄和伪句柄 CHRIS

GetCurrentProcess(), DuplicateHandle() Window中为什么会有句柄的概念: 从Visual C++的头文件来看,HANDLE被typedef为void的指针,那是指向未确定数据结构的指针:typedef void* HANDLE;但是这并不说明任何问题,因为句柄远远不只是指向任意数据类型的指针。它是指向数据对象指针的...

NETTY4中的BYTEBUF 内存管理

转 http://iteches.com/archives/65193 Netty4带来一个与众不同的特点是其ByteBuf的重现实现,老实说,java.nio.ByteBuf是我用得很不爽的一个API,相比之下,通过维护两个独立的读写指针,io.netty.buffer.ByteBuf要简单不少,也会更高效一些。不过,Netty的ByteBuf带给我们的...