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#基础-自动内存管理的更多相关文章

  1. JVM自动内存管理:内存区域基础概念

    1.课程概要 (1)Java虚拟机和Java内存区域概述 (2)Java虚拟机栈和本地方法栈 (3)Java堆 (4)方法区和运行时常量池 (5)直接内存 2.Java虚拟机运行时数据区 运行时数据区 ...

  2. JVM自动内存管理学习笔记

    对于使用 C.C++ 的程序员来说,在内存管理领域,他们既是拥有最高权力的皇帝又是从事最基础工作的劳动人民——拥有每一个对象的“所有权”,又担负着每一个对象生命开始到终结的维护责任.对于 Java 程 ...

  3. 垃圾回收算法手册:自动内存管理的艺术 BOOK

    垃圾回收算法手册:自动内存管理的艺术 2016-03-18 华章计算机 内容简介 PROSPECTUS 本书是自动内存管理领域的里程碑作品,汇集了这个领域里经过50多年的研究沉积下来的最佳实践,包含当 ...

  4. 【IOS学习基础】内存管理

    1.内存几大区域 1> 栈区:局部变量(基本数据类型.指针变量). 2> 堆区:程序运行的过程中动态分配的存储空间(创建的对象). 3> BSS段:没有初始化的全局变量和静态变量. ...

  5. JVM自动内存管理-Java内存区域与内存溢出异常

    摘要: JVM内存的划分,导致内存溢出异常的可能区域. 1. JVM运行时内存区域 JVM在执行Java程序的过程中会把它所管理的内存划分为以下几个区域: 1.1 程序计数器 程序计数器是一块较小的内 ...

  6. JVM介绍&自动内存管理机制

    1.介绍JVM(Java Virtual Machine,Java虚拟机) JVM是Java Virtual Machine的缩写,通常成为java虚拟机,作为Java可以进行一次编写,到处执行(Wr ...

  7. JVM自动内存管理机制——Java内存区域(下)

    一.虚拟机参数配置 在上一篇<Java自动内存管理机制——Java内存区域(上)>中介绍了有关的基础知识,这一篇主要是通过一些示例来了解有关虚拟机参数的配置. 1.Java堆参数设置 a) ...

  8. [深入理解Java虚拟机]<自动内存管理>

    Overview 走近Java:介绍Java发展史 第二部分:自动内存管理机制 程序员把内存控制的权利交给了Java虚拟机,从而可以在编码时享受自动内存管理.但另一方面一旦出现内存泄漏和溢出等问题,就 ...

  9. JVM自动内存管理机制--读这篇就GO了

    之前看过JVM的相关知识,当时没有留下任何学习成果物,有些遗憾.这次重新复习了下,并通过博客来做下笔记(只能记录一部分,因为写博客真的很花时间),也给其他同行一些知识分享. Java自动内存管理机制包 ...

随机推荐

  1. 模式识别之贝叶斯---朴素贝叶斯(naive bayes)算法及实现

    处女文献给我最喜欢的算法了 ⊙▽⊙ ---------------------------------------------------我是机智的分割线----------------------- ...

  2. [Hadoop]安装

    1 从官网下载hadoop稳定版 http://www.apache.org/dyn/closer.cgi/hadoop/common/ 2 安装JAVA 参考如下blog http://www.cn ...

  3. NLM算法

    non-Local Means 非局部均值 论文原文:http://www.ipol.im/pub/art/2011/bcm_nlm/?utm_source=doi 论文源代码:http://www. ...

  4. 【SR】MAP

    MAP:最大后验概率(Maximum a posteriori) 估计方法根据经验数据获得对难以观察的量的点估计.它与最大似然估计中的 Fisher方法有密切关系, 但是它使用了一个增大的优化目标,这 ...

  5. poj 3653(最短路)

    题目链接:http://poj.org/problem?id=3653 思路:题目意思很简单,就是二维平面上的图,要求起点到终点的最短路.建图略坑,需要坐标映射,化二维为一维.然后就是Dijkstra ...

  6. java的Date类型转换为MySQL数据库的Date类型

    最近遇到一个问题,需要把java中的日期类型存放为MySQL数据库的日期类型,两个日期之间需要进行转化才能进行存储,转化代码如下: package com.alphajuns.demo1; impor ...

  7. RadioButton ---- 样式效果切换

    \res\drawable\radio_button_bg.xml <?xml version="1.0" encoding="utf-8"?> & ...

  8. homebrew常用指令

    其它Homebrew指令: brew list   —列出已安装的软件 brew update   —更新Homebrew brew home  *—用浏览器打开 brew info   *—显示软件 ...

  9. Spring Cloud Feign组件

    采用Spring Cloud微服务框架后,经常会涉及到服务间调用,服务间调用采用了Feign组件. 由于之前有使用dubbo经验.dubbo的负载均衡策略(轮训.最小连接数.随机轮训.加权轮训),du ...

  10. StackExchange.Redis性能调优

    大家经常出现同步调用Redis超时的问题,但改成异步之后发现错误非常少了,但却可能通过前后记日志之类的发现Redis命令非常慢. PS: 以后代码都在Windows bash中运行,StackExch ...