JVM 常见垃圾回收算法简介

·  阅读 139

本文已参与掘金创作者训练营第三期「话题写作」赛道,详情查看:掘力计划|创作者训练营第三期正在进行,「写」出个人影响力

详细介绍了Java的JVM中常见三种垃圾收集算法:标记清除算法、复制算法、标记整理算法、分代分区收集算法。

Java的垃圾收集算法没有采用引用计数法来确定垃圾,而是基于可达性垃圾分析算法,由此产生了几种常见的垃圾收集算法。基本主要有标记-清除算法、复制算法、标记-整理算法等,另外还有分代、分区等综合多种算法的算法。关于垃圾收集器,可以看这篇文章:深入理解Java中的7种JVM垃圾收集器

1 基本算法

1.1 标记-清除算法(Mark-Sweep)

1.1.1 原理

标记-清除算法是最基础的收集算法,是现代垃圾回收算法的思想基础。它分为两个阶段:标记阶段和清除阶段。在标记阶段,首先通过根节点,标记所有从根节点开始的可达对象。因此,未被标记的对象就是未被引用的垃圾对象。然后,在清除阶段,清除所有未被标记的对象。

在这里插入图片描述

1.1.2 优缺点

优点:

  1. 标记清除算法解决了引用计数算法中的循环引用的问题(该算法基于可达性分析算法),没有从root节点引用的对象都会被回收。
  2. 该算法比较简单,后续的很多算法都是基于此算法改进的。

缺点:

  1. 效率较低,标记和清除两个动作都需要遍历所有的对象,并且在GC时,需要停止应用程序(STW),对于交互性要求比较高的应用而言这个体验是非常差的。
  2. 通过标记清除算法清理出来的内存,碎片化较为严重,因为被回收的对象可能存在于内存的各个角落,所以清理出来的内存是不连贯的。可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发GC动作。

1.2 复制算法(Copying)

1.2.1 原理

复制算法的核心就是,将原有的内存空间一分为二,每次只用其中的一块,在垃圾回收时,将正在使用的对象复制到另一个内存空间中,然后将该内存空间清空,交换两个内存的角色,完成垃圾的回收。

如果内存中的垃圾对象较多,需要复制的对象就较少,这种情况下适合使用该方式并且效率比较高,反之,则不适合。

在这里插入图片描述

1.2.2 优缺点

优点:

  1. 在垃圾对象较多的情况下,需要复制的对象就较少,这种情况下适合使用该方式并且效率比较高。
  2. 清理后,内存无碎片。

缺点:

  1. 在垃圾对象少的情况下,需要复制的对象就较多,不适用。
  2. 分配的2块内存空间,在同一个时刻,只能使用一半,内存使用率较低。

1.2.3 JVM改进的实现

现在的商业虚拟机都采用这种收集算法来回收新生代,IBM公司的专门研究表明,新生代中的对象98%是“朝生夕死”的,所以并不需要按照1:1的比例来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。

当回收时,将Eden和Survivor中还存活着的对象一次性地复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。HotSpot虚拟机默认Eden和Survivor的大小比例是8:1:1,也就是每次新生代中可用内存空间为整个新生代容量的90%(80%+10%),只有10%的内存会被“浪费”。当然,98%的对象可回收只是一般场景下的数据,我们没有办法保证每次回收都只有不多于10%的对象存活,当Survivor空间不够用时,需要依赖其他内存(这里指老年代)进行分配担保(Handle Promotion)。

内存的分配担保就好比我们去银行借款,如果我们信誉很好,在98%的情况下都能按时偿还,于是银行可能会默认我们下一次也能按时按量地偿还贷款,只需要有一个担保人能保证如果我不能还款时,可以从他的账户扣钱,那银行就认为没有风险了。内存的分配担保也一样,如果另外一块Survivor空间没有足够空间存放上一次新生代收集下来的存活对象时,这些对象将直接通过分配担保机制进入老年代

分配担保机制一般取之前每一次回收晋升到老年代对象容量的平均值作为经验值,与老年代的剩余空间进行比较,如果剩余空间足够则尝试只进行Minor GC,否则进行Full GC;如果担保失败,那么在Minor GC之后还要进行Full GC。更多具体的策略,在内存分配和回收策略部分有讲解。

1.3 标记-整理算法(Mark-Compact)

1.3.1 原理

又称为标记-压缩算法。标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存,“标记-整理”算法的示意图如下图。

在这里插入图片描述

1.3.2 优缺点

优缺点同标记清除算法,解决了标记清除算法的碎片化的问题,同时,标记压缩算法多了一步,对象移动内存位置的步骤,其效率也有有一定的影响。

2 综合算法

2.1 分代收集算法(Generational Collecting)

前面介绍了多种回收算法,每一种算法都有自己的优点也有缺点,谁都不能替代谁,所以根据垃圾回收对象的特点进行选择,才是明智的选择。

当前主流JVM垃圾收集都采用”分代收集算法”的综合算法,这种算法会根据对象存活周期的不同将内存划分为几块,如JVM中的 年轻代、老年代、永久代,然后根据各代特点分别采用最适当的GC算法在JVM中,年轻代适合使用复制算法,老年代适合使用标记清除或标记整理算法。堆内存GC模型以及详细的分代规则详见这篇文章:Java堆内存的GC模型概述

2.2 分区收集算法(Regin)

JDK1.7新增的G1垃圾收集器中,采用了最新的“分区收集算法”的复合算法,取消了传统的分代机制,则将整个堆空间有几片大的代空间划分为多个连续的不同小区间(Regin),每个小区间独立使用(可作为eden、survivor、old、humongous等区间),独立回收(使用的基础算法还是复制算法,并且具有天然的局部压缩性)。

虽然GC时也需要STW ,但是采用小分区的方法可以控制一次回收多少个小区间,根据目标停顿时间,每次合理地回收若干个小区间(而不是整个堆),从而减少一次GC所产生的停顿,获取最小的时间延迟。在JDK11中加入的ZGC垃圾收集器,也是采用分区(Regin)的思想。具体的算法和G1实现在后面的垃圾收集器部分讲解:深入理解Java中的7种JVM垃圾收集器

参考资料:

  1. 《深入理解Java虚拟机》

如有需要交流,或者文章有误,请直接留言。另外希望点赞、收藏、关注,我将不间断更新各种Java学习博客!

分类:
后端
标签: