前言:

组成.Net平台一个很重要的部分----垃圾收集器(Garbage Collection),今天我们就来讲讲它。想想看没有GC,.Net还能称之为一个平台吗?各种语言虽然都被编译成MSIL,但是运行时的资源回收工作却“各自为战”,这样不但增加了编程难度,也会使内存管理工作变得复杂无比(不同语言处理内存的微小差异,将在回收资源时被放大),更也不利于平台移植。

这篇文章将全面的为大家介绍.Net 垃圾收集的运行方式、算法,以及与垃圾收集相关的关键方法。

说到垃圾收集机制,很少有人知道,垃圾收集并不是伴随Java出现的,早在1958年,图林奖得主John发明的Lisp语言就已经提供了GC的功能,这是GC的第一次出现,是思想的一次闪光!而后,1984年Dave Ungar发明的Small talk语言第一次正式采用了GC机制。

.Net的垃圾回收机制是个很大的话题,如果你没接触过类似C++那样的语言,就很难理解GC是一个多么重要、令人兴奋的东西:

1.提高软件系统的内聚。

2.降低编程复杂度,使程序员不必分散精力去处理析构。

3.不妨碍设计师进行系统抽象。

4.减少由于内存运用不当产生的Bug。

5.成功的将内存管理工作从程序的编写时,脱离至运行时,使不可预估的管理漏洞变为可预估的。

正文:

本文将通过“GC的算法与工作方式”、“GC关键方法解析”两节来讲述.Net垃圾收集机制。

第一节. GC的算法与工作方式

1.算法

垃圾收集器的本质,就是跟踪所有被引用到的对象,整理对象不再被引用的对象,回收相应的内存。

这听起来类似于一种叫做“引用计数(Reference Counting)”的算法,然而这种算法需要遍历所有对象,并维护它们的引用情况,所以效率较低些,并且在出现“环引用”时很容易造成内存泄露。所以.Net中采用了一种叫做“标记与清除(Mark Sweep)”算法来完成上述任务。

“标记与清除”算法,顾名思义,这种算法有两个本领:

“标记”本领——垃圾的识别:从应用程序的root出发,利用相互引用关系,遍历其在Heap上动态分配的所有对象,没有被引用的对象不被标记,即成为垃圾;存活的对象被标记,即维护成了一张“根-对象可达图”。

其实,CLR会把对象关系看做“树图”,无疑,了解数据结构的同学都知道,有了“树图”的概念,会加快遍历对象的速度。

检测、标记对象引用,是一件很有意思的事情,有很多方法可以做到,但是只有一种是效率最优的,.Net中是利用栈来完成的,在不断的入栈与出栈中完成检测:先在树图中选择一个需要检测的对象,将该对象的所有引用压栈,如此反复直到栈变空为止。栈变空意味着已经遍历了这个局部根(或者说是树图中的节点)能够到达的所有对象。树图节点范围包括局部变量(实际上局部变量会很快被回收,因为它的作用域很明显、很好控制)、寄存器、静态变量,这些元素都要重复这个操作。一旦完成,便逐个对象地检查内存,没有标记的对象变成了垃圾。

“清除”本领——回收内存:启用Compact算法,对内存中存活的对象进行移动,修改它们的指针,使之在内存中连续,这样空闲的内存也就连续了,这就解决了内存碎片问题,当再次为新对象分配内存时,CLR不必在充满碎片的内存中寻找适合新对象的内存空间,所以分配速度会大大提高。但是大对象(large object heap)除外,GC不会移动一个内存中巨无霸,因为它知道现在的CPU不便宜。通常,大对象具有很长的生存期,当一个大对象在.NET托管堆中产生时,它被分配在堆的一个特殊部分中,移动大对象所带来的开销超过了整理这部分堆所能提高的性能。

Compact算法除了会提高再次分配内存的速度,如果新分配的对象在堆中位置很紧凑的话,高速缓存的性能将会得到提高,因为一起分配的对象经常被一起使用(程序的局部性原理),所以为程序提供一段连续空白的内存空间是很重要的。

2.代龄(Generation)

代龄就是对Heap中的对象按照存在时间长短进行分代,最短的分在第0代,最长的分在第2代,第2代中的对象往往是比较大的。Generation的层级与FrameWork版本有关,可以通过调用GC.MaxGeneration得知。

通常,GC会优先收集那些最近分配的对象(第0代),这与操作系统经典内存换页算法“最近最少使用”算法如出一辙。但是,这并不代表GC只收集最近分配的对象,通常,.Net GC将堆空间按对象的生存期长短分成3代:新分配的对象在第0代(0代空间最大长度通常为256K),按地址顺序分配,它们通常是一些局部变量;第1代(1代空间最大长度通常为2 MB)是经过0代垃圾收集后仍然驻留在内存中的对象,它们通常是一些如表单,按钮等对象;第2代是经历过几次垃圾收集后仍然驻留在内存中的对象,它们通常是一些应用程序对象。

当内存吃紧时(例如0代对象充满),GC便被调入执行引擎——也就是CLR——开始对第0代的空间进行标记与压缩工作、回收工作,这通常小于1毫秒。如果回收后内存依然吃紧,那么GC会继续回收第1代(回收操作通常小于10毫秒)、第2代,当然GC有时并不是按照第0、1、2代的顺序收集垃圾的,这取决于运行时的情况,或是手动调用GC.Collect(i)指定回收的代。当对第2代回收后任然无法获得足够的内存,那么系统就会抛出OutOfMemoryException异常

当经过几次GC过后,0代中的某个对象仍然存在,那么它将被移动到第1代。同理,第1、2代也按同样的逻辑运行。

这里还要说的是,GC Heap中代的数量与容量,都是可变的(这由一个“策略引擎”控制,在第二节中,会介绍到“策略引擎”), 以下代码结合Windbg可以说明这个问题,以下代码中,可以通过单击按钮“button1”,不断的分配内存,而后获得对象“a”的代龄情况,并且在Form加载时也会获得“a”的代龄。

public partial class Form1 : Form

    {

        private string a = new string('a',1);

        public Form1()

        {

            InitializeComponent();

        }

        private void button1_Click(object sender, EventArgs e)

        {

            a = new string('a', 900000);

            label1.Text = GC.GetGeneration(a).ToString();

        }

        private void Form1_Load(object sender, EventArgs e)

        {

            label1.Text = GC.GetGeneration(a).ToString();

        }

    }

程序刚加载时,“a”的代龄为第0代,通过windbg我们还获得了以下信息:

可以看出,GC堆被分成了两个段,三代,每代起始地址十进制差值为12。

点击数次“button1”按钮后,“a”的代龄升为第2代,通过windbg我们又获得了以下信息:

这里要注意一个很关键的地方,就是各代的起始(generation x starts at)十进制地址差值不再是12,0代与1代差为98904,1代与2代差为107908,这说明代的大小随程序运行在改变,并且GC heap的大小也有变化。

转自:http://www.cnblogs.com/isline/archive/2009/03/03/1402350.html

.Net Discovery系列之三 深入理解.Net垃圾收集机制(上)的更多相关文章

  1. .Net Discovery 系列之七--深入理解.Net垃圾收集机制(拾贝篇)

    关于.Net垃圾收集器(Garbage Collection),Aicken已经在“.Net Discovery 系列”文章中有2篇的涉及,这一篇文章是对上2篇文章的补充,关于“.Net Discov ...

  2. .Net Discovery系列之四 深入理解.Net垃圾收集机制(下)

    上一节给大家介绍了 .Net GC的运行机制,下面来讲下与GC相关的重要方法. 第二节.GC关键方法解析 1.Dispose()方法 Dispose可用于释放所有资源,包括托管的和非托管的,需要自己实 ...

  3. .Net Discovery 系列之五--深入浅出.Net实时编译机制(上)

    欢迎阅读“.Net Discovery 系列”文章,本文将分上.下两部分为大家讲解.Net JIT方面的知识,敬请雅正. JIT(Just In Time简称JIT)是.Net边运行边编译的一种机制, ...

  4. 深入理解JVM垃圾收集机制,下次面试你准备好了吗

    程序计数器.虚拟机栈和本地方法栈这三个区域属于线程私有的,只存在于线程的生命周期内,线程结束之后也会消失,因此不需要对这三个区域进行垃圾回收.垃圾回收主要是针对 Java 堆和方法区进行. 判断一个对 ...

  5. ActiveMQ系列之三:理解和掌握JMS

    JMS是什么 JMS Java Message Service,Java消息服务,是Java EE中的一个技术. JMS规范 JMS定义了Java 中访问消息中间件的接口,并没有给予实现,实现JMS  ...

  6. 深入理解JVM垃圾收集机制(JDK1.8)

    垃圾收集算法 标记-清除算法 最基础的收集算法是"标记-清除"(Mark-Sweep)算法,分两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象. 不足: ...

  7. 深入理解JVM 垃圾收集器(上)

    HotSpot虚拟机中的垃圾收集器 GC评价标准 GC调优 响应时间 吞吐量 1.新生代收集器 Serial收集器 ParNew收集器 Parallel Scavenge收集器 2.老年代收集器 Se ...

  8. .Net Discovery 系列之一--string从入门到精通(上)

    string是一种很特殊的数据类型,它既是基元类型又是引用类型,在编译以及运行时,.Net都对它做了一些优化工作,正式这些优化工作有时会迷惑编程人员,使string看起来难以琢磨,这篇文章分上下两章, ...

  9. .Net Discovery 系列之六--深入浅出.Net实时编译机制(下)

    接上文 在初始化时,HashTable中各个方法指向的并不是对应的内存入口地址,而是一个JIT预编译代理,这个函数负责将方法编译为本地代码.注意,这里JIT还没有进行编译,只是建立了方法表! 下表(表 ...

随机推荐

  1. Linux下JDK到底应该安装在哪儿?

    1 Linux 目录结构 即使这是个菜鸟级的问题,也经常难住老鸟.我就见过很资深的程序员把JDK不合适地安装到/home目录下.虽然不一定有最正确的安装位置,但一定有不适当的安装位置.为了确定我们到底 ...

  2. linux修改文件打开最大数(ulimit命令)

    解除 Linux 系统的最大进程数和最大文件打开数限制:vi /etc/security/limits.conf# 添加如下的行* soft noproc 65536 * hard noproc 65 ...

  3. 部分Web服务器信息对比

    本文参考wikipedia的Web服务器比较页面的数据,选取了其中自己感兴趣的Web服务器的信息进行了对比,包括,Apache HTTP Server.Apache Tomcat.Nginx.Catt ...

  4. git命令之git stash 暂存临时代码

    git stash — 暂存临时代码   stash命令可以很好的解决这样的问题.当你不想提交当前完成了一半的代码,但是却不得不修改一个紧急Bug,那么使用’Git stash’就可以将你当前未提交到 ...

  5. hdu 5038 (2014北京网络赛G 排序水题)

    题意:有n个数字,带入10000 - (100 - ai) ^ 2公式得到n个数,输出n个数中频率最大的数,如果有并列就按值从小到大都输出输出,如果频率相同的数字是全部的n个数,就输出Bad....题 ...

  6. Angular快速学习笔记(4) -- Observable与RxJS

    介绍RxJS前,先介绍Observable 可观察对象(Observable) 可观察对象支持在应用中的发布者和订阅者之间传递消息. 可观察对象可以发送多个任意类型的值 -- 字面量.消息.事件. 基 ...

  7. Vue.js项目集成ElementUI

    Vuejs实例-02Vue.js项目集成ElementUI   Vuejs实例-02Vue.js项目集成ElementUI 0:前言 vue.js的UI组件库,在git上有多个项目,我见的使用者比较多 ...

  8. PHP 文件路径获取文件名

    物理截取 $file = '/www/htdocs/inc/lib.inc.php'; $filename = basename($file); echo $filename, '<br/> ...

  9. 【LeetCode】142. Linked List Cycle II

    Difficulty:medium  More:[目录]LeetCode Java实现 Description Given a linked list, return the node where t ...

  10. 最受欢迎编程语言又是谁?C语言居首,大数据赢了

    C语言占据榜首,但大数据类是最大赢家. IEEE Spectrum的第三次“最受欢迎编程语言”交互式排行榜新鲜出炉.因为不可能顾及到每一个程序员的想法,Spectrum使用多样化.可交互的的指标权重来 ...