G1垃圾收集器!

G1(Garbage First)是一款面向服务器的垃圾收集器,主要针对配置多核处理器以及大容量内存的机器

  • 满足GC停顿时间要求的同时,还具备高吞吐量性能特征

JDK9 开始默认使用 G1 垃圾收集器

内存划分

G1将堆划分为多个大小相等的独立的Region区域

一般Region区的大小等于堆空间的总大小除以2048

  • 比如目前的堆空间总大小为8GB,就是8192MB/2048=4MB,那么最终每个Region区的大小为4MB

  • 也可以用参数-XX:G1HeapRegionSize强制指定每个Region区的大小

新生代和老年代

默认新生代对堆内存的初始占比是5%

  • 如果堆大小为8GB,那么年轻代占据400MB左右的内存,对应大概是100个Region
    • 可以通过-XX:G1NewSizePercent设置新生代初始占比

在Java程序运行中,JVM会不停的给新生代增加更多的Region

  • 但是最多新生代的占比不会超过堆空间总大小的60%,可以通过-XX:G1MaxNewSizePercent调整

新生代中的Eden区和Survivor区对应的Region区比例也跟之前一样,默认8:1:1

  • 假设新生代现在有400个Region,那么整个新生代的占比则为Eden=320,S0/From=40,S1/To=40

G1中的年老代晋升条件和之前的无差,达到年龄阈值的对象会被转入年老代的Region区中。

不同的是对于大对象的分配,在G1中不会让大对象进入年老代:

  • 在G1中由专门存放大对象的Region区叫做Humongous

  • 如果在分配对象时,判定出一个对象属于大对象,那么则会直接将其放入Humongous区存储

在G1中,判定一个对象是否为大对象的方式为:

对象大小是否超过单个普通Region区的50%:

  • 如果超过则代表当前对象为大对象,那么该对象会被直接放入Humongous

  • 比如:目前是8GB的堆空间,每个Region区的大小为4MB,当一个对象大小超过2MB时则会被判定为属于大对象

如果程序运行过程中出现一个巨型对象,当一个Humongous区存不下时,可能会横跨多个Region区存储它

Humongous区存在的意义:

可以避免一些短命的巨型对象直接进入年老代,节约年老代的内存空间,可以有效避免年老代因空间不足时的GC开销

  • 当堆空间发生全局GC(FullGC)时,除开回收新生代和年老代之外,也会对Humongous区进行回收

什么场景下适合采用G1收集器的建议

堆空间内50%以上的内存会被存活占用的应用

分配速度和晋升速度特别快的应用

至少8GB以上堆内存的应用

采用原本分代收集器GC时间会长达1s+的应用

追求停顿时间在500ms以内的应用

GC类型

YoungGC:

在G1中,当新生代区域被用完时,G1首先会大概计算一下回收当前的新生代空间需要花费多少时间

  • 如果回收时间远远小于参数-XX:MaxGCPauseMills设定的值,那么不会触发YoungGC

    • 而是会继续为新生代增加新的Region区用于存放新分配的对象实例
  • 用户未显式通过-XX:MaxGCPauseMills参数设定GC预期回收停顿时间值,那么G1默认为200ms

直至某次Eden区空间再次被放满并经过计算后:

  • 此次回收的耗时接近-XX:MaxGCPauseMills参数设定的值,那么才会触发YoungGC

  • YoungGC被触发时:

    • 首先会将目标Region区中的存活对象移动至幸存区空间(Survivor-From区标志的Region

同时达到晋升年龄标准的对象也会被移入至年老代Region中存储

  • G1收集器在发生YoungGC时,复制移动对象时是采用的多线程并行复制,以此来换取更优异的GC性能

MixedGC:

当整个堆中年老代的区域占有率达到参数-XX:InitiatingHeapOccupancyPercent设定的值后触发MixedGC

发生该类型GC后:

  • 会回收所有新生代Region

  • 部分年老代Region区:会根据期望的GC停顿时间选择合适的年老代Region区优先回收

  • 以及大对象Humongous

正常情况下,G1垃圾收集时会先发生MixedGC,主要采用复制算法

  • 在GC时先将要回收的Region区中存活的对象拷贝至别的Region区内
    • 拷贝过程中,如果发现没有足够多的空闲Region区承载拷贝对象,此时就会触发一次Full GC

FullGC:

当整个堆空间中的空闲Region不足以支撑拷贝对象或由于元数据空间满了等原因触发

  • 在发生FullGC时,G1首先会停止系统所有用户线程,然后采用单线程进行标记、清理和压缩整理内存
    • 以便于清理出足够多的空闲Region来供下一次MixedGC使用

但该过程是单线程串行收集的,因此这个过程非常耗时(ShenandoahGC中采用了多线程并行收集)

  • G1收集器中并没有FullGC,G1中的FullGC是采Sserial old FullGC

GC过程

image-20231026171802543

G1收集器在发生GC时执行过程大致会分为四个步骤(主要指MixedGC):

初始标记(InitialMark):

  • 先触发STW,然后使用单条GC线程快速标记GCRoots直连的对象

并发标记(ConcurrentMarking):

  • 与CMS的并发标记过程一致,采用多条GC线程与用户线程共同执行,根据Root根节点标记所有对象

最终标记(Remark):

  • 同CMS的重新标记阶段(也会STW),主要是为了纠正并发标记阶段因用户操作导致的错标、误标、漏标对象

筛选回收(Cleanup):

  • 先对各个Region区的回收价值和成本进行排序,找出 回收价值最大 的Region优先回收
  • 筛选回收阶段在G1收集器中是会停止所有用户线程后,采用多线程并行回收的

假设此时年老代空间共有800Region区,并且都满了,所以此刻会触发GC

但根据GC的预期停顿时间值,本次GC只能允许停顿200ms,而G1经过前面的成本计算后,大致可推断出:

  • 本次GC回收600Region区恰好停顿时间可控制在200ms左右
  • 那么最终就会以 回收600Region区 为基准触发GC
    • 这样则能尽量确保GC导致的停顿时间可以被控制在我们指定的范围之内

G1会在后台维护着一个优先列表

  • CollectionSet(CSet),它记录了GC要收集的Region集合,集合里的Region可以是任意年代的

在G1中回收算法都是采用复制算法,都会将一个Region区中存活的对象复制到另外一个Region区内

G1缺点:

G1需要记忆集 (卡表) 来记录新生代和老年代之间的引用关系

  • 这种数据结构在 G1 中需要占用大量的内存,可能达到整个堆内存容量的 20% 甚至更多
  • 而且 G1 中维护记忆集的成本较高,带来了更高的执行负载,影响效率

CMS 在小内存应用上的表现要优于 G1,而大内存应用上 G1 更有优势,大小内存的界限是6GB到8GB

G1 垃圾收集器支持在某些情况下让应用线程参与后台 GC 操作?有什么优势?

G1 垃圾收集器支持在某些情况下让应用线程参与后台 GC 操作

这是 G1 的一个特殊特性,用来提升垃圾回收效率并缩短暂停时间。

触发条件:

当 G1 的 GC 线程无法在目标时间内完成垃圾回收任务(如 -XX:MaxGCPauseMillis 指定的时间)时。

  • G1 会调度部分应用线程参与垃圾回收操作。

这些应用线程会在后台以异步方式执行部分 GC 工作,而不是完全暂停应用。

实现机制:

应用线程被调度来执行某些非关键路径的垃圾回收任务,如遍历堆对象、处理引用关系等。

应用线程的参与被限制在一定范围内,以确保不会对正常的应用逻辑造成太大干扰。