JVM(十六)七种垃圾收集器

发布时间 2023-07-16 14:47:31作者: Tod4

JVM(十六)七种垃圾收集器


0 垃圾回收器的组合关系:

image-20230712201712562
  • 实现相连的垃圾回收器表示可以搭配使用:
    • Serial GC - Serial Old GC
    • ParNew GC - CMS GC
    • Parallel Scavenge GC - Parallel Old GC
    • 还有一条CMS GC - Serial Old GC表示CMS出现“Concurrent Mode Failure”后的备选方案
  • (红色虚线)为了维护和兼容性测试的成本,在jdk1.8中将Serial GC-CMS GC的组合以及ParNew GC-Serial Old GC组合废弃了,在JDK9中进行了移除
  • (绿色虚线)JDK14弃用了Parallel Scavenge GC - Serial Old GC的组合
  • (青色虚线)JDK14删除了CMS GC

上面红色虚线的“维护和兼容性测试的成本”,主要是指的单线程垃圾收集器和多线程垃圾收集器同步工作的成本

Parallel Scavenge GC并没有和同为并行垃圾收集器的CMS配合工作,而ParNew可以的原因是Parallel Scavenge GC、Parallel Old GC所使用的架构和其他的垃圾收集器均不同

0 查看默认的垃圾回收器

  • -XX:+PrintCommandLineFlags:查看命令行相关参数(包括垃圾收集器)
  • 使用命令行指令:jinfo -flag 相关垃圾回收器参数 进程ID

1 Serial 垃圾收集器:串行回收

  • Serial收集器是最基本也是历史最悠久的垃圾收集器,是JDK1.3之前回收新生代的唯一选择
  • Serial收集器是HotSpot虚拟机中Client模式下的默认垃圾收集器
  • Serial收集器采用复制算法串行回收Stop-The-World机制的方式执行对内存年轻代的回收

2 Serial Old 垃圾收集器:串行回收

​ 除了新生代外,Serial收集器还提供用于执行老年代垃圾收集的Serial Old收集器:

  • Serial Old收集器同样采用串行回收Stop-The-World机制的方式,不过内存回收算法使用的是标记-压缩算法

  • Serial Old收集器是运行在Client模式下默认的老年代的垃圾回收器

  • Serial Old收集器在Server模式下主要有两个用途:

    • 与新生代的Parallel Scavenge配合使用
    • 作为老年代的CMS垃圾回收器的后备垃圾收集方案
  • 这个收集器是一个单线程的收集器,但是单线程的含义并不仅仅是它只会使用一个CPU或者一条收集线程去完成垃圾收集工作,更重要的是:它在垃圾回收的时候,必须停止其他所有的线程,直到它收集结束(STW)

    image-20230712221754461

  • 优势:简单高效(相对于其他GC的单线程而言,少了CPU的切换),常使用在用户的桌面应用场景中,可用内存一般不大、可以在较短时间内完成垃圾收集

  • 在HotSpot虚拟机中,使用-XX:+UseSerialGC参数可以指定年轻代和老年代都使用串行收集器

    这个等价于新生代使用Serial、老年代使用Serial Old

// -XX:+PrintCommandLineFlags -XX:+UseSerialGC

3 ParNew 垃圾回收器

  • 如果说Serial GC是年轻代中的单线程垃圾收集器,那么ParNew收集器则是Serial收集器的多线程版本

    Par是Parallel的缩写,New表示只能处理新生代

  • ParNew垃圾收集器除了在新生代采用“并行回收”的方式进行垃圾回收之外,两款垃圾回收器之间没有任何的区别,即也是采用复制算法Stop-The-World的机制

  • ParNew是很多JVM运行在Server模式下的默认垃圾收集器

  • 对于新生代,回收次数频繁,使用并行方式高效

  • 对于老年代,回收次数少,使用串行的方式节省资源(CPU并行需要切换线程,串行可以省去切换线程的资源),因此在单CPU场景下,ParNew GC并不比Serial GC高效

    image-20230713190800556
  • 通过选项-XX:UseParNewGC手动指定ParNew收集器执行内存回收任务,它表示年轻代使用并行回收器,并不会影响老年代-XX:ParallelGCThreads限制垃圾回收的线程数量

4 Parallel Scavenge 垃圾回收器:吞吐量优先

  • Parallel Scavenge GCParNew GC一样采用复制算法并行回收Stop-The-World的机制进行内存垃圾回收

  • Parallel Scavenge GC的目标是达到一个可控制的吞吐量,它也称为吞吐量优先的垃圾收集器

    高吞吐量可以高效地利用CPU和时间,尽快完成程序的运算任务,因此适合在后台运算而不需要太多交互的任务,常在服务器中使用。如,执行批量处理、订单处理、工资支付、科学计算的应用程序

  • 自适应调节策略也是Parallel Scavenge GCParNew GC的一个重要区别

5 Parallel Old GC

  • Parallel Scavenge GC在JDK1.6提供了用于执行老年代垃圾收集的Parallel Old GC,用来替代老年代的单线程GC Serial Old
  • Parallel Old GC采用标记-压缩算法,基于并行回收STW机制

image-20230713193842291

  • 在吞吐量优先的场景中,Parallel收集器Parallel Old的组合,在Server模式下的内存回收性能很不错

  • JDK8中,Parallel Old是默认的垃圾收集器

    C:\Users\admin>jinfo -flag UseParallelOldGC 24432
    -XX:+UseParallelOldGC
    
    C:\Users\admin>jinfo -flag UseParallelGC 24432
    -XX:+UseParallelGC
    
  • 只要开启Parallel Scavenge GCParallel Old GC中的其中一个,就会默认激活另一个

    // ...-XX:+UseG1GC -XX:-UseLargePagesIndividualAllocation jdk19默认G1 GC
    // -XX:+PrintCommandLineFlags -XX:+UseParallelGC 开启Parallel GC
    C:\Users\admin>jinfo -flag UseParallelGC 13596
    -XX:+UseParallelGC
    
5.1 Parallel GC的参数设置
  • -XX:+UseParallelGC:设置开启Parallel GC

  • -XX:ParallelGCThreads:设置年轻代并行垃圾收集器的线程数,一般情况下最好与CPU的数量相等,以避免过多线程影响垃圾收集性能的情况

    默认情况下,如果CPU的数量小于8个,ParallelGCThreads的值等于CPU数量

    大于8的时候,ParallelGCThreads的值等于 3 + [5 * CPU_count] / 8

  • -XX:MAXGCPauseMillis:设置垃圾收集器的最大停顿时间(即STW的时间),单位是毫秒

    为了尽可能把垃圾收集器的停顿时间控制在这个时间内,收集器在工作的时候会调整堆的大小和一些参数(时间过长则减小堆大小)

    对于用户来说,停顿时间越小体验越好,但是在服务器端注重高并发和整体的吞吐量,适合Parallel进行控制

    该参数使用需谨慎

  • -XX:GCTimeRatio垃圾收集时间占总时间的比例,用于衡量吞吐量的大小

    默认大小99,意思是垃圾回收的时间不超过1%

    上一个参数暂停的时间越长,这里就越容易超过这个比例

6 CMS 垃圾收集器:低延迟(低暂停时间)

​ CMS(Concurrent Mark-Sweep)是在JDK1.5时期,HotSpot推出的一种在强交互应用中具有划时代意义的垃圾收集器:

  • 也是HotSpot虚拟机中第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程和用户线程同时工作

    区别并发和并行

    • 并发指的是垃圾收集线程用户线程同一时间段内同时工作,微观上还是串行,因为STW机制的存在

    • 并行指的是多条垃圾收集线程同一时刻同时工作

  • CMS的垃圾收集算法采用标记-清除算法,并且也会STW

  • CMS GC的关注点是尽可能缩短垃圾收集时用户线程的停顿时间。停顿时间越短(低延迟)就越适合需要频繁与用户交互的程序,良好的响应速度能够提升用户的体验

    集中在网站或者B/S系统的服务端的Java应用,尤其重视服务的响应速度,希望系统的停顿时间最短,CMS就非常适合这类应用的需求

  • CMS作为老年代的GC,无法与JDK1.4.0中的新生代GC Parallel Scavenge配合工作(底层架构不同),只能选择新生代的ParNew或者 Serial GC

6.1 CMS的工作原理
image-20230714094852627

​ CMS的整个GC过程主要分为四个阶段:初始标记阶段并发标记阶段重新标记阶段并发清理阶段

  • 初始标记阶段(Initial-Mark):在这个阶段,所有的工作线程会因为STW机制而出现短暂的暂停,这个阶段的任务仅仅是标记处GC Roots中直接关联的对象,一旦标记完成就恢复之前被暂停的所有应用线程,因为GC直接关联的对象相对较少,所以这个阶段的时间很短

  • 并发标记阶段(Concurrent-Mark):从上个阶段标记的和GC Roots直接关联的对象开始,遍历整个对象图,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行

  • 重新标记阶段(Remark):在并发标记阶段,程序的工作线程会和垃圾收集线程同时或者交叉运行,因此修正并发标记期间,因为用户线程继续运行而导致的标记产生变动的那一部分的标记记录;这个阶段的时间通常比初始标记阶段稍长一些,但是远比并发标记阶段的时间要短

  • 并发清理阶段(Concurrent-Sweep):此阶段清理删除掉标记阶段判断的已经死亡的对象,释放其内存空间。由于不需要移动对象,所以这个阶段也是可以和用户线程并发执行的

  • CMS不是像其他垃圾收集器一样,等到老年代几乎被填满了再进行收集,而是当堆内存使用率达到某一阈值的时候就开始进行回收

    这是因为在并发标记阶段,垃圾收集的同时用户线程并没有中断,因此在CMS GC垃圾收集的时候必须确保用户线程有足够的空间运行

    如果CMS运行期间预留的内存无法满足程序需要,就会出现“Concurrent Mode Failure”,这时候虚拟机就会采用预备方案:临时启用Serial Old收集器来进行老年代的垃圾收集,这样停顿时间就会变得很长了

6.1 CMS GC优缺点分析
  • CMS GC采用的是并发回收(非独占式),不过在初始标记重新标记阶段仍要遵循STW机制暂停正在工作中,不过暂停时间不会太长,因此可以说明目前的所有垃圾收集器都做不到完全不需要STW,只是尽可能地缩短暂停时间

  • 由于最耗费时间的并发标记清理阶段都不需要暂停工作,所以整体的回收是低停顿的

  • CMS GC垃圾收集算法采用的是标记-清除算法,这意味着每次执行完内存回收后,由于被执行内存回收的无用对象所占用的内存空间可能是不连续的内存块,因此不可避免地会产生一些内存碎片。那么CMS在为新对象分配内存空间的时候,将无法使用指针碰撞的技术,而只能选择空闲列表进行分配。

    image-20230714141629592
6.2 为什么CMS GC不用标记-压缩算法而用标记-清除算法?
  • 在并发清除的时候,用标记-整理算法整理内存的时候,原来的用户线程就无法继续使用内存了,Mark Compact更适合“Stop The World”的场景
  • CMS GC对暂停时间要求较高,因此采用耗时较少的标记-整理算法
6.3 CMS GC 优缺点总结
  • 优点:

    • 并发收集
    • 低延迟
  • 缺点:

    • 产生垃圾碎片,导致垃圾并发清除后,用户线程可用的空间不足,无法为大对象分配空间,提前触发Full GC

    • 对CPU资源非常敏感,在并发阶段,虽然不会导致用户停顿,但是会因为占用了一部分线程而导致应用程序变慢,总吞吐量降低

    • 无法收集浮动垃圾,可能出现“Concurrent Mode Failure”而导致另一次Full GC的产生,在并发标记阶段,由于程序的工作线程和垃圾收集线程是同时或者交叉运行的,最终会导致程序的工作线程在此阶段产生的垃圾对象得不到及时的回收,只能在下一次执行GC的时候释放这些之前未被回收的内存空间

      重新标记阶段标记的对象只是修正“因为用户线程继续运行而导致的标记产生变动的那一部分的标记记录”,因此并不会对用户进程新产生的标记记录进行标记

      既然都STW了,为啥重新标记阶段只将记录变动修正而不再将此阶段用户对象产生的浮动垃圾进行标记回收呢?(实际上标记只标记的可达对象,而不是垃圾)

      这是因为用户程序如果对初始标记死亡的对象进行了关联,直接通过改这个对象的Header设置为可达进行修正即可,但是如果是产生的浮动垃圾,则想把这个对象变为不可达,则需要再进行一次GC Roots遍历判断根可达,才能把相关对象设置为不可达,那么这个阶段就需要STW,显然失去设计的初衷:使垃圾收集和用户线程并发执行

6.4 CMS收集器的参数设置
  • -XX:+UseConcMarkSweepGC:手动指定CMS垃圾收集器执行内存回收任务

    开启改参数后,会自动将-XX:+UseParNewGC打开,即:ParNew(Young GC) + CMS(Old GC) + Serial GC(Old 备用方案)

  • -XX:CMSLnitialtingOccupanyFraction设置堆内存使用率的阈值,一旦达到该阈值便开始进行回收

    • JDK5及以前的版本默认为68,JDK6及以后为92%,即老年代的空间使用率达到该值,就会执行一次CMS回收
    • 可以通过设置该值有效降低Full GC的执行次数,当内存增长缓慢可以设置一个较大的值,反之设置一个较小的值
  • -XX:+UseCMSCompactAtFullCollection:用于指定在执行完Full GC 后对内存空间进行压缩整理,以此避免内存碎片的产生,不过整理的过程中无法并发执行用户进程,因此带来的问题就是停顿时间变得更长了

  • -XX:CMSFullGCsBeforeCompaction:设置在执行多少次Full GC之后才对内存进行整理

  • -XX:ParallelCMSThreads:设置CMS的线程数量

    • 默认启动的线程数量是(ParallelGCThreads + 3) / 4,ParallelGCThreads是年轻代并行垃圾收集器的线程数
6.5 收集器小结以及版本的变化

​ Serial GC、Parallel GC、Concurrent Mark Sweep GC三个GC有什么不同吗?

  • 如果想最小化地使用内存和并行开销,选择Serial GC
  • 如果想最大化应用程序的吞吐量,选择Parallel GC
  • 如果想最小化GC的中断、停顿时间,选择CMS GC

​ GC的主要版本变化:

  • 在JDK9中,CMS被标记为Deprecate
  • JDK14移除CMS收集器

7 G1 垃圾收集器:区域化分代式垃圾回收

​ 随着业务越来越庞大、复杂,用户越来越多,也为了适应不断扩大的内存和不断增加的处理器数量,进一步降低延迟时间并兼顾好吞吐量,因此发布了“全功能收集器G1 GC

  • G1是一个并行收集器,它把内存划分成很多不想关的区域(Region,物理上可以是不连续的),使用不同的Region来表示Eden、幸存者0区、幸存者1区以及老年代

  • G1会有计划地避免在整个Java堆中进行全区域的垃圾收集,G1追踪各个Region后面的垃圾堆积的价值大小(回收所获得的空间大小以及所需时间的经验值),并在后台维护一个优先列表,每次根据允许的收集时间,优先回收最大的Region

    也正是因为这种方式的侧重点在于回收垃圾最大量的区间,所以给G1起名为Garbage First GC

  • G1是一款面向服务器端应用的垃圾收集器,主要针对配备多核CPU以及大容量内存的机器,以极高概率满足GC停顿时间的同时,还能够兼顾吞吐量的性能特征

  • 在JDK1.7中正式启用,移除了Experiment的标识,是JDK9及以后的默认垃圾收集器,取代了CMS和Parallel + Parallel Old的组合,被Oracle称为“全功能垃圾收集器”

  • CMS在JDK9中被标记为废弃,在JDK8中G1还不是默认垃圾收集器,需要使用-XX:+UseG1GC来启用

7.1 G1 GC的优势与不足

优点:

  • 兼顾并行与并发

    • 并行:G1在回收期间,可以有多个GC线程同时工作,有效利用多核计算能力,此时用户线程STW
    • 并发:G1具有与应用线程交替执行的能力,部分工作可以和应用程序同时执行,因此一般来说不会出现在回收阶段出现完全堵塞应用的情况
  • 分代收集

    • G1仍然属于分代垃圾收集器,它区分年轻代和老年代,年轻代仍然有Eden区和Survivor区,但是从堆的内存结构上来看,它不要求整个Eden区、年轻代或者老年代都是连续的,也不坚持固定大小和固定数量
    • 将堆空间划分为了若干个区域(Region),这些区域包含了逻辑上的年轻代和老年代
    • 能够同时兼顾年轻代和老年代的垃圾回收
  • 空间整合

    • CMS:标记-清除算法、产生的内存碎片在多次Full GC后进行一次整理
    • G1:将内存划分为一个个的Region,内存的回收是以Region为单位的,Region之间是复制算法,整体上可以看做是标记-压缩算法(两种算法都可以避免内存碎片),这种特性有利于程序的长时间运行:分配的大对象不会因为找不到足够的连续内存而提前触发GC,尤其当堆特别大的时候,G1的优势更加明显
  • 可预测的停顿时间模型(软实时 soft real-time)

    • G1除了追求低停顿外,还能够建立可预测的停顿时间模型,让使用者明确指定一个长度为M毫秒的时间片片段内,消耗的垃圾回收的时间不超过N毫秒(吞吐量则为(M-N)/M)
    • G1会有计划地避免在整个Java堆中进行全区域的垃圾收集,G1追踪各个Region后面的垃圾堆积的价值大小(回收所获得的空间大小以及所需时间的经验值),并在后台维护一个优先列表,每次根据允许的收集时间,优先回收最大的Region,保证了G1在有限的时间内获取尽可能高的回收效率
    • 相比CMS,G1未必能够做到CMS在最好情况下的延时停顿,但是最差情况也要好很多

缺点:

  • 在用户程序运行的过程中,G1无论是垃圾收集产生的内存占用还是运行程序时的额外负载,都要比CMS高很多

    一般要高10%-20%,经验来说在小内存上CMS表现大概率由于G1,G1在大内存应用上则发挥其优势,平衡点在6-8G之间

G1的记忆集与写屏障

​ 由于

  • 一个对象可能会被不同区域引用
  • 一个Region不可能是孤立的,一个Region中的对象可能被其他任意Region的对象引用,判断对象存活的时候,就需要扫描整个Java堆才能保证准确
  • 这样就导致回收新生代的时候也需要扫描老年代,降低Minor GC的效率

​ 因此G1 GC采用记忆集(Remembered Set)来避免全局扫描:

  • 每一个Region都有一个对应的Remembered Set
  • 每次Reference类型数据在进行写操作的时候,都会产生一个Write Barrier(写屏障)暂时中断操作
  • 中断后就检查要写入的引用指向的对象是否和该Reference类型数据在不同的Region(对比其他垃圾收集器只是检查老年代对象是否引用了新生代对象)
  • 如果不同,则通过CradTable(卡表,记忆集的具体实现)把相关引用信息记录到引用指向对象所在Region对应的Remembered Set
  • 当进行垃圾收集时,在GC根节点的枚举范围(GC Roots)加入Remember Set,这样就保证不进行全局扫描也不会有遗漏
image-20230715170651903
7.2 G1 回收器参数设置
  • -XX:UseG1GC:启用G1 GC执行内存回收任务
  • -XX:G1HeapRegionSize:设置每个Region的大小,值是2的幂,范围是1M到32M之间,目标是根据最小的Java堆的大小划分出约2048个区域,默认是堆内存的1/2000
  • -XX:MaxGCPauseMillis:设置期望达到的最大GC停顿时间指标(软实时),默认值是200ms
  • -XX:ParallelThreads:设置STW工作线程数,默认是8
  • -XX:ConcGCThreads:设置并发标记的线程数,将n设置成并发标记回收线程数的1/4左右
  • -XX:InitialtingHeapOccupancyPercent:设置触发并发GC周期的Java堆占用率阈值,默认为45
7.3 G1 GC 的适用场景
  • 面向服务端应用,针对具有大内存、多处理器的机器,最主要的应用是需要低GC延迟,并具有大堆的应用程序提供解决方案
  • 用来替换JDK1.5中的CMS收集器,以下场景中使用G1会比CMS要好:
    • 超过50%的Java堆被活动数据占用
    • 对象分配频率或者年代提升频率变化很大
    • GC停顿时间过长(长于0.5至1秒)
  • 当JVM的GC线程处理速度缓慢的时候,系统会调用应用程序线程帮助加速回收过程,而其他垃圾收集器只能使用内置的JVM线程执行GC的多线程工作
7.4 Region的使用
  • 使用G1垃圾回收器收集垃圾的时候,它将Java堆划分为了约2048个大小相同的独立Region块,每个Region块的大小根据堆空间的实际大小而定,整体被控制在1MB到32MB之间,且为2的幂次方,可以通过-XX:G1HeapRegionSize设置。所有的Region大小相等,且在JVM生命周期内不会改变

  • 虽然还保留由新生代和老年代,但是新生代和老年代不再是物理隔离的了,他们还是一部分Region(不需要连续)的集合,通过Region的动态分配方式实现逻辑上的连续

  • 一个Region在同一时间只能属于一个角色(Eden、Survivor、Old/Tenured),如下空白表示未使用的内存区域

    image-20230715155418972

  • H表示Humongous区域,主要用于存储大对象,如果超过1.5个region就放到该区域

    对于Java堆中的大对象,默认会分配到老年代,但是如果是暂时存在的大对象,则会对垃圾收集器造成负面影响,为了解决这个问题,G1划分H区专门用来存储大对象。如果一个H区装不下一个大对象,则寻找多个连续的H区存储,如果还没有则进行Full GC,G1的大多数行为将H区看做老年代的一部分

  • Region采用的指针碰撞的方式进行内存分配,并在其中设置TLAB(Thread Local Allocation Buffer),减少多线程访问堆内存的加锁消耗,提高对象分配的效率

    image-20230715160045895
7.5 G1 GC 的过程
image-20230715161212749

​ G1 GC的垃圾回收过程主要包括以下三个环节:

  • 年轻代GC(Young GC):当年轻代的Eden区用尽的时候,开始进行年轻代的回收过程;G1的年轻代收集阶段是一个并行(多条垃圾收集线程)的、独占式(STW)的收集器。在年轻代回收期间,会暂停所有的应用程序线程,启动多线程执行年轻代收集,然后从年轻代区间移动存活对象到Survivor区间或者老年代区间,也有可能是两个区间都涉及。G1 GC的新生代GC是回收掉Eden区所有的Region

    ​ 具体来说,在暂停应用程序的执行后(Stop The World)后,G1创建回收集(Collection Set),回收集是指需要被回收的内存分段的集合,年轻代回收过程的回收集包括年轻代Eden区和Survivor的所有内存分段。 如下图,新生代在进行YoungGC的时候,伊甸园区存活的对象和Survivor区存活的对象就使用复制算法复制保存到一个空闲Region,这个空闲的Region就变成了Survivor区,原有的空间就进行清空处理

    image-20230715191431835
    1. 扫描根:根是指的static变量指向的对象、正在执行的方法调用链条上的局部变量等(GC Roots)。根引用(GC Roots)连同记忆集(Remember Set)记录的外部引用作为扫描存活对象的入口

    2. 更新记忆集(Remember Set):处理direty card queue中的card,更新Remember Set。此阶段完成后,RSet就能够准确反映老年代对现在所在内存分段中对象的引用关系了

      对于应用程序中的引用复制语句,JVM并不会立刻更新记忆集(RSet)(直接更新则需要对这些应用程序进行同步操作),而是在direty card queue中入队一个保存了对象引用信息的card,在年轻代回收的时候,G1 GC再对队列中的所有card进行处理以更新记忆集

    3. 处理RSet:识别RSet中被老年代对象指向的Eden中的对象,这些指定的Eden中的对象就被认为是存活的对象。

    4. 复制对象:此阶段,对象树被遍历,Eden区内存段存活的对象会被复制到Survivor区中空的内存段,Survivor区中内存段中的存活对象如果年龄未达到阈值,年龄会+1,达到则复制到Old区中空的内存分段。如果Survivor空间不足,Eden空间的部分数据则会直接晋升到老年代。

    5. 处理引用:处理Soft、Weak、Phantom、Final、JNI Weak等引用。最终Eden区的数据为空,GC停止工作,而目标内存中的对象都是连续存储的,没有碎片,所以复制算法可以达到内存整理的效果,减少碎片。

  • 老年代并发标记过程(Concurrent Marking):当堆内存达到一定的值(默认45%)时,开始老年代并发标记过程。

    1. 初始标记阶段标记从根节点直接可达的对象,这个阶段是STW的,并且会触发一次Young GC
    2. 根区域扫描(Root Region Scanning):G1 GC扫描Survivor区可以直接可达的、老年代区域的对象,并标记被引用的对象,这一过程必须在Young GC前完成。(由于新生代采用复制算法,不在YGC前完成Survivor就变了 from -> to,新的Survivor还包含其他Eden、Survivor Region的对象,就不好找了
    3. 并发标记(Concurrent Marking):在整个堆中进行并发标记(指的是和应用程序并发执行),此过程可能被YGC中断,在并发标记阶段,若发现区域对象中的所有对象都是垃圾,则这个区域可以直接被回收。同时,还会计算每个区域的对象活性(区域中存活对象的比例)
    4. 再次标记(Remark):由于应用程序持续运行,因此需要修正上一次的标记结果,这一阶段是STW的,并且G1采用了比CMS更快的快照算法:SnapShot-At-The-Beginning(SATB)
    5. 独占清理(Clean-Up):STW的,计算各个区域的存活对象和GC回收比例,并进行排序,识别可以混合回收的区域,为下文做铺垫,这个阶段并不会实际上去做垃圾的收集
    6. 并发清理阶段:识别并清理完全空闲的区域
  • 混合回收(Mixed GC):标记完成后马上开始混合回收过程,G1 GC从老年代区间移动存活对象到空闲区间,这些空闲区间就成为了老年代的一部分。和年轻代不同,老年代的G1 GC不需要对整个老年代进行回收,一次只需要扫描一小部分老年代的Region即可,同时老年代的Region是和年轻代一起被回收的,具体来说:

    image-20230715200802301
    • 当越来越多的对象晋升到老年代Old Region的时候,为了避免内存被耗尽,虚拟机会触发一个混合的垃圾收集器Mixed GC既不是Old GC也不是Full GC,该算法除了能够收集整个Young Region,还会回收一部分的Old Region(正因为能够选择回收哪些Region,才能实现对垃圾回收时间的控制)

      image-20230715203408019
  • 单线程、独占式、高强度的Full GC,针对GC评估失败提供的失败保护机制,即强力回收

    • G1的初衷就是为了避免Full GC,一旦出现上述方式不能正常工作的情况,G1就会停止应用程序的执行(STW),使用单线程的内存回收算法进行垃圾回收,性能会变得非常差,应用程序的停顿时间会很长
    • 一旦发生Full GC的时候就需要进行调整,比如堆内存太小的时候,当G1在复制存活对象的时候没有空闲的内存分段可用,则会退回到Full GC,这种情况需要增大内存解决
    • 导致Full GC的原因可能有两个:
      • 清除的时候没有足够的to-space来存放晋升的对象
      • 回收阶段(Evacuation)完成之前空间耗尽
7.6 G1垃圾回收的优化建议

​ Oracle官方其实也有考虑在垃圾回收阶段(Evacuation)设计成和用户程序一起执行,但是这件事比较复杂,而且G1也只是回收一部分Region并且停顿时间是用户控制的,所以并没有迫切地去实现,而是把这个特性放到了G1之后出现的低延迟垃圾收集器中。此外还考虑到G1不是仅仅面向低延迟,停顿用户线程能够大幅度提高垃圾收集效率,为了保证吞吐量才选择了完全暂停用户线程的做法。

优化建议:

  • 应该固定年轻代大小

    • 避免使用-Xmn-XX:NewRatio等相关选项显示设置年轻代大小
    • 固定年轻代的大小会覆盖暂停时间目标(YGC比较频繁,年轻代大小固定的话,由于YGC是独占式的STW,则JVM就不能通过调整大小来达到暂停时间目标)
  • 暂停时间不要太苛刻

    • G1 GC的吞吐量目标是90%的应用程序时间,10%的垃圾回收时间
    • 暂停时间过于苛刻则表示愿意承受更多的垃圾回收开销(垃圾回收时间越短则开销越大,指的如内存大小、线程占用的开销),这些则会直接影响到吞吐量

8 7种经典垃圾回收器总结与调优建议

GC 分类 作用位置 使用算法 特点 使用场景
Serial 串行 新生代 复制算法 响应速度优先 适用于单CPU环境下的Client模式
Serial Old 串行 老年代 标记-压缩算法 响应速度优先 多CPU环境Server模式下与CMS配合使用
ParNew 并行 新生代 复制算法 响应速度优先 多CPU环境Server模式下与CMS配合使用
Parallel Scavenge 并行 新生代 复制算法 吞吐量优先 适用于后台运算、不需要太多交互的场景
Parallel Old 并行 老年代 标记-压缩算法 吞吐量优先 适用于后台运算、不需要太多交互的场景
CMS 并行 老年代 标记-清除算法 响应速度优先 适用于互联网或B/S业务
G1 并发+并行 新生代+老年代 复制算法+标记-压缩算法 响应速度优先 面向服务端应用
如何选择垃圾收集器?
  • 优先调整堆的大小让JVM自适应完成
  • 如果内存小于100M,使用串行收集器
  • 如果是单核、单机程序,并且没有停顿时间的要求,串行收集器
  • 如果是多CPU、需要高吞吐量、允许的停顿时间超过1秒,选择并行或者JVM自己选择
  • 如果是多CPU、追求低停顿时间、需要快速响应(延迟不能超过1s,互联网应用),使用并发收集器
    • 官方推荐G1,性能高,现在互联网的项目基本都是使用的G1

9 GC日志分析

9.1 常用的GC日志的参数
  • -XX:+PrintGC:输出GC日志,类似:-verbose:gc,只包括简单的GC过程堆变化

    [GC (Allocation Failure)  15353K->14078K(58880K), 0.0037351 secs]
    [GC (Allocation Failure)  29432K->29188K(58880K), 0.0035009 secs]
    [Full GC (Ergonomics)  29188K->29044K(58880K), 0.0077215 secs]
    [Full GC (Ergonomics)  44353K->43947K(58880K), 0.0065747 secs]
    
  • -XX:+PrintGCDetails:输出GC的详细日志,包括GC的过程和GC结束后的堆空间情况

  • -XX:+PrintGCTimeStamps:输出GC的时间戳(以基准时间的形式)

  • -XX:+PrintGCDateStamps:输出GC的时间戳(以日期的形式)

  • -XX:+PrintHeapAtGC:在进行GC的前后打印出堆的信息

  • -Xloggc:../logs/gc.log:日志文件的输出路径

9.2 GC中垃圾回收数据的分析

​ 日志补充说明:

  • "[GC""[Full GC"说明了这次垃圾收集的停顿类型,后者说明GC发生了Stop The World

    所有GC均有STW,这里的Full GC并不是狭义上的

  • 使用Serial收集器在新生代的名字是Default New Generation,因此显示的是"[DefNew]"

  • 使用ParNew收集器在新生代的名字是Parallel New Generation,因此显示的是"[ParNew]"

  • 使用Parallel Scavenge收集器在新生代的名字是"[PSYoungGen"

  • 老年代和新生代的道理一样,名字是收集器决定的

  • 使用G1收集器的话,会显示成"garbage-first heap"

  • Allocation Failure:表明此次引起GC的原因是在新生代没有足够的空间能够存储新数据了

  • [PSYoungGen: 17904K->2536K(17920K)] 29350K->29016K(58880K),...

    中括号内:YGC发生之前新生代的大小发生后的大小以及总的大小

    中括号外:YGC发生之前年轻代和老年代的大小回收后的大小以及总大小

  • [Times: user=0.00 sys=0.00, real=0.00 secs]

    user表示用户态回收耗时,sys内核态回收耗时,real实际耗时。由于多核的原因,时间的总和可能会超过real

image-20230716095225503

9.3 日志分析工具的使用
9.4 案例分析
public class AllocationTest {
    private static final int _1MB = 1024 * 1024;

    public static void testAllocation() {
        byte[] allocation1, allocation2, allocation3, allocation4;

        allocation1 = new byte[2 * _1MB];
        allocation2 = new byte[2 * _1MB];
        allocation3 = new byte[2 * _1MB];
        allocation4 = new byte[4 * _1MB];
    }

    public static void main(String[] args) {
		testAllocation();
    }
}

​ 首先设置虚拟机参数,堆空间为20M,新生代10M则老年代10M,新生代比例8:1:1,则Eden8M,两个幸存者区1M

-Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:+UseSerialGC

在jdk7下:

  • DefNew表示使用Serial GC的新生代的空间变化
  • total表示可用的新生代大小,可以看到整好等于eden + from
  • eden的使用的大小为4M,正好为allocation4的大小
  • 老年代tenured大小10M,使用大小6M正好为allocation1~3的大小

image-20230716102944675

分配的过程如下

  • 首先将allocation1~3放入新生代的Eden区
  • 然后为allocation4分配内存,eden区空间不足触发YGC,存活对象无法放入From区,则晋升老年代
  • YGC后空间充足,allocation4放入Eden区,正好对应上面的结果
image-20230716102834223

而在JDK8下,情况有些变化

image-20230716110521980

  • 这个第一行GC标志可不是GC,这个GC在日志是表示GC类型的,没有STW也就表明没有实际发生YGC。

  • 上面的结果表明,在JDK8中新建对象放入Eden区的时候,如果Eden空间不足,则直接进入老年代而不是进行一次YGC再判断能不能放入Eden区

    这里其实也有疑问,如果新生代空间一不足就往老年代放而不YGC那么YGC什么时候回触发呢?只在FullGC的时候频率太低不太可能

image-20230716102759573

10 新时期Epsilon和Shenandoah垃圾回收器

11 革命性的ZGC的性能介绍

12 其他厂商的垃圾回收器