您的位置:首页 > 编程语言 > C#

基于代的垃圾回收机制--《CLR via C#》读书笔记

2017-02-19 23:00 176 查看
我们知道,垃圾回收在内存无限大的理想情况下是不需要的,正是因为内存存在的瓶颈,我们才需要垃圾回收。在《垃圾回收算法之引用计数算法》《垃圾回收算法之引用跟踪算法》两篇文章中,我们了解了垃圾回收算法的基本原理,并介绍了两种垃圾回收算法。本篇是在垃圾回收的前提下,通过代的机制更进一步地提升程序的性能。

一.程序局部性原理

程序局部性原理是一种总结性的原理,它是指在程序执行时呈现局部性规律,即在一段时间内,整个程序的执行仅限于程序中的某一部分。相应地,执行所访问的存储空间也局限于某个内存区域。

这个局部性原理有两层含义:一层是时间局部性,如果一个变量正在被访问,那么它近期很有可能被再次访问;一层是空间局部性,马上要使用的信息与正在使用的信息在空间地址上是临近的。

通俗地讲:对象越新,生存期越短,对象越老,生存期越长。

二.代

C#的GC把托管堆分为3代,依次为G0、G1、G2,G0总是用来分配新对象,垃圾回收中G0幸存的对象放到对象生存期更长的G1中,在下一次的回收G1中垃圾后,G1中垃圾的对象放到对象生存期更长的G2中;CLR初始化时会为G0、G1、G2分配空间预算,每一代的垃圾回收只有在本代的空间预算用完后才会启动垃圾回收。

下面,我们通过《CLR via C#》中的例子来理解代的运行机制:

在托管堆初始化时不包含对象,添加到堆中的对象称为0代对象。



在step1中,G0中新进了3个对象,过了一会了,对象c变得不可达了,现在我要分配一个新的对象d、e、f,这时G0超过了预算了。

前面我们说过,G0总是用来分配新对象,因此虽然这时G1和G2空间是足够的,但也不能用来分配给新对象;另外,前面我们也说过,每一代的垃圾回收只有在本代的 空间预算用完后才会启动垃圾回收。

因此,我们需要启动G0的垃圾回收,将c回收掉,将a、b幸存,并升级到G1中,经过第1次的垃圾回收后如下:



回收后的G0是空的,正好可以用来分配新进的d、e、f,如下所示:



过了一段时间后,f变成可达了,现在我们要分配新的对象g、h、i,发现G0被占满了,于是回收G0(注意这时不回收G1,因为G1的空间预算还没有用完),并将d和e加入G1,经过这次的回收后如下图所示:



回收后的G0又变空了,可以容纳新的对象,如下所示:



现在,我们要分配新的对象,j、k、l,这时发现G0和G1的空间预算都用完了,于是回收G0和G1,并将G1中幸存的对象升至G2,G0中幸存的对象升至G1,经过这次的回收后如下图所示:



回收过后的G0又变空了,可以容纳新的对象j、k、l。依此往复。如果没有回收到足够多的内存时,垃圾回收器就会执行一次完整的垃圾回收,如果还是不够,就抛出OutOfMemoryException异常。

三.程序局部性原理在代机制中的应用

前面,我们谈到了程序局部性原理,其中说对象越新生存期越短,具体到代机制中,G0总是存着最新的对象,因此G0中包含垃圾的可能性也就更大,能回收更多的内存,对于G0的回收也更频繁。

同理,对于G1的回收就没有那么频繁了,因为根据程序局部性原理,G1中的对象活得更长,在回收G0且G1的内存没有超出预算时,如果也去回收G1中的垃圾,有可能找不出多少垃圾,因此对G1的垃圾回收有可能是浪费时间。

也就是说G0回收时,如果G1没有超出预算,是不会回收G1的,如果G1有垃圾,它将留在G1当中,,这样就加快了回收的速度。

四.另一个提升性能的重要因素:不必遍历所有根

另一个提升性能的重要因素,在于我们不必遍历所有的根,如果根或对象引用了老一代的对象,垃圾回收器就可以忽略老对象内部的所有引用,能更快地构建对象可达图。

但问题来了,如果老对象在原来的程序中是不可达的,在G0引入新对象后,又引用了新对象呢?那如何更新老对象的引用呢?

垃圾回收器利用了JIT编译器内部的一个机制,在对象的引用字段发生变化时,会设置一个相应的位标志,这样垃圾回收器就知道自上一次垃圾回收以来,哪些老对象已被写入,只有字段发生变化的老对象才需要是否引用了G0中的任何新对象。

五.大对象

到目前为止,前面我们说的都是小对象,CLR认为超过85000字节的即是大对象。针对大对象:

大对象不在小对象的地址空间分配,而在进程地址空间的其他地方分配

目前,GC不压缩,因为在内存中移动它代价太高,但这可能造成大对象之间造成碎片化

大对象总是G2,所以只能为需要长时间存活的资源创建大对象,分配短时间存活的大对象会导致G2被频繁地回收,会损害性能。

六.内存预算的调节—自调节

垃圾回收器会在执行垃圾回收的过程中了解应用程序的行为,同时根据行为来动态调整G0、G1、G2的预算。

假如应用程序构建了相当多的短时间使用的对象,造成G0的垃圾回收会回收大量内存,就可能减少G0的预算,这样G0的回收将更加频繁,但垃圾回收器每次做的事情也就更少了,这减少了进程的工作集。如果G0中全是垃圾,则不需要经过压缩内存,直接让NextObjPrj指针指回G0的开始处即可。这样回收起来更快!

假如垃圾回收器在回收G0时发现还有很多对象存活,就会增大G0的预算,垃圾的回收次数将减少,但每次垃圾回收时,回收的内存就会多得多。

参考

《CLR via C#》(第4版)
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: