C# 垃圾回收机制

一、GC机制基本原理


1、GC产生的背景

       每个程序都要使用这样或那样的资源,比如文件、内存缓冲区、屏幕空间、网络连接、数据库资源等。在面向对象的环境中,每个类型都代表可供程序使用的一种资源。要使用这些资源,必须为代表资源的类型分配内存

访问资源所需要的步骤有:

访问资源的步骤

以上步骤如果最后一步是由程序员负责,可能会产生一些无法预测的问题,有很多程序员忘记释放不再使用的内存,又有很多程序员试图使用已被释放的内存(即野指针),这种问题一般无法预测它们的后果或者出现问题时的时间,如果是其他bug,一旦发现程序行为异常,修正问题即可,但是这种bug会造成资源泄露(浪费内存)和对象损坏(影响稳定性),这就使得程序的表现变得不可预测。而正确的进行资源管理通常很难而且很枯燥,它会极大的分散程序员的注意力,使之无法专注于真正要解决的问题。为了不让程序员过多的关注资源管理问题,需要有那么一种机制能简化程序员的内存管理工作,这个机制就叫做垃圾回收机制(Garbage Collection)

2、C#资源类型介绍

托管资源和非托管资源

托管资源指的是.NET可以自动进行回收的资源,主要是指托管堆上分配的内存资源。托管资源的回收工作是不需要人工干预的,由.NET运行库在合适时机调用垃圾回收器进行回收。

非托管资源指的是.NET不知道如何回收的资源,最常见的一类非托管资源是包装操作系统资源的对象,例如:文件、窗口、网络连接、数据库连接、画刷、图标等。这类资源需要手动实现Finalize()或Dispose()方法释放资源。

非托管资源的处理不在本节内容中考虑。

3、托管堆资源的分配

CLR(Common Language Runtime 公共语言运行时,运行时有一些功能,如:内存管理、安全性、异常处理、线程,是由CLR完成的,CLR不是单独为某一种语言存在的,是面向CLR所有语言的存在,可理解为.Net的虚拟机)要求所有的资源都从托管堆分配。特殊的是我们永远不从托管堆中删除对象,因为应用程序不需要的对象会自动删除。

在进程初始化时,CLR要保留一块连续的地址空间,这个地址空间就是托管堆,托管堆还维护着一个指针NextObjPtr,它指向下一个对象在堆中的分配位置。

在IL指令创建对象时,经历了以下步骤:

1、计算类型(及其所有基类型)的字段需要的字节数;

2、对象开销所需字节数(两个开销字段:类型对象指针、同步块索引);

3、CLR检查保留区域是否能够提供分配对象所需的字节数,如果托管堆有足够的可用空间,对象会被放入,调用构造函数、返回对象的地址、NextObjPtr指针指向下一个地址;

创建了ABC三个对象  

应用程序调用new操作符创建对象时,可能没有足够的地址空间来分配该对象。托管堆将对象所需的字节数加到NextObjPtr指针中的地址上来检测这种情况,如果结果值超过了地址空间的末尾,表明托管堆已满,必须执行一次垃圾回收(事实上垃圾回收是在第0代满的时候发生的,代的概念后面会讲到)。

二、垃圾回收过程

根(Roots)

类中定义的任何的静态字段、方法的参数、局部变量(仅限引用类型变量)等都是根,根是CLR在堆之外可以找到的各种入口点。垃圾回收器会去检查所有的应用程序根,遍历每个根所引用到的对象,将其标记为活动的(live ),所有的根对象都检查完之后,有标记的对象就是可达对象,未标记的对象就是不可达对象,不可达对象就是回收的目标。

根的概念

进一步理解:对象之间通过引用(指针)连在一起,构成一个有向图,对象构成这个有向图的节点,而引用关系构成这个有向图的边。从根(root)出发,沿着有向边遍历对象,可达的(reachable)对象标记为活动对象,不可达(unreachable)的对象就是要清除的非活动对象。

对象的引用关系

在上图中,可以从程序变量(root)直接访问块1,并且可以间接访问块2和3。程序无法访问块4和5。第一步将标记块1,并记住块2和3以供稍后处理。第二步将标记块2,第三步将标记块3,但不标记块2,因为它已被标记。扫描阶段将忽略块1,2和3,因为它们已被标记,但会回收块4和5(4、5没有根引用)。

堆栈示例(7所指向的就是垃圾)

垃圾回收过程

CLR开始GC时,首先会暂停进程中所有的线程,这样可以防止线程在CLR检查期间访问对象并更改其状态。

标记-清除

(1) 标记

标记的过程,其实就是判断对象是否可达的过程,即将该对象的同步块索引中的位设置为1,一个对象被标记后,CLR会检查那个对象的根,标记它们引用的对象,如果发现对象已经被标记,就不重新检查对象的字段,避免因为循环引用而产生的死循环。当所有的根都检查完毕后,堆中将包含可达(已标记)与不可达(未标记)对象;

(2)清除

标记完成后将不可达对象清除。

(3)压缩

垃圾回收器线性遍历堆,寻找到不可达对象的连续内存块,把可达对象移动到这里以压缩堆。这个过程有点类似磁盘空间的碎片整理。

标记-清除-压缩

压缩之后,指向这些对象的指针都会失效,垃圾回收器必须重新访问所有根,并修改它们来指向对象的新内存位置。托管堆的NextObjPtr指针指向了最后一个幸存对象之后的位置,下一个分配对象将放在这个位置。该阶段完成后,CLR恢复应用程序的所有线程,这些线程会继续访问对象,就好像GC从未发生过一样。

三、垃圾回收算法-分代(Generation)算法

该算法对我们的代码做出了几点假设:

1、对象越新,生存期越短;(对象新老和何时创建有关)

2、对象越老,生存期越长;(对象新老和何时创建有关)

3、回收堆的一部分,速度快于回收整个堆。

托管堆在初始化时不包含任何对象,添加到堆的对象被称为第0代对象,即是那些第一次新构造的对象,垃圾回收器从未检查过它们(GC还没有发生)。

如下图,它分配了5个对象(从A到E),他们都是第0代,过了一会儿,C和E变得不可达(unreacheable,没有根引用它们)

GC尚未发生

CLR初始化时为第0代对象选择一个预算容量(以KB为单位),如果分配一个新的对象造成第0代超过预算,就必须启动一次垃圾回收

假设对象A到E刚好用完第0代的空间,那么分配新的对象F就必须启动垃圾回收,垃圾回收判断 C和E是垃圾(不可达),就会compact对象D,使之与对象B相邻(形成连续的地址空间),在这次垃圾回收中存活的对象A,B和D,现在成为了第1代对象,此时堆如图:

GC回收后

一次垃圾回收以后,第0代就不包含任何对象了(C和E被清理了,A,B,D变成了第1代对象),那么新对象的分配,就会在第0代中进行,这时候,新分配了对象F到K,这时候,随着应用程序的继续运行,对象B,H和J变得不可达,它们的内存将在某一时刻回收。

第1代有垃圾产生

现在,假定新分配的对象L会造成第0代超出预算,必须启动垃圾回收。开始垃圾回收时,垃圾回收必须检查哪些代?

前面说过,CLR初始化时,会为第0代对象选择预算。事实上,它还必须为第1代选择预算。

开始垃圾回收时,垃圾回收器还会检查第1代占用了多少内存。在本例中,由于第1代占用的内存远少于预算,所以垃圾回收器只检查第0代中的对象,回看之前的假设,越新的对象活得越短,因此,第0代中包含更多垃圾的可能性最大,能回收更多的内存

由于忽略了第1代中的对象,所以加快了垃圾回收速度,因为你不必遍历拖管堆中所有的对象。

基于代的垃圾回收器还假设,越老的对象活得越长,也就是说,第1代对象在应用程序中很有可能是继续可达的,如果垃圾回收器检查第1代中的对象,有可能找不到多少垃圾,结果是回收不了多少内存,因此,对第1代进行垃圾回收很可能是浪费时间,如果真的有垃圾在第1代中,它将留在那里,此时的堆如图:

0代提升到1代

所有幸存下来的对象,都成为了第1代的一部分,由于垃圾回收器没有检查第1代,所以对象B虽然已经不可达,但他并没有被回收,同样,在一次垃圾回收后,第0代不包含任何对象(之前分配的对象变成了第1代,不可达的对象被清理),这时候应用程序继续执行,并分配对象L到O,在运行过程中,对象G,L和M变得不可达,此时的托管堆如下:

第1代

假设分配对象P导致第0代超过预算,垃圾回收发生。由于第1代中所有对象占据的内存仍小于预算,所以垃圾回收器决定再次只回收第0代,忽略第1代中不可达的对象(对象B和G),回收后的情况如下:

从上图可以看到,第1代正在缓慢的增长,假如第1代的增长也超出了预算,这时候应用程序继续运行,并分配对象P到S,使第0代对象超出了预算,这时候堆如下图:

这时候,应用程序试图分配对象T时,由于第0代已满,所以必须进行垃圾回收,但这一次,垃圾回收器发现第1代占用了太多的内存,以至于用完了预算,所以这次垃圾回收器决定检查第1代和第0代中所有的对象,两代都被垃圾回收后,堆的情况如下:

第2代产生

和之前一样,垃圾回收后,第0代幸存者成为了第1代,第1代的幸存者成为了第2代,第0代再次空出来了,准备迎接新对象的到来,第2代中的对象经过2次或是更多次的检查,但只有在第1代超了预算才会检查第2代的对象,而在此之前,一般都已经对第0代进行了好几次的垃圾回收。

CLR初始化时,会为每一代选择预算。这个预算是使用类似于启发式算法自动进行调节的。

如果新创建的对象生存周期很短,第0代垃圾也会立刻被垃圾回收器回收(不用等空间分配满)。另外,如果回收了第0代,发现还有很多对象“可达”,并没有释放多少内存,就会增大第0代的预算至512KB,回收效果就会转变为:垃圾回收的次数将减少,但每次都会回收大量的内存。如果还没有释放多少内存,垃圾回收器将执行完全回收(3代),如果还是不够,则会抛出OutOfMemoryException异常。也就是说,垃圾回收器会根据回收内存的大小,动态的调整每一代的分配空间预算,达到自动优化!

四、总结

垃圾回收背后有这样一个基本的观念:编程语言(大多数的)似乎总能访问无限的内存。而开发者可以一直分配、分配再分配——像魔法一样,取之不尽用之不竭。

GC的基本工作原理是:通过最基本的标记清除原理,清除不可达对象;再像磁盘碎片整理一样压缩、整理可用内存;最后通过分代算法实现性能最优化。


2019.11.8 分享时问题记录:

1、第0代时幸存的对象变成第1代,那么有没有可能这个时候第0代幸存对象的空间算到第1代,导致第1代满了呢?

答:不会,因为第0代和第1代的预算容量相差悬殊,而且不是在第1代空间完全满的时候才清理内存的,而是差不多快满的时候就会清理内存,这个“快满”的空间是大于第0代的预算容量的;

各代预算容量

2、同步块索引,你刚刚说又可以锁住对象标记同步,又可以用来标记可达?

答:同步块索引的功能很多,即可以标记同步位,又可以标记可达,还可以存储哈希码;

https://blog.csdn.net/acmilanvanbasten/article/details/14521051  具体可以看看这篇文章,写的很详细。

3、不理解内存堆栈知识的可看这篇文章:

https://www.cnblogs.com/dotnet261010/p/9248555.html


参考文章:

https://www.jianshu.com/p/110c4363a1ff

http://www.west999.com/info/html/wangyezhizuo/css/20180623/4227150.html

https://zhuanlan.zhihu.com/p/68158037

https://yq.aliyun.com/articles/386565

https://my.oschina.net/BElement/blog/875626

https://www.cnblogs.com/nbclw/p/8458885.html

如有参考您的文章而未标注请与我联系...........

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,524评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,869评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,813评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,210评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,085评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,117评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,533评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,219评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,487评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,582评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,362评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,218评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,589评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,899评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,176评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,503评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,707评论 2 335

推荐阅读更多精彩内容