这是我参与8月更文挑战的第28天,活动详情查看:8月更文挑战
清理垃圾算法又叫内存回收算法。
1 标记(Mark)
垃圾回收的第一步,就是找出活跃的对象。根据 GC Roots 遍历所有的可达对象,这个过程,就叫作标记。
如图所示,圆圈代表的是对象。绿色的代表 GC Roots,红色的代表可以追溯到的对象。可以看到标记之后,仍然有多个灰色的圆圈,它们都是被回收的对象。
2 清除(Sweep)
清除阶段就是把未被标记的对象回收掉。
但是这种简单的清除方式,有一个明显的弊端,那就是碎片问题。比如我申请了 1k、2k、3k、4k、5k 的内存。
由于某种原因 ,2k 和 4k 的内存,我不再使用,就需要交给垃圾回收器回收。
这个时候,我应该有足足 6k 的空闲空间。接下来,我打算申请另外一个 5k 的空间,结果系统告诉我内存不足了。系统运行时间越长,这种碎片就越多。在很久之前使用 Windows 系统时,有一个非常有用的功能,就是内存整理和磁盘整理,运行之后有可能会显著提高系统性能。这个出发点是一样的。
3 复制(Copying)
优点
- 因为是对整个半区进行内存回收,内存分配时不用考虑内存碎片等情况。实现简单,效率较高
不足之处
- 既然要复制,需要提前预留内存空间,有一定的浪费
- 在对象存活率较高时,需要复制的对象较多,效率将会变低
4 整理(Compact)
其实,不用分配一个对等的额外空间,也是可以完成内存的整理工作。可以把内存想象成一个非常大的数组,根据随机的 index 删除了一些数据。那么对整个数组的清理,其实是不需要另外一个数组来进行支持的,使用程序就可以实现。它的主要思路,就是移动所有存活的对象,且按照内存地址顺序依次排列,然后将末端内存地址以后的内存全部回收。
但是需要注意,这只是一个理想状态。对象的引用关系一般都是非常复杂的,我们这里不对具体的算法进行描述。你只需要了解,从效率上来说,一般整理算法是要低于复制算法的。
5 扩展回收算法
目前JVM的垃圾回收器都是对几种朴素算法的发扬光大(没有最优的算法,只有最合适的算法):
- 复制算法(Copying) :复制算法是所有算法里面效率最高的,缺点是会造成一定的空间浪费
- 标记-清除(Mark-Sweep) :效率一般,缺点是会造成内存碎片问题
- 标记-整理(Mark-Compact) :效率比前两者要差,但没有空间浪费,也消除了内存碎片问题
6 标记清除(Mark-Sweep)
首先从 GC Root 开始遍历对象图,并标记(Mark)所遇到的每个对象,标识出所有要回收的对象。然后回收器检查堆中每一个对象,并将所有未被标记的对象进行回收。
不足之处
- 标记、清除的效率都不高
- 清除后产生大量的内存碎片,空间碎片太多会导致在分配大对象时无法找到足够大的连续内存,从而不得不触发另一次垃圾回收动作
7 标记整理(Mark-Compact)
与标记清除算法类似,但不是在标记完成后对可回收对象进行清理,而是将所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。
优点
- 消除了标记清除导致的内存分散问题,也消除了复制算法中内存减半的高额代价
不足之处
- 效率低下,需要标记所有存活对象,还要标记所有存活对象的引用地址。效率上低于复制算法
8 分代收集(Generational Collection)
研究表明大部分对象可以分为两类:
- 大部分对象的生命周期都很短
- 其他对象则很可能会存活很长时间
根据对象存活周期的不同将内存划分为几块。对不同周期的对象采取不同的收集算法:
- 新生代:每次垃圾收集会有大批对象回收,所以采取复制算法
- 老年代:对象存活率高,采取标记清理或者标记整理算法
① 年轻代(Young Generation)
年轻代使用的垃圾回收算法是复制算法。因为年轻代发生 GC 后,只会有非常少的对象存活,复制这部分对象是非常高效的。但复制算法会造成一定的空间浪费,所以年轻代中间也会分很多区域。
如图所示,年轻代分为: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 的对象将会直接进入老年代。