zoukankan      html  css  js  c++  java
  • 垃圾回收算法总结

    分代回收理论

    大多数商业虚拟机的垃圾收集器都遵循“分代收集”的理论设计,它建立在两个分带假说至上:

    1. 弱分代假说:绝对大多数对象都是朝生夕灭的。

    2. 强分代假说:熬过越多次垃圾收集过程的 对象就越难以消亡。

      这两个分代假说表明了垃圾收集器一致设计的原则:应该将Java堆划分为多个不同的区域,然后根据对象的年龄(年龄指的是熬过垃圾收集的次数)分配到不同的区域,比如:如果一个区域中的对象绝大多数都是朝生夕灭,应该将这些对象集中放置在一块区域,每次垃圾收集时只需要关注如何去保存少量存活的对象,而不是去标记大量将要回收的对象,以较低的代价回收这块区域;如果 剩下的都是难以消亡的对象,便可以把这些对象集中放置另一块区域,在垃圾回收时只关注少量将要回收的对象。这就同时兼顾了内存的空间和时间的效率。

      把分代收集理论体现到Java虚拟机中,设计者一般至少会把Java堆划分为两个区域:新生代和老年代。顾明思议,新生代的对象在每次进行垃圾收集后都有大部分的对象死亡,而剩下来的对象会随着年龄增长逐步晋升到老年代中存放。

    3. 跨代引用假说:跨代引用相对于同代引用来说仅占极少数。

      即便对Java堆进行了区域划分,垃圾收集器想要进行垃圾收集也并非易事,它至少存在一个明显的困难:对象的引用不是孤立的,也会存在跨代引用。

      跨分代引用假说其实是依据前两条分代假说推理出来:存在相互引用关系的对象,应该是趋向同时生存或者同时消亡的。比如:如果一个新生代对象存在跨代引用,由于老年代难以消亡,最终会使新生代的被引用对象难以消亡,并逐步晋升到老年代。这时也就不存在跨代引用的说法了,根据这条假说,我们可以认为不在为了少量的跨代引用去扫描整个老年代,只需要在新生代上建立一个全局数据结构(“记忆集”),这个结构会把老年代划分多个区域,标记处哪一块区域存在跨代引用。当发生Minor GC时,将标记出来的区域中的对象加入到GC Roots集合中去进行可达性分析。这个方法虽然需要在对象改变引用关系(比如:自己或者某个属性赋值)时维护“记忆集”中数据的正确性,增加运行时开销,但是同比扫描整个老年代是十分跨算的。

    • 整堆收集(Full GC):收集整个Java堆和方法区的垃圾收集。

    • 部分收集(Partial GC):指目标不是完整收集整个Java堆中垃圾收集,其中分为以下几类:

      • 新生代收集(Minor GC/Young GC):指目标是新生代的垃圾收集。
      • 老年代收集(Major GC/Old GC):指目标是老年代垃圾收集。目前只有CMS收集器会存在单独收集老年代的行为。
      • 混合收集(Mixed GC):指目标是收集整个新生代和部分老年代的垃圾收集。目前只有GI收集器会有这种行为。

    标记-清除算法

    标记—清除算法是最早的也是最基础的算法。

    算法分为“标记”和“清除”两个阶段:首先标记出需要回收的对象,然后在标记完成后对所标记的对象进行垃圾回收。当然,也可以反过来,标记不需要回收的对象,然后收集未标记的对象。标记过程就是判定对象是否属于垃圾的过程。

    后续的大多数算法都是以标记—清除算法进行改进得到的。

    它主要有两个缺点:

    1. 效率执行不稳定,如果Java堆中存放的对象绝大多数都是要回收的,那么该算法需要进行大量的标记和清除动作,导致标记和清除两个过程随着对象数量的增长效率持续降低;

    2. 空间碎片化的问题,在进行标记—清除算法后,产生大量不连续的内存碎片,如果内存碎片过多,导致下一次为对象分配内存时找不到合适的内存大小存储对象,导致不得不提前触发一次垃圾收集动作。或者说对象分配内存时,找不到合适的对象造成大量内存浪费。标记—清除算法如图:

    image-20200827171348998

    标记-复制算法

    标记—复制算法,主要解决了标记—清除算法之后内存过于碎片化的问题,它是将可用的内存区域划分为大小相等的两块内存(当然也不一定非要相等),在进行垃圾收集时,现将存活的对象标记,然后将标记的存活对象复制到另一半空闲的内存区域,最后将之前的一半区域中所有对象完全清除用来存放之后新生的对象。这样就解决了使用标记—回收算法之后内存碎片化的问题,而且在对象内存分配时也极为简单,只需要移动堆顶指针,按顺序分配即可,简单高效。

    缺点也极为明显:

    1. 如果某块内存区域中在进行垃圾回收时,有大量的存活的对象,那么就会产生大量的对象复制的开销。

    2. 本来一块完整的可使用的内存区域被分为两份区域,一次只能使用其中一份区域,无疑极大的浪费了内存。

    image-20200827171430479

    现在的商业虚拟机都采用这种算法来回收新生代,在 IBM 的研究中新生代中的对象 98% 都是「朝生夕死」,所以并不需要按照 1:1 的比例来划分空间,而是将内存分为一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 和其中一块 Survivor。当回收时,将 Eden 和 Survivor 中还存活的对象一次性复制到另一块 Survivor 空间上,最后清理掉 Eden 和刚才用过的 Survivor 空间。

    HotSpot 默认 Eden 和 Survivor 的大小比例是 8:1,也就是每次新生代中可用的内存为整个新生代容量的 90%(80%+10%),只有 10% 会被浪费。当然,98% 的对象可回收只是一般场景下的数据,我们没办法保证每次回收后都只有不多于 10% 的对象存活,当 Survivor 空间不够用时,需要依赖其它内存(这里指老年代)进行分配担保。如果另外一块 Survivor 空间没有足够空间存放上一次新生代收集下来存活的对象时,这些对象将直接通过分配担保机制进入老年代。

    标记-整理算法

    通过前面对复制-收集算法的介绍我们知道,其对老年代这种对象存活时间长的内存区域就不适用了,主要原因是老年代的对象存活时间久且就数量多,如果进行频繁的复制移动,效率很低。而标记整理的算法就比较适用这一场景。

    标记-整理算法的标记过程与「标记-清除」算法一样,但是后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

    image-20200827171443242

    总结

    • 标记—清除算法标记—整理算法的本质差异在于前者是一种非移动式回收算法,后者是移动式的。是否移动存活对象是一种优缺点并存的决策,如果移动存活对象,尤其是在老年代这种每次都有大量的存活对象的区域来说,移动存活对象无疑是一种很大的负担,而且像这种移动对象操作必须暂停用户应用程序才能进行

    • 像标记—清除算法那样不考虑移动和整理存活对象又不可避免的造成空间碎片化,只能依赖更复杂的内存分配器和内存访问器来解决。比如:通过“分区空闲分配链表”来解决内存分配问题(计算机硬盘存储大文件就不会要求物理连续的磁盘空间,能够在碎片化的硬盘上存储和访问就是通过硬盘分区表实现)。内存的访问是用户程序最频繁的操作,甚至没有之一,如果在这个环节额外增加负担,势必会直接影响应用程序的吞吐量。

    基于以上两点,是否移动对象都存在弊端,移动则内存回收时更复杂,不移动则内存分配上和访问更为复杂。从垃圾收集的停顿时间来看,不移动对象停顿时间更短,甚至不需要停顿,但是从整个应用程序的吞吐量来看,移动对象更划算。

    另外还有一种“和稀泥式”方案可以不再内存分配和访问上增加太大额外负担同时有不会频繁移动对象造成额外消耗,做法就是虚拟机平时大多数时间采用标记—清除算法,暂时容忍内存碎片的存在,直到内存空间碎片化程度已经大到影响对象分配时,采用一次标记—整理算法,获得规整的内存空间。

  • 相关阅读:
    呀?这就是锁(二)?
    呀?这就是锁(一)?
    线程的建立
    Mybatis基础使用简介
    使用apache+tomcat+mod_jk.so实现集群
    HttpClient使用详解
    HttpClient基础用法
    Collection集合学习(二)———List接口与具体实现
    Docker学习总结(二)—— 镜像,容器
    Docker学习总结(一)—— namespace,cgroup机制
  • 原文地址:https://www.cnblogs.com/code-duck/p/13573579.html
Copyright © 2011-2022 走看看