ly-newyear

导航

.NET读书学习笔记整理----垃圾回收

以下呢我个人整理的读书笔记,我主要看了三本书分别是:

  Andrew   Troelsen《C#与.NET3.5高级程序设计(第四版)》

  Jeffrey   Richter 《Microsoft.NET框架程序设计》

  王涛  《你必须知道的.NET》

个人认为要把.NET垃圾回收讨论清楚,可以将“垃圾回收”分开来讨论-即“垃圾”与“回收”。所以可以从两方面来讨论:

1、关于垃圾:

 1.1、什么是垃圾?

1.2、 垃圾在哪儿?

     1.3、CLR如何寻找垃圾?

2、关于回收:

    2.1、为什么要回收垃圾?

2.2、什么时候回收?

2.3、如何回收?

2.4、回收多少?

我们将一一讨论上面的问题:

什么是垃圾?

垃圾?很简单,就是不在被需要的东西。在.NET下垃圾是指那些不再被任何对象引用的对象或资源。也就是不再被任何对象需要的东西就会被认为是垃圾。

垃圾在哪儿?

      如果我们要回收垃圾当然要知道垃圾在哪儿,才好对症下药。CLR分配内存时会将对象分配在以下几个地方:

  • 线程堆栈:值类型分配于线程堆栈上,值类型对象和引用对象是不同的,其并不受GC管理,它主要由我们的操作系统管理。当值类型对象所在的方法结束时,其占用的内存空间便自动释放。
  • GC堆(垃圾回收堆):用于分配大小小于85000字节的引用类型对象,其受GC管理,当第0代(generation代龄,GC使用代龄来提高回收效率)充满时会引发垃圾回收。
  • LOH(Large Object Heap大对象堆):用于分配不小于85000字节的对象,大对象堆不会被压缩,因为搬移大对象的代价很高会话很多CPU时间影响系统性能。还有就是大对象堆总是第2代。

      现在我们来讨论引用类型对象的内存分配,当我们新建引用对象时如:

Student  stu=new  Student();

这样的语句在程序中非常常见,C#允许我们在定义变量时就实例化该变量。其实以上语句可以分为两个步骤。

首先,Student   stu;该语句新建了一个Student类型的变量stu,它是一个指向Student类型的引用(也就是一个指针),但现在它还是空的(null)。

然后是new  Student()语句。new关键字将会触发CIL的newobj指令(String与数组对象除外)。newobj会完成 以下几个步骤:

  1. 计算实例化该类型所有字段所需的字节总数(包括其基类的所有字段一直计算到System.Object类)。
  2. 实例化对象的两个附加类型TypeHandle和SyncBlockIndex,(TypeHandle:指向加载堆(Loader  Heap)中该类型的方法表;SyncBlockIndex:用于管理线程同步的变量。)因此类型的字节总数还需要加上这两个附加类型的字节数,总共8个字节(32位CPU,64位为16字节)。
  3. CLR检查托管堆中是否有足够的空间来分配将要到来的对象,如果空间充足,则将其分配到NextObjPtr(下一个对象指针,总是指向下一个将要创建的对象的地址)指向的位置,然后调用类型构造器,同时NextObjPtr的值会传递给this参数,接着NextObjPtr重置,越过新创建的对象重新指向下一个将要创建的对象的地址,最后newobj指令返回所分配的内存地址;如果没有足够空间则会引发垃圾回收。

CLR如何寻找到垃圾?

其实呢我们每个应用程序都有一组根,根是一个存储位置,保存着一个指向引用类型的内存指针,该指针指向托管堆中的对象。如全局变量、静态变量都会被认为是一个应用程序根。实际上,GC就是通过应用程序的根来寻找到可回收的垃圾的。

当垃圾回收开始时,GC会遍历所有的应用程序的份。如果某个对象有根的引用,那么这个对就被认为是可达的(reachable),最终GC会将所有可达对象全部找到并形成一个可达对象图。可达对象图以外的对象会被认为是可回收的垃圾。例如下图:

如图:GC遍历所有的应用程序的根是将会发现A、C、E、G是有根 的,GC会认为这些对象是可达的,所以GC将会把A、C、E、G加入到可达对象图中,然而A又引用着B,所以B也是可达的,因而B也会被加入到可达对象图。

当GC检查完所有的根,它将得到完整的可达对象图,任何不在该图中的对象将会被认为是可回收的垃圾。接着GC会线性遍历包含着可回收对象的连续内存区块,如果GC遍历到较大的连续区块,GC将会压缩托管堆,把一些非垃圾对象搬移到这些连续的空间里,以避免出现内存碎片。搬移对象时GC会负责更新搬移对象的根,使其指向半以后的位置,同时也将更新NextObjPtr的位置,让其指向下最后一个对象的下一个内存地址。

为什么要回收垃圾?

垃圾回收的目的是合理的利用内存,提升系统效率,防止内存泄露。有时我们忘记释放内存,还可能造成数据丢失。例如我们使用FileStream类访问文件,以BinaryWriter写入数据时,如果忘记显示释放资源空间就很容易造成数据丢失。FileStream类使用内存缓冲来提升效率,只有当缓冲区充满时BinaryWriter对象才会向文件中写入数据。由于FileStream类实现了终结器所以如果FileStream对象先于BinaryWriter对象被终结的话,缓冲区数据就会丢失。(这在Jeffrey   Richter 《Microsoft.NET框架程序设计》一书的19.4.3节中有详细解释)

什么时候回收?

  1. 托管堆没有多余的空间来分配将要分配的对象(实际上就是第0代充满时)
  2. 使用GC.Collect()方法强制垃圾回收。
  3. Windows内存不足。
  4. CLR卸载AppDomain(应用程序域)时。
  5. 其他情况。

如何回收?

在.NET平台下,CLR会自动的为我们回收托管堆中的垃圾,不需要我们自己显示的去清楚对象占用的空间。当垃圾回收时,GC会启动垃圾回收线程,并挂起或劫持其他线程以回收对象。这些都是自动的,不需要程序员操心,但这是针对托管资源而言,很多时候我们会用到很多非托管资源,这些对象的资源很宝贵,不及时清除很容易拖累内存,甚至造成其它我们不期望的后果(比如内存泄露,数据丢失等)。所以我们需要在对象被清除以前将对象的资源清除掉。常见的非托管资源有套接字、数据库连接、网络连接、文件等等,对于非托管资源的清理,.NET提供了两种方式:终结模式和Dispose模式。这两种方式都是通过Win32的CloseHandle()方法实现的。

²  终结模式:

终结模式即是对System.Object.Finalize()方法的重写实现。Finalize()方法是Object类的虚方法,其本身没有提供任何实现,子类要实现终结模式就必须重写该方法。但奇怪的是微软并不提供Finalize()显式重写,而且在VS2010中智能感知系统根本就不提示Finalize方法。Finalize()方法被设计成了一个特殊方法,要重写该方法只能通过析构器语法去实现。

终止化有很多缺点:

  1. 我们无法控制终止化的时间和顺序。C#中的终结器由GC负责调用,GC只会在垃圾回收的前一步调用对象的终结器,这样几乎没办法及时的释放资源空间。而且对象的终结器执行顺序无法保证,很有可能出现一个对象访问一个已经被终止化的对象,这会引起异常。
  2. CLR为终止化提供一个特殊终止化线程,这必然影响系统性能。
  3. 终止化对象执行需要较长的时间,切需要额外内存空间。因为终止化对象的引用需要保存到终止化链表上,终止化执行时需要遍历该表,并形成一个终止化可达队列的内部数据结构。终止化执行时会将继承链上的对象全部终结,这也是执行时间长的原因。
  4. 终止化会托大对象的代龄。
  5. 终止化可能不会执行。

²  Dispose模式:

Dispose模式是一种显式释放对象资源的有效方法。实现Dispose模式需要实现IDisposable接口,其提供了Dispose方法。.NET中的有非托管资源的类型都实现了Dispose方法,其中部分类型同时实现了终结器和Dispose方法,所以在使用这些类型时一定不能忘记使用Dispose方法释放对象的资源,否则便会陷入终结器麻烦中。.NET中的部分类型实现了Dispose方法的同时还提供了Close方法,它们没有任何区别,仅仅是Close方法调用了Dispose而已,大概是微软工程师们认为Close比Dispose更亲切吧!

回收多少?

关于这个问题我们必须要了解代龄的概念。GC将垃圾回收堆分为3代,即第0代、第1代、第2代,如图:

 

每一代都有自己的容量的最大值叫做阙值容量,默认情况下,第一代的阙值容量为256K,第1代为2M,第2代为10M。具体情况可能与此不同,因为GC会自动调节代的阙值容量。例如,如果一次垃圾回收后,存活下来的对象很少,那么GC可能会将第0代的阙值容量由256K调整为128K。

    在一次垃圾回收中,在第0代中存活下来的对象会被移到第1代中,在第1代中存活下来的对象会被移到第2代中。GC总是回收第0代中的垃圾而忽略其他的代,只有第1 代充满时才会回收第1代,第1代被回收时第0代也会被回收,第2代也是如此。

posted on 2011-04-20 11:48  ly-newyear  阅读(296)  评论(0编辑  收藏  举报