在开发.NET程序过程中,由于CLR中的垃圾回收(garbage collection)机制会管理已分配的对象,所以程序员就可以不用关注对象什么时候释放内存空间了。但是,了解垃圾回收机制还是很有必要的,下面我们就看看.NET垃圾回收机制的相关内容。

创建对象

在C#中,我们可以通过new关键字创建一个引用类型的对象,比如下面一条语句。New关键字创建了一个Student类型的对象,这个新建的对象会被存放在托管堆中,而这个对象的引用会存放在调用栈中。(对于引用类型可以查看,C#中值类型和引用类型)

  1. Student s1 = new Student();

在C#中,当上面的Student对象被创建后,程序员就可以不用关心这个对象什么时候被销毁了,垃圾回收器将会在该对象不再需要时将其销毁。

当一个进程初始化后,CLR就保留一块连续的内存空间,这段连续的内存空间就是我们说的托管堆。.NET垃圾回收器会管理并清理托管堆,它会在必要的时候压缩空的内存块来实现优化,为了辅助垃圾回收器的这一行为,托管堆保存着一个指针,这个指针准确地只是下一个对象将被分配的位置,被称为下一个对象的指针(NextObjPtr)。为了下面介绍垃圾回收机制,我们先详细看看new关键字都做了什么。

new关键字

当C#编译器遇到new关键字时,它会在方法的实现中加入一条CIL newobj命令,下面是通过ILSpy看到的IL代码。

  1. IL_0001: newobj instance void GCTest.Student::.ctor()

其实,newobj指令就是告诉CLR去执行下列操作:

  • 计算新建对象所需要的内存总数
  • 检查托管堆,确保有足够的空间来存放新建的对象
    • 如果空间足够,调用类型的构造函数,将对象存放在NextObjPtr指向的内存地址
    • 如果空间不够,就会执行一次垃圾回收来清理托管堆(如果空间依然不够,就会报出OutofMemoryException)
  • 最后,移动NextObjPtr指向托管堆下一个可用地址,然后将对象引用返回给调用者

按照上面的分析,当我们创建两个Student对象的时候,托管堆就应该跟下图一致,NextObjPtr指向托管堆新的可用地址。

托管堆的大小不是无限制的,如果我们一直使用new关键字来创建新的对象,托管堆就可能被耗尽,这时托管堆可以检测到NextObjPtr指向的空间超过了托管堆的地址空间,就需要做一次垃圾回收了,垃圾回收器会从托管堆中删除不可访问的对象

应用程序的根

垃圾回收器是如何确定一个对象不再需要,可以被安全的销毁?

这里就要看一个应用程序根(application root)的概念。根(root)就是一个存储位置其中保存着对托管堆上一个对象的引用,根可以属性下面任何一个类别:

  • 全局对象和静态对象的引用
  • 应用程序代码库中局部对象的引用
  • 传递进一个方法的对象参数的引用
  • 等待被终结(finalize,后面介绍)对象的引用
  • 任何引用对象的CPU寄存器

垃圾回收可以分为两个步骤:

  1. 标记对象
  2. 压缩托管堆

下面结合应用程序的根的概念,我们来看看垃圾回收这两个步骤。

标记对象

在垃圾回收的过程中,垃圾回收器会认为托管堆中的所有对象都是垃圾,然后垃圾回收器会检查所有的根。为此,CLR会建立一个对象图,代表托管堆上所有可达对象。

假设托管堆中有A-G七个对象,垃圾回收过程中垃圾回收器会检查所有的对象是否有活动根。这个例子的垃圾回收过程可以描述如下(灰色表示不可达对象):

  1. 当发现有根引用了托管堆中的对象A时,垃圾回收器会对此对象A进行标记
  2. 对一个根检测完毕后会接着检测下一个根,执行步骤一种同样的标记过程,标记对象B,在标记B时,检测到对象B内又引用了另一个对象E,则也对E进行标记;由于E引用了G,同样的方式G也会被标记
  3. 重复步骤二,检测Globales根,这次标记对象D

代码中很有可能多个对象中引用了同一个对象E,垃圾回收器只要检测到对象E已经被标记过,则不再对对象E内所引用的对象进行检测,这样做有两个目的:一是提高性能,二是避免无限循环

所有的根对象都检查完之后,有标记的对象就是可达对象,未标记的对象就是不可达对象。

压缩托管堆

继续上面的例子,垃圾回收器将销毁所有未被标记的对象,释放这些垃圾对象所占的内存,再把可达对象移动到这里以压缩堆。

注意,在移动可达对象之后,所有引用这些对象的变量将无效,接着垃圾回收器要重新遍历应用程序的所有根来修改它们的引用。在这个过程中如果各个线程正在执行,很可能导致变量引用到无效的对象地址,所以整个进程的正在执行托管代码的线程是被挂起的。

经过了垃圾回收之后,所有的非垃圾对象被移动到一起,并且所有的非垃圾对象的指针也被修改成移动后的内存地址,NextObjPtr指向最后一个非垃圾对象的后面。

对象的代

当CLR试图寻找不可达对象的时候,它需要遍历托管堆上的对象。随着程序的持续运行,托管堆可能越来越大,如果要对整个托管堆进行垃圾回收,势必会严重影响性能。所以,为了优化这个过程,CLR中使用了"代"的概念,托管堆上的每一个对象都被指定属于某个"代"(generation)。

"代"这个概念的基本思想就是,一个对象在托管堆上存在的时间越长,那么它就更可能应该保留。托管堆中的对象可以被分为0、1、2三个代:

  • 0代:从没有被标记为回收的新分配的对象
  • 1代:在上一次垃圾回收中没有被回收的对象
  • 2代:在一次以上的垃圾回收后仍然没有被回收的对象

下面还是通过一个例子看看代这个概念(灰色代表不可达对象):

  1. 在程序初始化时,托管堆上没有对象,这时候新添到托管堆上的对象是的代是0,这些对象从来没有经过垃圾回收器检查。假设现在托管堆上有A-G七个对象,托管堆空间将要耗尽。

  2. 如果现在需要更多的托管堆空间来存放新建的对象(H、I、J),CLR就会触发一次垃圾回收。垃圾回收器就会检查所有的0代对象,所有的不可达对象都会被清理,所有没有被回收掉的对象就成为了1代对象。

  3. 假设现在需要更多的托管堆空间来存放新建的对象(K、L、M),CLR会再触发一次垃圾回收。垃圾回收器会先检查所有的0代对象,但是仍需要更多的空间,那么垃圾回收器会继续检查所有 的1代对象,整理出足够的空间。这时,没有被回收的1代对象将成为2代对象。2代对象是目前垃圾回收器的最高代,当再次垃圾回收时,没有回收的对象的代数依然保持2。

通过前面的描述可以看到,分代可以避免每次垃圾回收都遍历整个托管堆,这样可以提高垃圾回收的性能。

System.GC

.NET类库中提供了System.GC类型,通过该类型的一些静态方法,可以通过编程的方式与垃圾回收器进行交互。

看一个简单的例子:

  1. class Student
  2. {
  3. public int Id { get; set; }
  4. public string Name { get; set; }
  5. public int Age { get; set; }
  6. public string Gender { get; set; }
  7. }
  8.  
  9. class Program
  10. {
  11. static void Main(string[] args)
  12. {
  13. Console.WriteLine("Estimated bytes on heap: {0}", GC.GetTotalMemory(false));
  14.  
  15. Console.WriteLine("This OS has {0} object generations", GC.MaxGeneration);
  16.  
  17. Student s = new Student { Id = , Name = "Will", Age = , Gender = "Male"};
  18. Console.WriteLine(s.ToString());
  19.  
  20. Console.WriteLine("Generation of s is: {0}", GC.GetGeneration(s));
  21.  
  22. GC.Collect();
  23. Console.WriteLine("Generation of s is: {0}", GC.GetGeneration(s));
  24.  
  25. GC.Collect();
  26. Console.WriteLine("Generation of s is: {0}", GC.GetGeneration(s));
  27.  
  28. Console.Read();
  29. }
  30. }

程序的输出为:

从这个输出,我们也可以验证代的概念,每次垃圾清理后,如果一个对象没有被清理,那么它的代就会提高。

强制垃圾回收

由于托管堆上的对象由垃圾管理器帮我们管理,所有我们不需要关心托管堆上对象的销毁以及内存空间的回收。

但是,有些特殊的情况下,我们可能需要通过GC.Collect()强制垃圾回收:

  1. 应用程序将要进入一段代码,这段代码不希望被可能的垃圾回收中断
  2. 应用程序刚刚分配非常多的对象,程序想在使用完这些对象后尽快的回收内存空间

在使用强制垃圾回收时,建议同时调用"GC.WaitForPendingFinalizers();",这样可以确定在程序继续执行之前,所有的可终结对象都必须执行必要的清除工作。但是要注意,GC.WaitForPendingFinalizers()会在回收过程中挂起调用的线程。

  1. static void Main(string[] args)
  2. {
  3. ……
  4. GC.Collect();
  5. GC.WaitForPendingFinalizers();
  6. ……
  7. }

每一次垃圾回收过程都会损耗性能,所以要尽量避免通过GC.Collect()进行强制垃圾回收,除非遇到了真的需要强制垃圾回收的情况。

总结

本文介绍了.NET垃圾回收机制的基本工作过程,垃圾回收器通过遍历托管堆上的对象进行标记,然后清除所有的不可达对象;在托管堆上的对象都被设置了一个代,通过了代这个概念,垃圾回收的性能得到了优化。

下一篇我们看看可终结对象(Finalize)和可处置对象(IDisposable)。

.NET垃圾回收 – 原理浅析的更多相关文章

  1. .net垃圾回收-原理浅析

    本文引自:http://www.cnblogs.com/wilber2013/p/4357910.html 在开发.NET程序过程中,由于CLR中的垃圾回收(garbage collection)机制 ...

  2. jvm垃圾回收原理(转)

    原文链接:jvm垃圾回收原理 在jvm中堆空间划分为三个代:年轻代(Young Generation).年老代(Old Generation)和永久代(Permanent Generation).年轻 ...

  3. go GC垃圾回收原理

    目录 1.前言 2. 垃圾回收算法 3. Golang垃圾回收 3.1 垃圾回收原理 3.2 内存标记(Mark) 3.3 三色标记 3.4 Stop The World 4. 垃圾回收优化 4.1 ...

  4. jvm的垃圾回收原理

    什么是垃圾回收? 垃圾回收是Java中自动内存管理的另一种叫法.垃圾回收的目的是为程序保持尽可能多的可用堆(heap). JVM会删除堆上不再需要从堆引用的对象. 用一个例子解释垃圾回收? 比方说,下 ...

  5. JVM垃圾回收原理

    原文地址:http://chenchendefeng.iteye.com/blog/455883 一.相关概念 基本回收算法 1. 引用计数(Reference Counting) 比较古老的回收算法 ...

  6. Java垃圾回收原理

    无意中在网络上找到了这篇介绍垃圾回收机制的文章,好文!转一下: 垃圾回收器是如何工作的?我现在就简单的介绍一下 首先要明确几点: Java是在堆上为对象分配空间的 垃圾回收器只跟内存有关,什么IO啊, ...

  7. KingbaseESV8R6 垃圾回收原理以及如何预防膨胀

    背景 KingbaseESV8R6支持snapshot too old 那么实际工作中,经常看到表又膨胀了,那么我们讨论一下导致对象膨胀的常见原因有哪些呢? 未开启autovacuum 对于未开启au ...

  8. Javascript 垃圾回收机制

    转载于https://www.cnblogs.com/zhwl/p/4664604.html 一.垃圾回收的必要性 由于字符串.对象和数组没有固定大小,所有当他们的大小已知时,才能对他们进行动态的存储 ...

  9. 浅析JVM内存区域及垃圾回收

    一.JVM简介 JVM,全称Java Virtual Machine,即Java虚拟机.以Java作为编程语言所编写的应用程序都是运行在JVM上的.JVM是一种用于计算设备的规范,它是一个虚构出来的计 ...

随机推荐

  1. python 面向对象初级篇

    Python 面向对象(初级篇) 概述 面向过程:根据业务逻辑从上到下写垒代码 函数式:将某功能代码封装到函数中,日后便无需重复编写,仅调用函数即可 面向对象:对函数进行分类和封装,让开发" ...

  2. supervisor-1:基础篇

    别人博客转载,做个记录 原文链接:http://lixcto.blog.51cto.com/4834175/1539136 有阵子没写博客了,这段时间一直在研究python django框架和前端相关 ...

  3. 我的Python学习之路 Python的初识与准备工作

    注:文笔不好,不喜勿喷,当个段子看看就好 一.初识Python 第一次听到Python是在2016年大概暑假 时候(即将大三),因为对黑客技术的蜜汁热爱(虽然自己并不会),在玄魂大大的公众微信号中看到 ...

  4. RTX二次开发集成

    1,rtx服务器端有很多端口,二次发的程序与这些打开的端口交互.打开端口的方法在rtx服务管理器中,默认http服务未启用.需要手动启用http端口如下: 如果打开rtx服务器没有启用http的801 ...

  5. 调用0A中断输入字符串数据段的DUP定义

    ;这是自动生成的代码模板 STACKS SEGMENT STACK ;堆栈段 DW DUP(?) ;注意这里只有128个字节 STACKS ENDS DATAS SEGMENT ;数据段 STRING ...

  6. Vue - 实例

    1.实例属性介绍如下图所示: 具体介绍如下: A.$parent:访问当前组件的父实例 B.$root:访问当前组件的根实例,要是没有的话,则是自己本身 C.$children:访问当前组件实例的直接 ...

  7. wpf 触发器理解

    (1)属性触发器:其对应的类是Trigger.它在特定关联属性发生变化时被触发.一个属性的更改会在另一个属性中触发即时或动态更改. (2)数据触发器:其对应的类是DataTrigger.它在特定的CL ...

  8. css3实现圆形逐渐减少动画

    写这个动画刚开始完全没有思路,后来参考网上的资料发现可以用半圆实现,具体原理如下: 1.,一个div作为背景,三个div做出三个半圆出来,一个用于旋转,一个靠左(用于与背景吻合),一个靠右(用于与背景 ...

  9. 图的基本遍历算法的实现(BFS & DFS)复习

    #include <stdio.h> #define INF 32767 typedef struct MGraph{ ]; ][]; int ver_num, edge_num; }MG ...

  10. iOS XMPP 通信协议实现 图形化直观感受

    第一次随笔,实在不知写点什么有用的东西,那就分享一下本人最近的研究所得吧! 是关于iOS-XMPP-通信协议的实现,具体代码比较复杂,三言两句也实在难表达清楚,网上已有很多关于iOS XMPP协议的讲 ...