zoukankan      html  css  js  c++  java
  • 基于代的垃圾回收机制--《CLR via C#》读书笔记

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

    一.程序局部性原理

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

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

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

    二.代

           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字节的即是大对象。针对大对象:

    1. 大对象不在小对象的地址空间分配,而在进程地址空间的其他地方分配
    2. 目前,GC不压缩,因为在内存中移动它代价太高,但这可能造成大对象之间造成碎片化
    3. 大对象总是G2,所以只能为需要长时间存活的资源创建大对象,分配短时间存活的大对象会导致G2被频繁地回收,会损害性能。

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

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

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

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

    参考

           《CLR via C#》(第4版)

  • 相关阅读:
    HTTP状态码
    CentOS 7 上安装vim(默认未安装)
    yum安装提示Another app is currently holding the yum lock; waiting for it to exit...
    CentOS 7 安装telnet服务
    shell编程
    shell基础
    ssh相关命令
    ssh无密码连接
    centos7小命令
    日志管理
  • 原文地址:https://www.cnblogs.com/gudi/p/6417722.html
Copyright © 2011-2022 走看看