JVM系列(十三) 垃圾收集器之 G1 对象分配策略

628 阅读5分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 31 天,点击查看活动详情

前面我们已经介绍了G1 Garbage-First 通用垃圾收集器 G1垃圾收集器的Region分区原理和JVM参数配置信息,今天我们讲解下G1 YoungGC的垃圾回收过程和G1的 YonugGC 日志分析

1.G1的对象分配策略

为了了解对象在什么时候溢出会发生GC,我们要了解以下G1的对象分配策略,因为只有知道了对象什么时候被分配到什么区域Eden/survior/old等,才能知道对象何时被回收

G1的年轻代由Eden region 和 Survivor region 两部分组成,新建的对象(除了大对象)都在eden region中分配内存,如果eden region已经被全部占满,该对象就无法分配空间,这时会触发一次Young gc,回收Eden region的垃圾对象,释放空间,满足当前的分配需求。

G1对象分配策略,它分为3个阶段:

  • TLAB(Thread Local Allocation Buffer)线程本地分配缓冲区
  • Eden区中分配
  • Humongous区分配

image.png

2.TLAB(Thread Local Allocation Buffer)线程本地分配缓冲区

为什么会产生TLAB,其实是为了减少空间分配时候并发冲突损耗同步时间,因为对象在一个共享的空间中分配,那么我们就需要采用同步机制来解决并发冲突问题,默认启用-XX:+UseTLAB 线程本地分配缓冲区,目的为了使对象尽可能快的分配出来

  • TLAB属于Eden区域中的内存,不一样线程的TLAB都位于Eden区,Eden区对全部的线程都是可见的
  • G1 为每个应用线程和GC线程分配了一个本地分配缓冲区TLAB
  • 分配对象内存时,就在这个 buffer 内分配,线程之间不再需要进行任何的同步,提高GC效率
  • Eden区空间中,每一个线程都有一个固定的分区用于分配对象,即一个TLAB
  • 创建小对象时,优先从TLAB中分配内存
  • 分配对象时,线程之间不再需要进行任何的同步
  • 如果线程Buffer耗尽后,就会导致分配失败,说明当前TLAB的剩余空间不满足分配需求,则调用allocate_new_tlab方法重新申请一块TLAB空间
  • 需要申请新的Buffer,申请时候难免会有并发冲突问题
2.1 指针碰撞方式

什么时指针碰撞方式分配?

  • 如果TLAB剩余空间(end - top) 大于当前对象待分配空间。则直接修改 top = top + objSize(对象大小)直接给对象分配空间
  • 若是TLAB满了,则会保留这一部分空间,从新从堆内存中划一片空间给TLAB

TLAB的慢速分配

  • 如果TLAB中的剩余空间很小(TLAB满了),说明这个空间不够对象分配,能够直接丢弃,填充一个dummy对象,重新申请一个新的TLAB来分配对象。
  • 若是TLAB剩余空间比较多,这时候就直接将对象分配到堆中
2.2 申请一个新的TLAB缓冲区

如何申请一个新的TLAB缓冲区

  • 快速无锁分配:在当前能够分配的堆空间内,经过CAS来获取一块内存
  • 如果分配成功,就能够做为TLAB的空间。
  • 该过程使用CAS,抢占式申请,有可能申请失败
  • 申请失败,则进行慢速分配。
  • 慢速分配就会尝试对Heap加锁,拓展新生代区域或垃圾回收等处理后再分配区间

3.Eden区分配

如果TLAB无法分配,那么就会采用Eden区空间,分配对象

  • 对TLAB空间中无法分配的对象,JVM会尝试在Eden空间中进行分配
  • 如果Eden空间无法容纳该对象,就只能在老年代中进行分配空间。

4.Humongous区分配

大对象的分配发生Humongous区

  • 大对象在会独占一个、或多个连续分区,区域被标记为开始巨型分配(StartsHumongous)
  • 相邻连续分区被标记为连续巨型(ContinuesHumongous)
  • 由于无法享受 TLab 带来的优化,并且确定一片连续的内存空间需要扫描整堆,因此确定巨型对象开始位置的成本非常高
  • 实际项目中 应用程序应避免生成巨型对象。

5.G1的YoungGC 触发时机

新生代的YoungGC 几乎都是差不多的,G1的Young GC也是采用标记-复制-清除算法。

  • G1的Young GC 不会等Eden区对象满了才会触发,而是需要根据实时计算判断哪块Region要回收的
  • G1会设置-XX:MaxGCPauseMillis 指定目标最大停顿时间(默认200ms)
  • 计算现在Eden区回收大概要多久时间,如果回收时间远远小于参数设定的值,那么增加年轻代的region,扩容继续存放新对象不会马上做Young GC
  • 直到下一次Eden区放满 或者G1计算回收时间接近参数 -XX:MaxGCPauseMills 设定的值,就会触发Young GC

所以 当Eden区已满,JVM分配对象到Eden区失败时,便会触发一次STW式的年轻代收集young GC,将 Eden 区存活的对象将被拷贝到 to survivor 区;from survivor 区存活的对象则根据存活次数阈值分别晋升到 TLAB、to survivor 区和老年代中;如果 survivor 空间不够,Eden区的部分数据会直接晋升到年老代空间。最终Eden空间的数据为空,GC停止工作,应用线程继续执行。

young GC 还负责维护对象的年龄(存活次数),辅助判断老化(tenuring)对象晋升时的去向。young GC 首先将晋升对象尺寸总和、年龄信息维护到年龄表中,再根据年龄表、Survivor尺寸、Survivor填充容量 -XX:TargetSurvivorRatio(默认50%)、最大任期阈值 -XX:MaxTenuringThreshold(默认15),计算出一个恰当的任期阈值,凡是超过任期阈值的对象都会被晋升到老年代。