清理垃圾算法|8月更文挑战

172 阅读6分钟

这是我参与8月更文挑战的第28天,活动详情查看:8月更文挑战

清理垃圾算法又叫内存回收算法。

1 标记(Mark)

垃圾回收的第一步,就是找出活跃的对象。根据 GC Roots 遍历所有的可达对象,这个过程,就叫作标记。

标记(Mark).png

如图所示,圆圈代表的是对象。绿色的代表 GC Roots,红色的代表可以追溯到的对象。可以看到标记之后,仍然有多个灰色的圆圈,它们都是被回收的对象。

2 清除(Sweep)

清除阶段就是把未被标记的对象回收掉。

清除(Sweep).png

但是这种简单的清除方式,有一个明显的弊端,那就是碎片问题。比如我申请了 1k、2k、3k、4k、5k 的内存。

清除(Sweep)-内存.jpg

由于某种原因 ,2k 和 4k 的内存,我不再使用,就需要交给垃圾回收器回收。

清除(Sweep)-回收.jpg

这个时候,我应该有足足 6k 的空闲空间。接下来,我打算申请另外一个 5k 的空间,结果系统告诉我内存不足了。系统运行时间越长,这种碎片就越多。在很久之前使用 Windows 系统时,有一个非常有用的功能,就是内存整理和磁盘整理,运行之后有可能会显著提高系统性能。这个出发点是一样的。

3 复制(Copying)

复制(Copying)算法.png

优点

  • 因为是对整个半区进行内存回收,内存分配时不用考虑内存碎片等情况。实现简单,效率较高

不足之处

  • 既然要复制,需要提前预留内存空间,有一定的浪费
  • 在对象存活率较高时,需要复制的对象较多,效率将会变低

4 整理(Compact)

其实,不用分配一个对等的额外空间,也是可以完成内存的整理工作。可以把内存想象成一个非常大的数组,根据随机的 index 删除了一些数据。那么对整个数组的清理,其实是不需要另外一个数组来进行支持的,使用程序就可以实现。它的主要思路,就是移动所有存活的对象,且按照内存地址顺序依次排列,然后将末端内存地址以后的内存全部回收。

整理(Compact).png

但是需要注意,这只是一个理想状态。对象的引用关系一般都是非常复杂的,我们这里不对具体的算法进行描述。你只需要了解,从效率上来说,一般整理算法是要低于复制算法的。

5 扩展回收算法

目前JVM的垃圾回收器都是对几种朴素算法的发扬光大(没有最优的算法,只有最合适的算法):

  • 复制算法(Copying) :复制算法是所有算法里面效率最高的,缺点是会造成一定的空间浪费
  • 标记-清除(Mark-Sweep) :效率一般,缺点是会造成内存碎片问题
  • 标记-整理(Mark-Compact) :效率比前两者要差,但没有空间浪费,也消除了内存碎片问题

收集算法.png

6 标记清除(Mark-Sweep)

标记清除(Mark-Sweep)算法.png

首先从 GC Root 开始遍历对象图,并标记(Mark)所遇到的每个对象,标识出所有要回收的对象。然后回收器检查堆中每一个对象,并将所有未被标记的对象进行回收。

不足之处

  • 标记、清除的效率都不高
  • 清除后产生大量的内存碎片,空间碎片太多会导致在分配大对象时无法找到足够大的连续内存,从而不得不触发另一次垃圾回收动作

7 标记整理(Mark-Compact)

标记整理(Mark-Compact)算法.png

与标记清除算法类似,但不是在标记完成后对可回收对象进行清理,而是将所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。

优点

  • 消除了标记清除导致的内存分散问题,也消除了复制算法中内存减半的高额代价

不足之处

  • 效率低下,需要标记所有存活对象,还要标记所有存活对象的引用地址。效率上低于复制算法

8 分代收集(Generational Collection)

研究表明大部分对象可以分为两类:

  • 大部分对象的生命周期都很短
  • 其他对象则很可能会存活很长时间

根据对象存活周期的不同将内存划分为几块。对不同周期的对象采取不同的收集算法:

  • 新生代:每次垃圾收集会有大批对象回收,所以采取复制算法
  • 老年代:对象存活率高,采取标记清理或者标记整理算法

① 年轻代(Young Generation)

年轻代使用的垃圾回收算法是复制算法。因为年轻代发生 GC 后,只会有非常少的对象存活,复制这部分对象是非常高效的。但复制算法会造成一定的空间浪费,所以年轻代中间也会分很多区域。

年轻代.jpg

如图所示,年轻代分为:1个伊甸园空间(Eden )2个幸存者空间(Survivor ) 。当年轻代中的 Eden 区分配满的时候,就会触发年轻代的 GC(Minor GC)。具体过程如下:

  • 在 Eden 区执行了第一次 GC 之后,存活的对象会被移动到其中一个 Survivor 分区(以下简称from)
  • Eden 区再次 GC,这时会采用复制算法,将 Eden 和 from 区一起清理。存活的对象会被复制到 to 区,然后只需要清空 from 区就可以了

在这个过程中,总会有1个 Survivor 分区是空置的。Eden、from、to 的默认比例是 8:1:1,所以只会造成 10% 的空间浪费。这个比例,是由参数 -XX:SurvivorRatio 进行配置的(默认为 8)。

② 老年代(Old/Tenured Generation)

老年代一般使用“标记-清除”、“标记-整理”算法,因为老年代的对象存活率一般是比较高的,空间又比较大,拷贝起来并不划算,还不如采取就地收集的方式。对象进入老年代的途径如下:

  • 提升(Promotion)

    如果对象够老,会通过“提升”进入老年代

  • 分配担保

    年轻代回收后存活的对象大于10%时,因Survivor空间不够存储,对象就会直接在老年代上分配

  • 大对象直接在老年代分配

    超出某个大小的对象将直接在老年代分配

  • 动态对象年龄判定

    有的垃圾回收算法,并不要求 age 必须达到 15 才能晋升到老年代,它会使用一些动态的计算方法。

    比如,如果幸存区中相同年龄对象大小的和,大于幸存区的一半,大于或等于 age 的对象将会直接进入老年代。