本文引自:http://www.cnblogs.com/wilber2013/p/4357910.html

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

创建对象

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

Student s1 = new Student();

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

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

new关键字

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

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类型,通过该类型的一些静态方法,可以通过编程的方式与垃圾回收器进行交互。

看一个简单的例子:

class Student
{
public int Id { get; set; }
public string Name { get; set; }
public int Age { get; set; }
public string Gender { get; set; }
} class Program
{
static void Main(string[] args)
{
Console.WriteLine("Estimated bytes on heap: {0}", GC.GetTotalMemory(false)); Console.WriteLine("This OS has {0} object generations", GC.MaxGeneration); Student s = new Student { Id = 1, Name = "Will", Age = 28, Gender = "Male"};
Console.WriteLine(s.ToString()); Console.WriteLine("Generation of s is: {0}", GC.GetGeneration(s)); GC.Collect();
Console.WriteLine("Generation of s is: {0}", GC.GetGeneration(s)); GC.Collect();
Console.WriteLine("Generation of s is: {0}", GC.GetGeneration(s)); Console.Read();
}
}

程序的输出为:

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

强制垃圾回收

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

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

  1. 应用程序将要进入一段代码,这段代码不希望被可能的垃圾回收中断

  2. 应用程序刚刚分配非常多的对象,程序想在使用完这些对象后尽快的回收内存空间

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

static void Main(string[] args)
{
……
GC.Collect();
GC.WaitForPendingFinalizers();
……
}

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

总结

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

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

  1. .NET垃圾回收 – 原理浅析

    在开发.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. WPF向系统发送消息 并传递结构体

    场景 :需要开发一个通讯组件 流程为:界面-开启接收服务-通过发送组件发送信息到 其他客户端和服务端 接受服务接收其他客户端发送的消息 需要传递给对应组件或者界面 因此会出现类库重复引用问题.因为采用 ...

  2. 入门 IT 行业,该具备哪些技能?

    对于刚开始进入IT的新人来说,“必备技能”往往意味着一个长长的.标有重要度的学习列表,但是过长的列表通常会导致新人不知如何开始学习,压力倍增.本文尝试列举出最重要的几个技能,也期望通过此列表能给新人一 ...

  3. 常用几个空格的 Unicode 码

    const SPACE_UNICODE = { 'ensp': '\u2002', 'emsp': '\u2003', 'nbsp': '\u00a0' }

  4. ZBrush为电影制作设计独特的生物概念

    任何一个从事3D行业的艺术家,在雕刻和画画方面,都要有牢固的基本技能,还要会使用一些软件.比如今天我们提到的这位概念设计师.插画师和艺术导演Ian Joyner,他在创作新角色之前,都会思考如何以及为 ...

  5. Spring、Spring MVC、MyBatis 整合文件配置详解

    使用SSM框架做了几个小项目了,感觉还不错是时候总结一下了.先总结一下SSM整合的文件配置.其实具体的用法最好还是看官方文档. Spring:http://spring.io/docs MyBatis ...

  6. 洛谷T44252 线索_分治线段树_思维题

    分治线段树,其实就是将标记永久化,到最后再统一下传所有标记. 至于先后顺序,可以给每个节点开一个时间戳. 一般地,分治线段树用于离线,只查询一次答案的题目. 本题中,标记要被下传 222 次. Cod ...

  7. 电子邮件的三个协议: SMTP、IMAP、POP3

    个人总结: 读完这篇文章需要10分钟 讲解了跟电子邮件有关的三个协议: SMTP(simple message transfer protocol 简单信息传输协议 IMAP (internet me ...

  8. HDU1061 - Rightmost Digit

    Given a positive integer N, you should output the most right digit of N^N. Input The input contains ...

  9. 模块打包机--webpack--基础使用

    什么是webpack? 作用有哪些?   WebPack可以看做是模块打包机:它做的事情是,分析你的项目结构,找到JavaScript模块以及其它的一些浏览器不能直接运行的拓展语言(Scss,Type ...

  10. 前端实现input[type='file']上传图片预览效果

    众所周知JavaScript在设计上处于安全角度考虑,是不允许读写本地文件的(原因请自行百度): 但是在实际项目应用中,经常会使用到上传图片,并且可以让用户直接预览图片.对于此种做法有两种方法可以实现 ...