0x00 前言
本文我们将介绍几种常见的垃圾回收算法,了解它们的优缺点,并尝试在这些算法上进行改进得到更好的GC算法。
0x01 GC算法的评价指标
- 最大暂停时间。在相同的总暂停时间下,最大暂停时间越大,则用户程序中断的时间越久,而对于用户而言,他们的耐心显然是有一个阈值,如果最大暂停时间超过了这个阈值,那么就会带来极差的用户体验。因此一个好的GC算法应该尽可能减少每次STW(stop the world)时间。
- 吞吐量。即GC时间占用程序总运行时间的比值,显然我们希望GC时间占用的越少越好,尽可能多地运行用户代码。
- 堆使用效率。标记-清除算法不需要使用额外的堆空间,因此留给用户程序的内存更多,而复制算法则需要使用一半的堆空间。因此前者的堆使用效率更高。我们希望GC算法占用的额外堆空间越少越好。
通常来说,这三者不可能全部达成,即我们很难获得一个最大暂停时间短,吞吐量高,堆使用效率高的GC算法。我们需要根据实际的业务场景去使用最适合的GC算法。
简单解释一下为什么不太可能同时获得三者,因为最大暂停时间、吞吐量与算法的时间复杂度有关,而堆使用效率与算法的空间复杂度有关,我们知道通常需要牺牲空间去换取时间,或者反之去牺牲时间来换取空间,所以二者很难兼得。
0x02 标记-清除算法
- 标记阶段:从GC root对象出发,根据可达性分析,给对象打上标记。
- 清除阶段:将没有标记的对象回收。
其优点在于实现简单,堆内存效率高,而缺点则是(1)容易产生内存碎片,JVM需要维护一个空闲链表用于堆内存的分配(2)每次分配内存需要遍历空闲链表,直到找到合适大小的空闲块,时间复杂度为O(n)。
0x03 复制算法
堆分为from空间和to空间,对象在from空间上进行分配。
- GC开始时,将根对象从from空间移动到to空间
- 从根对象开始遍历,采用追加的方式将引用到的对象复制到to空间
- 清理from空间,交换from空间和to空间的名称
缺点显而易见,对象分配只能使用一半的堆内存,同时复制对象增加了额外的时间开销。优点在于追加的方式不会产生内存碎片。
0x04 标记-整理算法
标记-整理算法解决了标记-清除算法的内存碎片问题,主要步骤包括
- 标记对象
- 将所有标记对象移动到内存数组的前半部分,留出后半部分
- 清理后半部分的内存
其优点在于高效的堆内存使用以及无内存碎片产生,且分配内存是可以采用高效的追加方式,然而整理的过程涉及到了对象的移动,因此时间上复杂度更高。
0x05 分代算法
分代算法的思想是将对象按存活时间进行分类,新产生的对象会被放入新生代,而在多轮GC中存活的对象被认为是长期存在的对象,会被放入老年代。新生代发生GC的频率会高于老年代。
新生代进一步可分为Eden区、S-from区和S-to区。新创建的对象会被分配到Eden区,当Eden区满,则触发Young GC,使用复制算法进行回收,Eden区和S-from区的对象被复制到S-to区,并且增加每轮GC存活下的对象年龄(从0开始),当一个对象年龄大于等于某个值(默认15,具体取决于垃圾回收器),会将其从新生代转移到老年代。
并不是所有老年代的对象都是长期存活的对象,当新生代无法满且YGC无法回收这些对象时,会将这些对象放入到老年代。因此当老年代满时,会优先对新生代进行一次YGC,如果YGC无法回收则会对整个堆进行一次Full GC,Full GC的STW时间更长,频率也更低。
为什么要分代呢?目的在于更加精细化的GC策略。对于新对象,被回收的概率大,且大部分是小对象,使用复制算法不会浪费过多的内存,并且效率上有一定保证;对于老对象,一直存在的概率大,且大部分是大对象,则使用标记-清除/整理算法更适合。