.net垃圾回收-原理浅析
本文引自: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寄存器
垃圾回收可以分为两个步骤:
- 标记对象
- 压缩托管堆
下面结合应用程序的根的概念,我们来看看垃圾回收这两个步骤。
标记对象
在垃圾回收的过程中,垃圾回收器会认为托管堆中的所有对象都是垃圾,然后垃圾回收器会检查所有的根。为此,CLR会建立一个对象图,代表托管堆上所有可达对象。
假设托管堆中有A-G七个对象,垃圾回收过程中垃圾回收器会检查所有的对象是否有活动根。这个例子的垃圾回收过程可以描述如下(灰色表示不可达对象):
- 当发现有根引用了托管堆中的对象A时,垃圾回收器会对此对象A进行标记
- 对一个根检测完毕后会接着检测下一个根,执行步骤一种同样的标记过程,标记对象B,在标记B时,检测到对象B内又引用了另一个对象E,则也对E进行标记;由于E引用了G,同样的方式G也会被标记
- 重复步骤二,检测Globales根,这次标记对象D
代码中很有可能多个对象中引用了同一个对象E,垃圾回收器只要检测到对象E已经被标记过,则不再对对象E内所引用的对象进行检测,这样做有两个目的:一是提高性能,二是避免无限循环。
所有的根对象都检查完之后,有标记的对象就是可达对象,未标记的对象就是不可达对象。
压缩托管堆
继续上面的例子,垃圾回收器将销毁所有未被标记的对象,释放这些垃圾对象所占的内存,再把可达对象移动到这里以压缩堆。
注意,在移动可达对象之后,所有引用这些对象的变量将无效,接着垃圾回收器要重新遍历应用程序的所有根来修改它们的引用。在这个过程中如果各个线程正在执行,很可能导致变量引用到无效的对象地址,所以整个进程的正在执行托管代码的线程是被挂起的。
经过了垃圾回收之后,所有的非垃圾对象被移动到一起,并且所有的非垃圾对象的指针也被修改成移动后的内存地址,NextObjPtr指向最后一个非垃圾对象的后面。
对象的代
当CLR试图寻找不可达对象的时候,它需要遍历托管堆上的对象。随着程序的持续运行,托管堆可能越来越大,如果要对整个托管堆进行垃圾回收,势必会严重影响性能。所以,为了优化这个过程,CLR中使用了"代"的概念,托管堆上的每一个对象都被指定属于某个"代"(generation)。
"代"这个概念的基本思想就是,一个对象在托管堆上存在的时间越长,那么它就更可能应该保留。托管堆中的对象可以被分为0、1、2三个代:
- 0代:从没有被标记为回收的新分配的对象
- 1代:在上一次垃圾回收中没有被回收的对象
- 2代:在一次以上的垃圾回收后仍然没有被回收的对象
下面还是通过一个例子看看代这个概念(灰色代表不可达对象):
- 在程序初始化时,托管堆上没有对象,这时候新添到托管堆上的对象是的代是0,这些对象从来没有经过垃圾回收器检查。假设现在托管堆上有A-G七个对象,托管堆空间将要耗尽。
- 如果现在需要更多的托管堆空间来存放新建的对象(H、I、J),CLR就会触发一次垃圾回收。垃圾回收器就会检查所有的0代对象,所有的不可达对象都会被清理,所有没有被回收掉的对象就成为了1代对象。
- 假设现在需要更多的托管堆空间来存放新建的对象(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()强制垃圾回收:
- 应用程序将要进入一段代码,这段代码不希望被可能的垃圾回收中断
- 应用程序刚刚分配非常多的对象,程序想在使用完这些对象后尽快的回收内存空间
在使用强制垃圾回收时,建议同时调用"GC.WaitForPendingFinalizers();",这样可以确定在程序继续执行之前,所有的可终结对象都必须执行必要的清除工作。但是要注意,GC.WaitForPendingFinalizers()会在回收过程中挂起调用的线程。
static void Main(string[] args)
{
……
GC.Collect();
GC.WaitForPendingFinalizers();
……
}
每一次垃圾回收过程都会损耗性能,所以要尽量避免通过GC.Collect()进行强制垃圾回收,除非遇到了真的需要强制垃圾回收的情况。
总结
本文介绍了.NET垃圾回收机制的基本工作过程,垃圾回收器通过遍历托管堆上的对象进行标记,然后清除所有的不可达对象;在托管堆上的对象都被设置了一个代,通过了代这个概念,垃圾回收的性能得到了优化。
.net垃圾回收-原理浅析的更多相关文章
- .NET垃圾回收 – 原理浅析
在开发.NET程序过程中,由于CLR中的垃圾回收(garbage collection)机制会管理已分配的对象,所以程序员就可以不用关注对象什么时候释放内存空间了.但是,了解垃圾回收机制还是很有必要的 ...
- jvm垃圾回收原理(转)
原文链接:jvm垃圾回收原理 在jvm中堆空间划分为三个代:年轻代(Young Generation).年老代(Old Generation)和永久代(Permanent Generation).年轻 ...
- go GC垃圾回收原理
目录 1.前言 2. 垃圾回收算法 3. Golang垃圾回收 3.1 垃圾回收原理 3.2 内存标记(Mark) 3.3 三色标记 3.4 Stop The World 4. 垃圾回收优化 4.1 ...
- jvm的垃圾回收原理
什么是垃圾回收? 垃圾回收是Java中自动内存管理的另一种叫法.垃圾回收的目的是为程序保持尽可能多的可用堆(heap). JVM会删除堆上不再需要从堆引用的对象. 用一个例子解释垃圾回收? 比方说,下 ...
- JVM垃圾回收原理
原文地址:http://chenchendefeng.iteye.com/blog/455883 一.相关概念 基本回收算法 1. 引用计数(Reference Counting) 比较古老的回收算法 ...
- Java垃圾回收原理
无意中在网络上找到了这篇介绍垃圾回收机制的文章,好文!转一下: 垃圾回收器是如何工作的?我现在就简单的介绍一下 首先要明确几点: Java是在堆上为对象分配空间的 垃圾回收器只跟内存有关,什么IO啊, ...
- KingbaseESV8R6 垃圾回收原理以及如何预防膨胀
背景 KingbaseESV8R6支持snapshot too old 那么实际工作中,经常看到表又膨胀了,那么我们讨论一下导致对象膨胀的常见原因有哪些呢? 未开启autovacuum 对于未开启au ...
- Javascript 垃圾回收机制
转载于https://www.cnblogs.com/zhwl/p/4664604.html 一.垃圾回收的必要性 由于字符串.对象和数组没有固定大小,所有当他们的大小已知时,才能对他们进行动态的存储 ...
- 浅析JVM内存区域及垃圾回收
一.JVM简介 JVM,全称Java Virtual Machine,即Java虚拟机.以Java作为编程语言所编写的应用程序都是运行在JVM上的.JVM是一种用于计算设备的规范,它是一个虚构出来的计 ...
随机推荐
- 11.MATLAB基本编程
概述: 1 脚本M文件 clear all; %设置精度 format long; %定义变量 n= s= %循环 :n s=s+/^i; end s format short; 2 函数M文件 fu ...
- caffe下python环境的编译
安装python所需的依赖包 (1)sudo apt-get install python-numpy python-scipy python-matplotlib ipython ipython-n ...
- pyCrypto python 3.5--转
原文地址:https://gxnotes.com/article/198426.html 问题描述 我发现一些PyCrypto安装程序为Python 3.3和3.4,但没有任何Python 3.5. ...
- 欢迎来到Flask的世界
不多说,直接上文档链接:Flask的文档 教程 API 快速上手
- C#、SQL中的事务
c#方法一: TransactionOptions transactionOption = new TransactionOptions(); //设置事务隔离级别 transactionOption ...
- Android studio关于点击事件后的页面跳转,选择完成后返回(onActivityResult)
我这个人喜欢直接上代码,在代码中说明更方便,更直接. 首先在.xml中设置一个button按钮,和一个EditText框,并分别做好id号. 这里我以籍贯测试对象. <LinearLayout ...
- elasticsearch集群添加节点
最简配置文件: cluster.name: your_cluster_name node.name: your_ip network.host: 0.0.0.0 http.port: your_p ...
- 最近邻插值法&线性插值&双线性插值&三线性插值
最近邻插值法nearest_neighbor是最简单的灰度值插值.也称作零阶插值,就是令变换后像素的灰度值等于距它最近的输入像素的灰度值. 造成的空间偏移误差为像素单位,计算简单,但不够精确.但当图像 ...
- selenim
一.安装selenium Pip install selenium==2.53.1 (稳定版) 下载火狐浏览器35.0.1 http://dl.pconline.com.cn/download ...
- node——简单的服务器启动+乱码问题解决,响应报文头
这个是一个比较简单的代码 // 1.加载hrrp模块 var http=require('http'); // 2.创建一个http服务对象 var server=http.createServer( ...