托管堆和垃圾回收

275 阅读6分钟

CLR via C# 托管堆和垃圾回收

CLR via C# (豆瓣) (douban.com)

21.1托管堆基础

每个程序都需要各种资源,包括文件、内存缓冲区、屏幕空间、网络连接、数据库资源。要使用这些资源,必须为代表资源的类型分配内存。访问一个资源所需的步骤。

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

特殊清理类型,有时需要尽快清理资源,而不是非要等到GC介入。可以在这些类中调用一个额外的方法(称为:Dispose)。一般包含你(文件、套接字和数据库连接)。

C#的new操作符导致CLR执行以下步骤。

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

image.png

如图:包含了三个对象A,B,C的一个托管堆。如果分配新的对象,他将放在NextObjPtr指针指向的位置。由于托管堆在内存中连续分配这些对象,所以会因为引用的“局部化”而获得性能上的提升。应用程序只需要使用很少的内存,从而提高了速度。代码使用对象可以全部驻留在CPU的缓存中。

垃圾回收算法

应用程序调用new 操作符创建对象时,可能没有足够的地址空间来分配该对象。发现空间不够,CLR就会执行垃圾回收。

对象生命周期的管理,Microsoft自己的组件对象模型用的就是引用计数。堆上的每个对象都维护着一个内存字段来统计程序中多少的“部分”正在使用对象。当计数字段变成0,对象就可以从内存中删除了。引用计数最大的问题是处理不好循环引用。

CLR改为使用一种引用跟踪算法。只关心引用类型的变量,我们将所有引用类型的变量称为

CLR开始GC时,首先暂定进程中的所有线程。这样可以防止线程在CLR检查期间访问对象并更改其状态。然后,CLR进入GC的标记阶段。CLR遍历堆中所有的对象,将同步块索引字段中的一位设为0。这表明所有对象都应删除。然后,CLR检查所有的活动根,查看他们引用了哪些对象。任何根引用了堆上的对象,CLR都会标记那个对象,也就是将该对象的同步块索引的位设置为1。一个对象标记后,CLR会检查那个对象中的根,标记他们引用的对象。如果发现对象已经标记,就不重新检查对象的字段。这就避免了因为循环引用而产生死循环。

如果CLR再一次GC之后回收不了内存,而且进程中没有空间来分配新的GC区域,就说明该进程的内存已耗尽。此时,试图分配更多的内存的new操作符会抛出OutOfMemoryException

静态字段引用的对象一直存在,知道用于加载类型的AppDomain卸载为止。内存泄漏的一个常见原因是让静态字段引用某个集合对象,然后不停的地向集合对象中添加数据。尽量避免使用静态字段。

  • 对象越新,生存周期越短
  • 对象越老,生存周期越长
  • 回收堆的一部分,速度快于回收整个堆

托管堆初始化时不包含对象。添加到对的对象称为第0代。如下图:A B C D E。过了一会C和E变得不可达。 image.png CLR初始化时为第0代超过预算,就必须启动一次垃圾回收。假设对象A到E刚好用完0代的空间,那么分配新的对象F就必须启动垃圾回收。垃圾回收判断对象C和E时垃圾,所以会压缩对象D,使之与对象B相邻。在垃圾回收中活着的对象A B D现在成为第一代。

image.png

一次垃圾回收后,第0代就不包含任何对象了。和前面一样,新的对象会分配到第0代。

image.png

开始垃圾回收时垃圾回收器必须决定检查哪些代。CLR初始化时会为第0代对象选择预算,他还必须为第1代选择预算。 本例中,由于第一代占用的内存远少于预算,所以垃圾回收只检查第0代中的对象。第0代包含更多的垃圾的可能性很大,能回收更多的内存。由于忽略了第1代中的对象,所以加快了垃圾回收的速度。

Microsoft的性能测试表明,对第0代执行一次垃圾回收,所花的时间不超过1毫秒。Mircrosoft的目标是使垃圾回收所发的时间不超过一次普通的内存页面错误的时间。

image.png 如图21-10,应用程序试图分配新的对象时,由于第0代已满,所以必须开始垃圾回收。但这一次垃圾回收器发现第一代占用了太多的内存,以至于用完了预算。所以这次决定检查第1代和第0代的所有对象。

image.png 垃圾回收后,第0代的幸存者被提升至第1代,第1代的幸存者被提升至第2代,第0代空了出来。

托管堆只支持3代:第0代、第1代、第2代。CLR初始化时会为每一代选择预算。然而,CLR的垃圾回收器是自调节的。