JVM垃圾收集器

JVM都有哪些垃圾收集器,分别用于什么场景,详细介绍CMS、G1垃圾收集器

不同的垃圾收集器有不同的特性,适用于不同的场景,往常JVM的垃圾收集器是配合使用的,但是现代垃圾收集器是比较强大的、独立的。

年轻代和老年代的垃圾收集器并不可以随意搭配,如CMS就不能和ParallelScavenge搭配;

Serial/SerialOld

单线程串行收集器,简单高效

  • Serial:复制算法收集年轻代;
  • Serial Old:标记-整理收集老年代;

行为:

1、必须暂停其他所有的工作线程,直到它收集结束(Stop The World)

2、Client模式下,新生代的默认收集器。

在用户场景下,JVM管理的内存不会很大,Serial收集器在整理200M以内的内存,可以控制在100ms以内,可以接受。

ParallelNew/ParallelOld

多线程收集器

  • ParNew:复制算法收集新生代;
  • ParallelOld:标记-整理收集老年代;

默认开启的线程数量与CPU核数相同,可以使用

-XX:ParallelGCThreads
参数来设置线程数;

ParallelScavenge

针对年轻代的多线程收集器,使用复制算法;

目的:与ParallelNew关注点不同,尽可能缩短垃圾收集时用户线程的停顿时间;

吞吐量优先:CPU用于运行用户代码的时间占总时间的比值。

控制最大垃圾收集停顿时间:

-XX:MaxGCPauseMillis

直接设置吞吐量大小:

-XX:GCTimeRatio

CMS

JDK9标记弃用,JDK14正式弃用; Concurrent Mark Sweep:并发标记—清除算法:并发收集,低停顿;

  • CMS仅是老年代垃圾收集器;
  • 年轻代会搭配:Parallel New收集器 / Serial收集器;

工作过程

1、初始标记:STW;仅仅标记 GC Roots 能直接关联到的对象,速度很快; 2、并发标记:进行 GC Roots Tracing 的过程,它在整个回收过程中耗时较长,不需要停顿;

  • 进行[三色标记]
  • 并发标记期间,新晋老年代对象、引用发生变化的对象,都会被标记为Dirty; 3、重新标记:STW;修正并发标记期间用户程序又产生的新的引用、新的对象(即上一步标记的Dirty对象);
  • 只关注Dirty对象,继续使用三色标记进行标记
  • 完成之后,清除Dirty标记; 4、并发清除:不需要停顿,耗时略长,直接清理所有的白色标记对象

缺点

1、CMS垃圾收集器在垃圾收集过程中的CPU使用率高 2、会以抢占的方式执行GC线程,抢占用户线程资源;

  • 吞吐量低:低停顿时间是以牺牲吞吐量为代价的,导致CPU利用率不够高;
  • 在并发清除时,用户线程仍会产生垃圾,这些CMS无法处理;
  • 收集算法导致:会产生内存碎片

G1

一些参考:

内存模型

G1:Garbage First(JDK9默认收集器)

  • Region间采用复制算法;
  • 整体采用标记清除算法;

Region

在G1之前的垃圾回收算法中,每个区域的内存都是连续的,G1中每个区域物理内存不再连续,而是分块:

  • 内存依然分区,但是会进一步分为:Region块;并对不同类型的块,进行逻辑上的分代标记
    • E:Eden
    • S:Survivor(From/To)
    • O:Old
    • H:特殊Region,当单个对象大于Region的一半,则单独存储在Humongous
  • Region物理上不连续;且Region也可以转变类型,如Eden变成Old;以此调整分区间比例大小
  • Region都是2的整数次幂:1MB、2MB、4MB...(
    -XX:G1HeapRegionSize=1m
    指定)
  • Region是动态分配的,如当Eden不够用时,分配一个,再将新对象放进去;或者老年代不够,动态分配一个老年代Region;
  • 每个代的Region个数,也是动态的;当收集Eden时间不能达到预期时,下次Eden的总Region就会相应减少,以达到预期收集时间;

Card/Card Table(CT)

Card:每个Region被分割为一个个Card;

Card Table:一个字节数组,存储了Region中Card的内存地址;可以对每个Card进行索引;

  • 每个Card:512Byte;
  • 对象可以占用一个或多个Card;
  • Card Table中存储Card的内存地址;可以进行随机查找;

Remember Set

每个Region都维护一个RSet,标记着当前的Region引用了其他哪些Region里的对象;本质是一个哈希表

最终目的是为了在真正触发回收动作时,确定哪些Region是回收目标Region(处于CSet中的Region);

通过RSet引用关系,确定CSet回收范围

  • 通过Card Table数组 + 哈希表进行跨Region的引用记录;
  • Region中的每个对象,都以Card为单位存储,可以占用一个或多个;并由Card Table数组索引,可以做到随机访问;
  • RSet可以通过哈希表,O(1)的复杂度获取引用对象所在位置;再通过Table Card,锁定对象所在Card;

因此GC Roots对象根据RSet + Card Table可以快速进行Region间的可达性算法分析;

RSet需要记录的引用类型

  • 老年代引用新生代:需要记录到RSet;如图所示,新生代被引用的对象不应该被回收;
  • 老年代引用老年代:需要记录到RSet;因为老年代回收时不一定全部扫描;(如图中B一定是老年代Region)

RSet不需要记录的引用类型

  • Region内部对象间引用:不需要记录到RSet;
  • 新生代Region间的对象引用,不需要记录到RSet,因为GC时会包含整个新生代,可达性分析中可以关联到所有新生代间的引用,不会导致存在引用的对象被回收;
  • 新生代引用老年代:不需要记录到RSet,新生代对象被回收,不影响老年代;

写屏障

RSet在程序运行期间,通过写屏障,不断地进行更新;

每当用户线程分配新的对象到Region中,都会触发写屏障,会将新分配的对象所在Card,标记为Dirty Card

Collection Set

CSet:记录了可被回收的Region的候选;可以来自所有分代的Region;

  • 年轻代的Region会一直在CSet中,也就是任何GC都会对年轻代进行回收;
  • 老年代对年轻代的引用,会通过记录在RSet中,再标记过程中,被添加到CSet中;

当GC结束,CSet中存活的数据,会被移动到别的可用分区;然后将这些Region清空;

G1如何控制停顿时间

1、Young GC:通过控制年轻代Region个数的分配,控制Young GC的停顿; 2、Mixed GC:通过优先列表,GC时优先选择收益高的Region;

收集过程

  • G1没有严格的Full GC;通常是Young GC和Mixed GC(同时收集年轻代和老年代)
  • 标记算法同样采用[三色标记]

G1并发标记(SATB算法)

并发标记是GC触发的前提,标记完成后,根据内存情况,触发Young GC或Mixed GC;

  1. 初始标记:STW(这阶段需要STW,但耗时很短;)

    • 标记GC Roots能直接关联到的对象
    • 同时生成快照图,标记此时的存活对象,并通过一个指针(NextTams)锁定每个Region中内存的最大位置;
    • 并且修改TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可用的Region中创建新对象
  2. 并发标记:(与用户程序并发进行可达性分析)

    • 期间的新对象,会在NextTams指针后进行分配,并且会直接标记为存活对象;(以此来解决三色标记中可能漏标的问题)
    • 期间产生的新对象,同时会通过写屏障,写入本地队列中;
    • 期间会进行RSet的更新;
    • SATB算法可能误判,但是追求速度快;
  3. 最终标记:STW

    • 并发标记期间因用户程序继续运作,可能产生新的引用、垃圾;在这个阶段进行RSet记录修复;
    • 直到三色标记完成;
  4. 筛选回收:

    • 首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划,这个阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分Region,时间是可控制的,而且停顿用户线程将大幅提高收集效率。

Young Collection

年轻代大小从5%到60%动态调整;

YoungGC触发:Eden区达到阈值,达到可以在

MaxGCPauseMillis
预期时间内完成收集的最大程度,则触发YoungGC:(STW)

  • 根据最大停顿时间(MaxGCPauseMillis),选择最佳回收策略;
  • 根据GC Root可达算法分析和RSet的记录,对选中的Region进行标记;
  • 将可达的对象复制到新的Region(不够年龄的对象进Survivor,达到年龄的对象,进入老年代);
  • 将这些Region清空,完成垃圾回收;

1、GC Roots扫描,同时进行三色标记,标记可达的对象;(使用SATB算法与用户线程并发标记) 2、触发Evacuation Young GC:开启并发GC线程,进行复制 + 清除;

  • 将Eden存活对象拷贝至Survivor区
  • 将Survivor存活对象拷贝至另一个Survivor;
  • 满足年龄要求,或超大对象,直接进入老年代;
  • 清除CSet中的Region内存、RSet、Card Table等;

Mixed GC

通过SATB算法进行并发标记后,如果整个堆内存已使用的大小,达到了阈值45%(

-XX:InitiatingHeapOccupancyPercent=45%
),则触发Mixed GC,进行整个堆空间的垃圾回收;

G1根据设置的停顿时间

-XX:MaxGCPauseMillis=200ms
,根据优先列表,来制定回收计划,有选择地回收那些价值高更的Region

Full GC

G1的目标就是尽可能避免Full GC,但是仍然有可能触发Full GC;

通常触发Full GC的原因:Allocation Failure;通过日志可以查看具体原因和回收情况:

  • Full GC会是一个单线程的STW;会停顿较长时间,进行完整的内存回收;
  • 如果是几天触发以此FullGC,可能并无大碍;
  • 如果是几个小时就触发,则可能存在问题;

收集特点

1、低停顿、没有内存碎片(Region间复制算法);

2、可预测停顿 因为Region的分区,G1可以进行部分区域的回收,可以缩小回收范围;

G1会跟踪每个Region的价值大小(回收获得的空间/回收需要的时间),维护一个优先列表,根据允许的停顿时间,指定回收计划,优先回收价值最大的Region,有限时间内获得更高的收集效率;

3、适用于大内存环境,堆内存6-8G以上;

  • G1收集器为了收集算法的时间,用了很多空间换时间的操作;需要更多的内存;
  • 如:Card Table、RSet等等;

G1参数配置

参数作用
-XX:+UseG1GC开启G1垃圾收集器
-XX:G1HeapRegionSize=1mG1中Region大小,只会是2n2^n:1,2,4,8...32(最大32M)
-XX:G1NewSizePercentG1初始时新生代占用总内存大小比例(5%)
-XX:G1MaxNewSizePercentG1会动态调控年轻代大小,最大不超过此比例(60%)
-XX:MaxGCPauseMillis=200msGC触发的最大停顿时间(默认200ms,不建议改,不好确定合适的值)
-XX:SurvivorRation=8Eden占新生代的8/10,剩余2/10,From/To 平分
-XX:InitiatingHeapOccupancyPercent触发 MixedGC 的内存阈值(默认老年代内存达到 45%触发)

停顿时间能否设置过小

当最大停顿时间过小,容易造成,G1判定无法达成目标,就会不进行回收,直到内存无法正常工作,触发Full GC;

为什么需要大内存

1、G1收集器会对内存进行分区,分为多个Region,如果内存过小,Region的个数就会变少,G1的灵活度就会降低,会增加扫描、标记、收集的时间,最好是3072个Region以上(2n2^n),Region 2MB以上;就最少需要6GB了;

2、G1收集器为了收集算法的时间,用了很多空间换时间的操作;需要更多的内存;如:Card Table、RSet都需要额外的空间;