探秘常见垃圾回收算法及对应回收器

1个月前发布 gsjqwyl
21 0 0

探秘常见垃圾回收算法及对应回收器

垃圾回收算法有哪些?

常见的垃圾回收算法包含四类,分别为标记清除法、标记整理法、复制算法以及分代收集算法。

  • 标记清除算法:首先借助可达性原理遍历内存,对存活对象和垃圾对象进行标记。标记完成后,统一回收所有被标记的对象。该算法效率相对较低,还会产生较多不连续的空间碎片。
  • 复制清除算法:属于半区复制方式,应用于新生代垃圾回收场景。把内存划分为大小相等的两块,每次使用其中一块。当该块内存用尽时,将仍存活的对象复制到另一块,之后一次性清理当前使用的空间。特点:实现简便,运行高效,但可用内存减半,造成空间浪费。
  • 标记整理算法:针对老年代特性提出的标记算法,标记流程与标记 – 清除算法一致,但后续步骤并非直接清理可回收对象,而是让所有存活对象向一端移动,随后清理边界外的内存。
  • 分代收集算法:依据各个年代的特性选用最为适宜的收集算法。通常把堆划分为新生代和老年代。新生代采用复制算法,老年代运用标记清除算法或标记整理算法。在新生代里,每次垃圾收集时多数对象会消亡,仅有少量存活,采用复制算法较为恰当,只需付出少量存活对象的复制成本便可完成收集。老年代对象存活率高,适合运用标记 – 清理或标记 – 整理算法开展垃圾回收。

JVM 新生代垃圾回收如何避免全堆扫描?

JVM 在开展新生代垃圾回收时,通过下述方式避免全堆扫描:
卡表(Card Table)机制:JVM 利用卡表记录老年代对新生代对象的指针变动情况,如此在进行新生代回收时,仅扫描老年代中确实存在引用指向新生代的区域,避免了全堆扫描。
写屏障(write Barier):当老年代中的对象引用新生代对象时,写屏障会拦截该引用,并在卡表中标记相应信息。如此,垃圾回收器扫描时只需检查标记的区域,无需遍历整个老年代。

什么是指针碰撞

在Java里,指针碰撞是垃圾收集算法中用于内存分配的一种方式。它常应用于实现停顿时间较短的垃圾收集器,像复制算法和标记整理算法。

指针碰撞的基本理念是把堆内存分为两个区域:其一为已分配的对象区域,其二为未分配的空闲区域。借助一个指针来划分这两个区域。当需要分配对象时,垃圾收集器会对比对象大小与空闲区域大小,若空闲区域能容纳对象,就将指针向前移动对象的大小,并把指针的旧值作为对象的起始地址返回。若空闲区域无法容纳对象,便进行垃圾回收操作,释放部分内存后再行分配。

指针碰撞的优势在于内存分配速度极快,仅需简单移动一个指针就能完成。并且由于已分配对象区域和未分配空闲区域是连续的,所以内存利用率较高。

然而,指针碰撞算法的缺陷是要求堆内存具备连续性,即堆内存得是一块连续的空间。这在某些内存分配场景下可能构成限制,因为连续空间易受碎片化影响,致使无法分配足够大的对象。故而,实际应用中,指针碰撞算法通常与其他内存分配算法配合使用,以弥补其局限性。

有哪些垃圾回收器?

并行收集

指多条垃圾收集线程并行运作,不过此时用户线程仍处于等待状态。

并发收集

表示用户线程与垃圾收集线程同时工作(不一定是并行的,可能交替执行)。用户程序持续运行,而垃圾收集程序在另一CPU上运行

吞吐量

也就是CPU用于运行用户代码的时间和CPU总消耗时间的比率(吞吐量 = 运行用户代码时间 /(运行用户代码时间 + 垃圾收集时间))。比如:虚拟机总共运行100分钟,垃圾收集器花费1分钟,那么吞吐量便是99%

串行Serial / Serial Old 收集器

  • 特点:单线程、简洁高效(相较于其他收集器的单线程),采用复制算法。针对限定单个CPU的环境,Serial收集器因无线程交互开销,专注垃圾收集可获取最高单线程收集效率。收集器进行垃圾回收时,必须暂停其他所有工作线程,直至结束(Stop The World)。参数:-XX:+UseSerialGC -XX:+UseSerialOldGC

Serial Old 是Serial收集器的老年代版本:采用标记整理 算法
特点
– 单线程收集器
– 收集效率高,不会产生对象引用变更
– STW时间长
– 使用场景:适合内存小几十兆以内,比较适合简单的服务或者单CPU服务,避免了线程交互的开销。
– 优点:小堆内存且单核CPU执行效率高。
– 缺点:堆内存大,多核CPU不适合,回收时长非常长。

ParNew 收集器

年轻代:-XX:+UserParNewGC 老年代搭配 CMS
ParNew收集器其实就是Serial收集器的多线程版本
特点:多线程、ParNew收集器默认开启的收集线程数与CPU的数量相同,在CPU非常多的环境中,可以使用-XX:ParallelGCThreads参数来限制垃圾收集的线程数。和Serial收集器一样存在Stop The World问题

CMS 收集器

老年代:-XX:+UserConcMarkSweepGC年轻代搭配ParNew
Concurrent Mark Sweep,一种以获取最短回收停顿时间为目标的老年代收集器
特点:基于标记清除算法 实现。并发收集、低停顿,但是会产生内存碎片

运行过程分分为下列4步:
1. 初始标记:标记GCRoots直接关联的对象以及年轻代指向老年代的对象,会发生Stop the word。但是这个阶段的速度很快,因为没有向下追溯,即只标记一层。

例如:Math math = new Math();此时new Math()即为math的直接引用对象,再往下为间接引用不做记录,例如构造方法中引用了其他成员变量
2. 并发标记:接着从gc roots的直接引用对象开始遍历整条引用链并进行标记,此过程耗时较长,但无需停顿用户线程,可与垃圾收集线程一起并发运行。由于用户线程继续运行,因此可能会导致已经标记过的对象状态发生改变,这个阶段采用三色标记算法 , 在对象头(Mark World)标识了一个颜色属性,不同的颜色代表不同阶段,扫描过程中给与对象一个颜色,记录扫描位置,防止cpu时间片切换不需要重新扫描。
3. 重新标记:为了修正并发标记 期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录。仍然存在Stop The World问题,这里会慢一些
4. 并发清理:标记结束之后开启用户线程,同时垃圾收集线程也开始对未标记的区域进行清除,此阶段若有新增对象则会被标记为黑色,不做任何处理

在整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,不需要进行停顿。
应用场景:适用于注重服务的响应速度,希望系统停顿时间最短,给用户带来更好的体验等场景下。如web程序、b/s服务
优点
– 并发收集;
– STW时间相对短,低停顿;
缺点
– 吞吐量低: 低停顿时间是以牺牲吞吐量为代价的,导致 CPU 利用率不够高。
– 内存碎片问题:CMS本质上是实现了标记清除算法的收集器,这意味着会产生内存碎片,当碎片化非常严重的时候,这时候有大对象进入无法分配内存时会触发FullGC,特殊场景下会使用Serial收集器,导致停顿不可控。
– 无法处理浮动垃圾,需要预留空间,可能出现 Concurrent Mode Failure。浮动垃圾是指并发清除阶段由于用户线程继续运行而产生的垃圾,这部分垃圾只能到下一次 GC 时才能进行回收。由于浮动垃圾的存在,因此需要预留出一部分内存,意味着 CMS 收集不能像其它收集器那样等待老年代快满的时候再回收。如果预留的内存不够存放浮动垃圾,就会出现 Concurrent Mode Failure,这时虚拟机将临时启用 Serial Old 来替代 CMS,会导致停顿时间更长

三色标记算法

  • 黑色:代表了自己已经被扫描完毕,并且自己的引用对象也已经确定完毕。
  • 灰色:代表自己已经被扫描完毕了, 但是自己的引用还没标记完。
  • 白色:则代表还没有被扫描过。标记过程结束后,所有未被标记的对象都是不可达的,可以被回收。

三色标记算法的问题场景 :当业务线程做了对象引用变更,会发生B对象不会被扫描,当成垃圾回收。

public class Demo3 {

    public static void main(String[] args) {
        R r = new R();
        r.a = new A();
        B b = new B();
        // GCroot遍历R, R为黑色, R下面的a引用链还未扫完置灰灰色,R.b无引用, 切换时间分片
        r.a.b = b;
        // 业务线程发生了引用改变, 原本r.a.b的引用置为null
        r.a.b = null;
        // GC线程回来继续上次扫描,发现r.a.b无引用,则认为b对象无任何引用清除
        r.b = b;
        // GC 回收了b, 业务线程无法使用b
    }
}

class R {
    A a;
    B b;
}

class A {
    B b;
}

class B {
}

当GC线程标记A时,CPU时间片切换,业务线程进行了对象引用改变,这时候时间片回到了GC线程,继续扫描对象A,
发现A没有任何引用,则会将A赋值黑色扫描完毕,这样B则不会被扫描,会标记B是垃圾, 在清理阶段将B回收掉,错误的回收正常的对象,发生业务异常。

CMS基于这种错误标记的解决方案是采取写屏障 + 增量更新Incremental Update
在业务线程发生对象变化时,重新将R标识为灰色,重新扫描一遍,Incremental Update
在特殊场景下还是会产生漏标。即当黑色对象被新增一个白色对象的引用的时候,记录下发生引用变更的黑色对象,并将它重新改变为灰色对象,重新标记。

public class Demo3 {

    public static void main(String[] args) {
        // Incremental Update还会产生的问题
        R r = new R();
        A a = new A();
        A b = new A();
        r.a1 = a;
        // GC线程切换, r扫完a1, 但是没有扫完a2, 还是灰色
        r.a2 = b;
        // 业务线程发生引用切换, r置灰灰色(本身灰色)
        r.a1 = b;
        // GC线程继续扫完a2, R为黑色, b对象又漏了~
    }
}

class R {
    A a1;
    A a2;
}

class A {
}

当GC 1线程正在标记O, 已经标记完O的属性 O.1, 准备标记O.2时,业务线程把属性O,1 = B,这时候将O对象再次标记成灰色, GC
1线程切回,将O.2线程标记完成,这时候认为O已经全部标记完成,O标记为黑色, B对象产生了漏标, CMS针对Incremental
Update产生的问题,只能在remark阶段,暂停所有线程,将这些发生过引用改变过的,重新扫描一遍。

吞吐量优先Parallel

  • 多线程
  • 堆内存较大,多核CPU
  • 单位时间内,STW(stop the world,停掉其他所有工作线程)时间最短
  • JDK1.8默认使用的垃圾回收器

Parallel Scavenge 收集器

新生代收集器,基于复制算法
实现的收集器。特点是吞吐量优先,故也称为吞吐量优先收集器,能够并行收集的多线程收集器,允许多个垃圾回收线程同时运行,降低垃圾收集时间,提高吞吐量。 Parallel Scavenge 收集器关注点是吞吐量,高效率的利用 CPU 资源。 CMS 垃圾收集器关注点更多的是用户线程的停顿时间。

Parallel Scavenge 收集器提供了两个参数用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间的
-XX:MaxGCPauseMillis 参数以及直接设置吞吐量大小的-XX:GCTimeRatio 参数。
– -XX:MaxGCPauseMillis 参数的值是一个大于0的毫秒数,收集器将尽量保证内存回收花费的时间不超过用户设定值。
– -XX:GCTimeRatio 参数的值大于0小于100,即垃圾收集时间占总时间的比率,相当于吞吐量的倒数。

该收集器的目标是达到一个可控制的吞吐量。还有一个值得关注的点是:GC自适应调节策略 (与ParNew收集器最重要的一个区别)

GC自适应调节策略: Parallel Scavenge收集器可设置-
XX:+UseAdptiveSizePolicy参数。当开关打开时不需要手动指定新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRation)、晋升老年代的对象年龄(-XX:PretenureSizeThreshold)等,虚拟机会根据系统的运行状况收集性能监控信息,动态设置这些参数以提供最优的停顿时间和最高的吞吐量,这种调节方式称为GC的自适应调节策略。

  • 使用场景:适用于内存在几个G之间,适用于后台计算服务或者不需要太多交互的服务,保证吞吐量的服务。
  • 优点:可控吞吐量、保证吞吐量,并行收集。
  • 缺点:回收期间STW,随着堆内存增大,回收暂停时间增大。

Parallel Old 收集器

是Parallel Scavenge收集器的老年代版本
– 特点:多线程,采用标记整理算法 (老年代没有幸存区)
– 响应时间优先
– 多线程
– 堆内存较大,多核CPU
– 尽可能让单次STW时间变短(尽量不影响其他线程运行)

G1垃圾回收期

详情可以看这篇文章:G1收集器

CMS 垃圾回收流程是怎样的?

  1. 初始标记(initial mark):在这个阶段,CMS 会进行一个快速的初始标记,标记所有根对象(如栈中的引用)直接可达的对象。此过程是 STW的,但时间较短
  2. 并发标记(Concurrent marking):初始标记后,CMS 进入并发标记阶段,在此阶段,垃圾收集器与应用线程并发运行,从上一步标记的根直接可达对象开始进行tracing,递归扫描所有可达对象。此阶段可能会持续较长时间。
  3. 并发预清理(Concurrent precleaning):这个阶段也是和应用线程并发,就是想帮重新标记阶段先做点工作,扫描一下卡表脏的区域和新晋升到老年代的对象等,因为重新标记是 STW 的,所以先分担一点。
  4. 可中断的预清理阶段(AbortablePreclean):这个和上一个阶段基本上一致,就是为了分担重新标记标记的工作,但是它可以被中断
  5. 重新标记(remark):这个阶段是 STW 的,因为并发阶段引用关系会发生变化,所以要重新遍历一遍新生代对象、Gc Roots、卡表等,来修正标记
  6. 并发清理(Concurrent sweeping):CMS 进行并发清除阶段,标记为不可达的对象会被清除。此过程与应用线程并发运行,旨在减少停顿时间。
  7. 并发重置(Concurrent reset):这个阶段和应用线程并发,重置 cms内部状态。

cms 的瓶颈就在于重新标记阶段,需要较长花费时间来进行重新扫描。

G1垃圾回收流程是怎样的?

G1 从大局上看分为两大阶段,分别是并发标记和对象拷贝。
– 并发标记:并发标记是基于 SATB 的,可以分为四大阶段:
1. 初始标记(initial marking),这个阶段是 STW 的,扫描根集合,标记根直接可达的对象即可。在G1中标记对象是利用外部的bitmap来记录,而不是对象头。
2. 并发阶段(concurent marking),这个阶段和应用线程并发,从上一步标记的根直接可达对象开始进行tracing,递归扫描所有可达对象。SATB 也会在这个阶段记录着变更的引用。
3. 最终标记(final marking),这个阶段是 STW 的,处理 SATB 中的引用。
4. 清理阶段(clenaup),这个阶段是 STW 的,根据标记的 bitmap 统计每个 region 存活对象的多少,如果有完全没存活的 region 则整体回收.
– 对象拷贝阶段(evacuation):这个阶段是 STW 的。根据标记结果选择合适的 reigon 组成收集集合(collection set 即
CSet),然后将 CSet 存活对象拷贝到新 region 中

G1 的瓶颈在于对象拷贝阶段,需要花较多的时间来转移对象。

什么是三色标记算法

三色标记法是一种用于垃圾回收算法中的对象标记方法,特别用于标记清除
型垃圾回收器。这种方法通过使用三种颜色(白色、灰色和黑色)来跟踪对象的可达性和垃圾回收状态,以避免对象的重复回收和丢失。

三色标记的基本概念:
– 白色:表示对象尚未被检查。白色对象可能是垃圾,直到证明它们是可达的。
– 灰色:表示对象被检查过,并且其本身是可达的,但其引用的对象还未全部检查。
– 黑色:表示对象和它所有引用的对象都已检查且是可达的。

三色标记步骤:
1

© 版权声明

相关文章

没有相关内容!

暂无评论

none
暂无评论...