托管堆和垃圾回收

来源:互联网 发布:gui config.json下载 编辑:程序博客网 时间:2024/05/16 12:11


  托管堆基础

 1.调用IL指令newobj,为代表资源的类型分配内存。
  2.初始化内存,设置资源的初始状态并使资源可用。类型的实例构造器负责设置初始状态。
  3.访问类型的成员来使用资源(有必要可以重复)。
  4.摧毁资源的状态以进行清理。
  5.释放内存。垃圾回收器独自负责这一步。

托管堆分配资源

 CLR要求所有对象都从托管堆分配。进程初始化时,CLR划出一个地址空间区域作为托管堆,CLR还要维护一个指针,我把它称作NextObjPtr。该指针指向下一个对象在堆中的分配位置。刚开始的时候,NextObjPtr设为地址空间区域的基本地址。
  一个区域被非垃圾对象填满后,CLR会分配更多的区域。这个过程一直重复,直至整个进程地址空间都被填满。所以,你的应用程序的内存受进程的虚拟地址空间的限制。32位进程最多能分配1.5GB,64位进程最多能分配8TB。

 C#的new操作符导致CLR执行以下步骤。
 1.计算类型的字段(以及从基类型继承的字段)所需的字节数。
 2.加上对象的开销所需的字节数。每个对象都有两个开销字段:类型对象指针和同步块索引。对于32位应用程序,这两个字段各自需要32位,所以每个对象都要8字节。对于64位应用程序,这两个字段各自需要64位,所以每个对象要增加16个字节。
 3.CLR检查区域中是否有分配对象所需的字节数。如果托管堆有足够的可用空间,就在NEXTOBJPTR指针指向的地址处放入对象,为对象分配的字节会被清零。接着调用类型的构造器,NEW操作符返回对象引用。就在返回这个引用之前,NEXToBJpTtr指针的值会加上对象战胜的字节数来得到一个新值,即下个对象放入托管堆时的地址。

垃圾回收算法

    应用程序调用new操作符创建对象时,可能没有足够地址空间来分配该对象,发现空间不够,CLR就执行垃圾回收。
    至于对象生存期的管理,有的系统采用的是某种引用计数算法。Microsoft自己的“组件对象模型”用的就是引用计算。在这种系统中,堆上的每个对象都维护着一个内存字段来统计程序中多少“部分”正在使用对象。随着每一“部分”到达代码中某个不再需要对象的地方,就递减对象的计算字段。计数字段成0,对象就可以从内存中删除了。许多引用计数系统最大的问题是处理不好循环引用。
    鉴于引用计数垃圾回收器算法存在的问题,CLR改为使用一种引用跟踪算法。引用跟踪算法中关心引用类型的变量,因为只有这种变量才能引用堆上的对象;值类型变量直接包含值类型实例。引用类型变量可在许多场合使用,包括静态和实例字段,或者方法的参数和局部变量。我们将所有引用类型的变量都称为根。
  CLR开始GC时,首先暂停进程中的所有线程。这样可以防止线程在CLR检查期间访问对象并更改其状态。然后,CLR进入GC的标记阶段。在这个阶段,CLR遍历堆中的所有对象,将同步块索引字段中的一位设为0。这表明所有对象都应删除。然后,CLR检查所有的活动根,查看它们引用了哪些对象。这正是CLR的GC称为引用跟踪GC的原因。如果一个根包含NULL,CLR忽略这个根并继续检查下一个根。
    任何根如果引用了堆上的对象,CLR都会标记那个对象,也就是将该对象的同步块索引中的位设为1。检查完毕后,堆中的对象要么已标记,要么未标记。已标记的对象不能被垃圾回收,因为至少有一个根在引用它。我们说这种对象是可达的。因为应用程序代码可通过仍在引用它的变量抵达(访问)它。未标记的对象是不可达的。因为应用程序中不存在使对象能被再次访问的根。
    CLR知道哪些对象可以幸存,哪些可以删除后,就进入GC的压缩阶段。在这个阶段。CLR对堆中标记的对象进行“乾坤大挪移”,压缩所有幸存下来的对象,使它们占连续的内存空间。这样做有许多好处。首先,所有幸存对象在内在中紧掺在一起,恢复了引用的“局部化”,减小了应用程序的工作集,从而提升了将来访问这些对象时的性能。
  如果CLR在一次GC之后回收不了内存,而且进程中没有空间来分配新的GC区域,就说明进程的内存已耗尽。此时,试图分配更多内存的new操作符会抛出出异常

垃圾回收和调试

  一旦根离开作用域,它引用的对象就会变得“不可达”,GC会回收其内存;不保存对象在方法的生存期中自始自终的存活。

  static void Main(string[] args)
        {
            Timer t = new Timer(TimerCallBack,null,0,2000);
            Console.ReadLine();
        }
        private static void TimerCallBack(object o)
        {
            Console.WriteLine("In timeCallback:" + DateTime.Now);
            GC.Collect();
        }
        观察代码,可能以为TimerCallBack方法每隔2000毫秒调用一次,毕竟,代码创建了一个Timer对象,而且有一个变量T引用该对象。只要计时器对象存在,计时器就应该一直触发。但要注意,TimerCallBack方法调用GC.Collect()强制执行了一次垃圾回收。
   回收开始时,垃圾回收器首先假定堆中所有的对象都是不可达的(垃圾);这自然也包括Timer对象。然后,垃圾回收器检查应用程序的根,发现在初始化后,Main方法再也没有用过变量t。既然应用程序没有任何变量引用timer对象,垃圾回收自然会回收分配给它的内存,这使计时器停止触发,并解释了为什么TimerCallBack方法只被调用了一次。
   使用C#编译器的/debug开关编译程序集时,编译器会应用System.Diagnostics.DebuggableAttribute,并为结果程序集设置DebuggingModes的DisableOptiimizations标志。运行时编译方法时,JIT编译器看到这个标志,会将所有根的生存期延长至方法结束。在我的例子中,JIt编译器认为Main的T变量必须存活至方法结束,所以在垃圾回收时,GC认为T仍然是一个根,t引用的对象仍然“可达”。Timer对象会在回收中存活,TimerCallBack方法会被反复调用,甚至Console.ReadLine方法返回而且Main方法退出。

 垃圾回收触发条件

       前面说过,CLR在检测第0代超过预算时触发一次GC。这是GC最常见的触发条件,下面列出其他条件。
       1.代码显示调用System.GC的静态Collect方法
代码可显示请求CLR执行回收。虽然Microsoft强烈反对这种请求,但有时情势比人强。
       2.Windows报告低内存情况
CLR 内部WIN32函数CreateMemoryResourceNotification和QueryMemoryResourceNotification监视系统的总体内存使用情况。如果Windows报告低内存,CLR将强制垃圾回收以释放死对象,减小进程工作集。
       3.CLR正在卸载AppDomain
一个AppDomain卸载时,ClR认为其中一切都不是根,所以执行函盖所有代的垃圾回收。
       4.CLR正在关闭
CLR 在进程正常终止时关闭。关闭期间,CLR认为进程中一切都不是根。对象有机会进行资源清理,但CLR不会试图压缩或释放内存。整个进程都要终止了,Winodws将回收进程的全部内存。

 垃圾回收模式 

       1.工作站
        该模式针对客户端应用程序优化GC。GC造成的延时很低,应用程序线程挂起时间会很短,避免使用户感到焦虑。在该模式中,GC假定机器运行的其他应用程序都不会消耗太多的CPU资源。

      2.服务器
       该模式针对服务器端应用程序优化GC。被优化的主要是吞叶量和资源利用。GC假定机器上没有运行其他应用程序,并假定机器的所有CPU都可用来辅助完成GC。该模式造成托管堆被拆分成几个区域,每个CPU一个。开始垃圾回收时,垃圾回收器在每个CPU上都运行一个特殊线程;每个线程都和其他线程并发回收它自己的区域。对于工作者线程行为一致的服务器应用程序,并发回收能很好地进行。这个功能要求应用程序在多CPU计算机上运行,使线程能真正地同时工作,从而获得性能的提升。

     
 







0 1
原创粉丝点击