初步介绍了G1的内存模型和分配规则,包括了下面的一些知识:

  • 每个Region多大
  • 新生代包含多少Region,
  • 新生代如何动态增加Region
  • Eden和Survivor两个区域仍然还是存在
  • 什么时候触发新生代的垃圾回收
  • 垃圾回收的复制算法
  • 还有G1特有的预设GC停顿时间的作用
  • 什么时候对象进入老年代
  • 大对象的独立Region存放和回收
  • G1 是如何工作的?
  • 对象什么时候进入新生代的 Region?
  • 什么时候触发 Region GC?
  • 什么时候对象进入老年代的 Region?
  • 什么时候触发老年代的 Region GC?
  • 如果使用G1垃圾回收的时候,应该值得优化的是什么地方?
  • 什么时候可能会导致G1频繁的触发Mixed混合垃圾回收?
  • 如何尽量减少Mixed GC的频率?

1. G1垃圾回收器

  • G1 的垃圾回收性能,比“ParNew+CMS”的垃圾回收器组合更好;
  • 核心设计思想:
    • 将 Java 堆内存拆分为很多个大小相等的 Region;
    • Region 刚开始可能谁都不属于,动态按需分配,第一次可能分配给了新生代,GC 后可能下次就分配给老年代了;
    • 新生代和老年代各自对应一些 Region;
    • G1 允许设置一个垃圾回收的预期停顿时间(例如一小时 Stop the world 不超过1分钟)
    • G1 会追踪每个 Region 的回收价值,每个 Region 中可以回收的对象大小和预估时间;
    • 回收垃圾的时候,尽可能挑选停顿时间最短以及回收对象最多的 Region,尽量保证达到我们指定的垃圾回收系统停顿时间;

2. 从对象在内存中的分配到垃圾回收的触发过程分析

(1) G1 对应的内存空间是如何分配的?

  • 使用“-XX:+UseG1GC”来指定使用 G1 垃圾回收器;
  • 使用“-Xms”和“-Xmx”来给整个堆内存设置了大小后,默认情况下 G1 会自动计算和设置有多少个 Region 和每个 Region 大小是多少;
    1. - JVM 最多可以有2048 Region
    2. - Region 的大小 堆内存大小 / 2048,且 Region 的大小必须是2的倍数,比如1M2M4M之类;
  • 可以使用“-XX:G1HeapRegionSize”来手动指定 Region 大小;
  • 刚开始的时候,默认新生代对堆内存(4G)的占比是5%,2048*5%≈100个 Region,占200M左右的内存;
    • 可以使用“-XX:G1NewSizePercent”设置刚开始运行时新生代的初始占比,默认5%;
    • 可以使用“-XX:G1MaxNewSizePercent”设置新生代占比最大值,默认60%,系统运行后,JVM 会不断给新生代增加更多的 Region,最后不能超过最大占比值;
  • 可以使用“-XX:SurvivorRatio=8”划分新生代的 Eden 和 Survivor 比例,默认8:1:1,例如,新生代初始有 100 Region,那么 Eden 就是80个 Region,两个 Survivor 就是各占10个 Region;
  • 因为新生代最大占比60%,所以老年代最大空间占比40%,初始值是0个 Region,因为对象刚开始都是放在新生代;
  • G1 提供了专门的 Region 来存放大对象,而不是让大对象进入老年代的 Region 中;
    • 大对象判定规则:对象的大小超过了一个 Region 大小的50%,例如每个 Region 是2M大小,只要一个对象超过了 1M 就是大对象;
    • 如果一个对象太大,可以横跨多个 Region 来存放;
    • 虽然没有明确分配大对象 Region 的占比,但是 Region 都是按需动态分配的,会有很多空的 Region 用来存放大对象;

(2) G1 的新生代垃圾回收机制

  • 触发垃圾回收机制和之前的很类似;
  • 系统不停的向新生代的 Eden 对应的 Region 中放对象,JVM 就会不停的给新生代增加更多的 Region,直到新生代占比达到60%(Eden 1000个Region、Survivor 各 100个Region);
  • Eden 占满,触发新生代的 GC,G1 还是使用复制算法进行垃圾回收;
  • 进入 Stop the World 状态;
    • 可以使用“-XX:MaxGCPauseMills”设置 G1 执行 GC 的时候最多让系统停顿多长时间,默认200ms;
    • G1 会对每个 Region 追踪回收他需要多少时间,可以回收多少对象来选择一部分的 Region,保证 GC 停顿时间控制在指定范围内,尽可能多的回收掉一些对象;
  • 然后把 Eden 对应的 Region 中的存活对象放入 S1 对应的 Region 中;
  • 接着回收掉 Eden 对应的 Region 中的垃圾对象;

(3) 对象什么时候进入老年代?

  • 对象进入老年代的时机,几乎和之前是一样的;
    • 使用“-XX:MaxTenuringThreshold”设置对象躲过几次垃圾回收进入老年代,默认15;
    • 动态年龄判定规则,如果发现某次 GC 后,存活对象超过了 Survivor 的 50%,此时判断一下,比如年龄1…年龄4的对象大小总和超过了 Survivor 的50%,此时4岁以上的对象全部进入老年代;

(4) 大对象 Region 的垃圾回收

  • 大对象 Region 既不属于新生代,也不属于老年代;
  • 新生代、老年代在回收的时候,会顺带对大对象 Region 一起回收;

(5) G1 的新生代+老年代的混合垃圾回收机制

image.png

  • 触发时机:
    • 使用“-XX:InitiatingHeapOccupancyPercent”设置触发混合回收的触发时机,默认45%,即老年代占据堆内存的45%的 Region 的时候,堆内存2048个 Region,老年代接近1000个 Region 的时候,就会触发一个混合回收;
  • 回收过程:
    • 初始标记:进入 Stop the World 状态停止系统工作线程,然后对各个线程栈内存中的方法局部变量、以及方法区中的类静态变量所代表的 GC Roots,进行扫描,标记出来他们直接引用的对象作为存活对象;
      • 速度很快,只扫描 GC Roots 的直接关联对象;
    • 并发标记:系统程序继续运行,从 GC Roots 变量直接关联的对象开始追踪,标记出所有间接引用的对象作为存活对象;
      • 速度慢,要扫描所有的存活对象,但是对系统程序影响不大;
      • 系统继续运行,会对一些对象做出修改,JVM 会对这些对象的修改记录下来,如那个对象被新建、哪个对象失去了引用;
    • 最终标记:进入 Stop the World 状态停止系统工作线程,根据并发标记阶段的做的修改记录,最终标记一下有哪些存活对象,有哪些是垃圾对象;
    • 混合回收
      • 会对新生代、老年代、大对象进行垃圾回收;
      • 首先,计算新生代、老年代、大对象中每个 Region 的存活对象数量,存活对象的占比,还有执行垃圾回收的预期性能和效率;
      • 此阶段的回收,是通过反复多次的执行混合回收完成,尽可能让系统不要停顿太长时间,可以在多次回收的间隙,也运行一下;
        • 使用“-XX:G1MixedGCCountTarget”设置此阶段执行几次混合回收,默认8次;
        • 进入 Stop the World 状态,G1 会从新生代、老年代、大对象里各自挑选一些 Region 进行回收,在指定的停顿时间内尽可能回收更多的垃圾对象;
        • 使用“-XX:G1MixedGCLiveThresholdPercent”,默认值85%,即确定要回收的 Region 的时候,必须存活对象低于85%的 Region 才可以进行回收;
        • 混合回收的时候,基于复制算法,将要回收的 Region 里的存活对象放入空的 Region,清空要回收的 Region;
        • 使用“-XX:G1HeapWastePercent”默认值5%,即清理空闲出来的 Region 数量达到堆内存的5%时,就会立即停止本次混合回收;
  • 回收失败时 Full GC
    • 混合回收基于复制算法,如果在将各个 Region 的存活对象拷贝到空 Region 的过程中,发现没有空闲的 Region,就会触发一次失败;
    • 一旦失败,就会停止系统工作线程,采用单线程进行标记、清理和压缩整理,空闲出一批 Region;
    • 这个过程是极慢的,对系统性能影响很大;