博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
CLR内存管理之GC的工作原理
阅读量:4339 次
发布时间:2019-06-07

本文共 4275 字,大约阅读时间需要 14 分钟。

前两篇文章中,讲CLR的内存管理,都提到了垃圾回收器(GC)的作用。那么GC具体是怎么在虚拟内存中进行存储空间的监控和释放的呢?下面就介绍一下这个过程:

什么是GC?

正如其名,Garbage Collector,垃圾收集器,也就是说,GC的主要任务就是垃圾的清理。以应用程序的root为基础,遍历应用程序在Heap上动态分配的所有对象,通过识别它们是否被引用来确定哪些对象是已经死亡的哪些仍需要被使用。已经不再被应用程序的root或者别的对象所引用的对象就是已经死亡的对象,即所谓的垃圾,需要被回收。这就是GC工作的原理。为了实现这个原理,GC有多种算法。比较常见的算法有Reference CountingMark SweepCopy Collection等等。目前主流的虚拟系统.net CLRJava VMRotor都是采用的Mark Sweep算法。本文以.net为基础,这里只对Mark Sweep算法进行讲述。

Mark Sweep算法:

在程序运行的过程中,不断的把Heap的分配空间给对象,当Heap的空间被占用到不足 以为下一个对象分配的时候Mark Sweep算法被激活,将垃圾内存进行回收并将其返回到free list中。Mark Sweep就像它的名字一样在运行的过程中分为两个阶段,Mark阶段和Sweep阶段。Mark阶段的任务是从root出发,利用相互的引用关系遍历整个Heap,将被root和其它对象所引用的对象标记起来。没有被标记的对象就是垃圾。之后是Sweep阶段,这个阶段的任务就是回收所有的垃圾。

Mark Sweep算法虽然速度比Reference Counting要快,并且可以避免循环引用造成的内存泄漏。但是也有不少缺点,它需要遍历Heap中所有的对象(存活的对象在Mark阶段遍历,死亡的对象在Sweep阶段遍历)所以速度也不是十分理想。而且对垃圾进行回收以后会造成大量的内存碎片。为了解决这两个问题,Mark Sweep算法得到了改进。首先是在算法中加入了Compact阶段,即先标记存活的对象,再移动这些对象使之在内存中连续,最后更新和对象相关的地址和free list。这就是Mark Compact算法,它解决了内存碎片的问题。而为了提高速度,Generation(代)的概念被引入了。

Generation(代):

Generation的设计是基于以下几个原则:

1,对象越年轻则它的生命周期越短

2,对象越老则它的生命周期越长

3,年轻的对象和其它对象的关系比较强,被访问的频率也比较高

4,对Heap(托管堆)一部分的回收压缩比对整个Heap的回收压缩要快

Generation的概念就是对Heap中的对象进行分代(分成几块,每一块中的对象生存期不同)管理。当对象刚被分配时位于Generation 0中,当Generation 0的空间将被耗尽时,Mark Compact算法被启动。经过几次GC后如果这个对象仍然存活则会将其移动到Generation 1中。同理,如果经过几次GC后这对象还是存活的,则会被移动到Generation 2中,直到被移动到最高级中最后被回收或者是同程序一同死亡。采用Generation的最大好处就在于每次GC不用对整个Heap都进行处理,而是每次处理一小块。对于Generation 0中的对象,因为它们死亡的可能性最大,所以对它们GC的次数可以安排多一些,而其它相对死亡的可能性小一些的对象所在的Generation可以少安排几次GC。这样做就使得GC的速度得到了一定程度的提高。这样就产生了几个有待讨论的问题,首先是应该设置几个Generation,每个Generation应该设置成多大,然后是对每个对象升级时它应该是已被GC了多少次而仍然存活。关于.net CLR对这个问题的处理,在本文的最后将给出一个例子对其进行测试。

相关的数据结构:

.net GC相关的数据结构有三个Managed HeapFinalization QueueFreachable Queue

Managed Heap(托管堆)

Managed Heap是一个设计简单而优化的堆,它与传统的C-runtime的堆不太一样。它的简单管理方法是为了提高对堆的管理速度,同时也是基于一个简单的(也是不可能的)假设。对Managed Heap的管理假设内存是无穷无尽的。在Managed Heap上有一个称为NextObjPtr的指针,这个指针用于指示堆上最后一个对象的地址。当有一个新的对象要分配到这个堆上时,所要做的仅仅是将NextObjPtr的值加上新对象的大小形成新的NextObjPtr。这只是一个简单的相加,当NextObjPtr的值超出了Managed Heap边界的时候说明堆已经满了,GC将被启动。

Finalization QueueFreachable Queue

这两个队列和.net对象所提供的Finalize方法有关。这两个队列并不用于存储真正的对象,而是存储一组指向对象的指针。当程序中使用了new操作符在Managed Heap上分配空间时,GC会对其进行分析,如果该对象含有Finalize方法则在Finalization Queue中添加一个指向该对象的指针。在GC被启动以后,经过Mark阶段分辨出哪些是垃圾。再在垃圾中搜索,如果发现垃圾中有被Finalization Queue中的指针所指向的对象,则将这个对象从垃圾中分离出来,并将指向它的指针移动到Freachable Queue中。这个过程被称为是对象的复生(Resurrection),本来死去的对象就这样被救活了。为什么要救活它呢?因为这个对象的Finalize方法还没有被执行,所以不能让它死去。Freachable Queue平时不做什么事,但是一旦里面被添加了指针之后,它就会去触发所指对象的Finalize方法执行,之后将这个指针从队列中剔除,这是对象就可以安静的死去了。.net frameworkSystem.GC类提供了控制Finalize的两个方法,ReRegisterForFinalizeSuppressFinalize。前者是请求系统完成对象的Finalize方法,后者是请求系统不要完成对象的Finalize方法。ReRegisterForFinalize方法其实就是将指向对象的指针重新添加到Finalization Queue中。这就出现了一个很有趣的现象,因为在Finalization Queue中的对象可以复生,如果在对象的Finalize方法中调用ReRegisterForFinalize方法,这样就形成了一个在堆上永远不会死去的对象,像凤凰涅槃一样每次死的时候都可以复生(GC直接控制方法中有一个KeepAlive()方法,估计就是运用的这个机制保证对象的长久驻留,有兴趣的朋友可以验证一下~~)。

虽然.net提倡让GC自动去管理内存的清除工作,但是还是提供了一些直接控制GC的方法,请看下面的示例:

using System;using System.Collections.Generic;using System.Linq;using System.Text;using System.Threading.Tasks;namespace Demo{    class GCTest    {        static void Main()        {            Console.WriteLine(string.Format("GC MaxGengerations: {0}", GC.MaxGeneration));            Generation gen = new Generation();            gen.ConsoleGeneration();            for (int i = 0; i <= GC.MaxGeneration; i++)            {                GC.Collect();                gen.ConsoleGeneration();            }            gen = null;            for (int i = 0; i <= GC.MaxGeneration; i++)            {                GC.Collect(i);                GC.WaitForPendingFinalizers();            }                Console.ReadLine();        }    }    public class Generation    {        public void ConsoleGeneration()        {            Console.WriteLine(GC.GetGeneration(this));        }    }}

首先我们声明了一个类Generation用于验证GC的按Generation(代)处理的机制。通过GC.GetGeneration(this)我们可以得到当前实例的Generation值。通过GC.MaxGeneration我们可以得到GC中Generation的最大值。接下来的循环中,由于我们没有取消对实例的引用,所以每次的Collect()操作都会把gen的Generation上移一次。然后,我们取消对gen对实例的引用,再次循环进行Collect()操作,注意我们这里使用了GC.WaitForPendingFinalizers()方法,MSDN对其的解释是:“挂起当前线程,直到处理终结器队列的线程清空该队列为止”。也就是为了防止上面提到的复生(Resurrection)过程造成对象没有被删除。

上面示例的运行结果如下:

当然,有关GC直接控制的方法绝不止于这些,详细的介绍,请参考:

转载于:https://www.cnblogs.com/yonghuisoft/archive/2013/01/20/2868786.html

你可能感兴趣的文章
【NOIP 模拟赛】Evensgn 剪树枝 树形dp
查看>>
java学习笔记④MySql数据库--01/02 database table 数据的增删改
查看>>
两台电脑如何实现共享文件
查看>>
组合模式Composite
查看>>
程序员最想得到的十大证件,你最想得到哪个?
查看>>
我的第一篇CBBLOGS博客
查看>>
【MyBean调试笔记】接口的使用和清理
查看>>
07 js自定义函数
查看>>
jQueru中数据交换格式XML和JSON对比
查看>>
form表单序列化后的数据转json对象
查看>>
[PYTHON]一个简单的单元測试框架
查看>>
iOS开发网络篇—XML数据的解析
查看>>
[BZOJ4303]数列
查看>>
一般处理程序在VS2012中打开问题
查看>>
C语言中的++和--
查看>>
thinkphp3.2.3入口文件详解
查看>>
POJ 1141 Brackets Sequence
查看>>
Ubuntu 18.04 root 使用ssh密钥远程登陆
查看>>
Servlet和JSP的异同。
查看>>
虚拟机centOs Linux与Windows之间的文件传输
查看>>