G1收集器(-XX:+UseG1GC)

G1收集器的内存划分

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

以极高概率满足GC停顿时间的要求同时,还具备高吞吐量性能的特征。

G1将Java的堆划分为多个大小相等的独立区域,它不再是传统的一部分Eden、一部分Survivor、一部分Old。

(传统的堆内存划分)
gc_g1_1

(G1的堆内存划分)
gc_g1_2

如上图所示,G1将Java的堆划分为多个大小相等的独立区域,称为"Region",JVM最多可以有2048个Region。

一般Region的大小等于堆的大小除以2048,比如堆大小为4096,则Region的大小为2M,当然也可以用使用参数"-XX:G1HeapRegionSize"手动指定Region的大小,但是推荐默认的计算方式。

G1保留了年轻代和老年代的概念,但不再是物理隔阂了,它们都是可以不连续的Region集合。

默认年轻代对堆内存的占比是5%,如果堆大小为4096M,那么年轻代占据200M左右的内存,对应大概是100个Region,可以通过"-XX:G1NewSizePercent" 设置新生代初始占比,在系统运行中,JVM会不停的给年轻代增加更多的Region,但是最多新生代的占比不会超过60%,可以通过"-XX:G1MaxNewSizePercent"调整。

年轻代中的Eden和Survivor对应的Region也跟传统的占比一样,默认是8:1:1,假设年轻代现在有1000个Region,那么Eden区对应800个Region,S0对应100个Region,S1对应100个Region。

Region区域的功能可能会变化的,假设一个Region之前是年轻代,经过垃圾回收清空该Region区域,后面该Region存入了老年代的内存,那么该Region会变为老年代。

G1垃圾收集器对于对象转移到老年代的规则和传统的其他GC规则一样,不同的是对大对象的处理,G1有专门分配大对象的Region叫Humongous区,而不是让大对象直接进入老年代的Region中。

在G1中,大对象的判断规则就是一个对象超过了Region大小的50%,比如按照4096M的堆内存来算,每个Region容量为2M,只要一个对象的内存超过了1M,那么它就会被判定为大对象,并且放入Humongous中,而且一个大对象如果太大,一个Region存放不下,那么会横跨多个Region来一起存放,

Humongous区专门存放短期的大对象,不用直接进如老年代,可以节约老年代的空间,避免因为老年代空间不够的GC开销。

Full GC的时候除了收集年轻代和老年代之外,也会将Humongous区一并回收。

G1收集器GC的运行过程

gc_g1_3

  • 初始标记:( initial mark , STW )暂停所有的应用程序的工作线程,并记录下GC Roots直接能引用的对象,速度很快;
  • 并发标记:(Concurrent Marking)同CMS的并发标记
  • 最终标记:(Remark , SWT)同CMS的重新标记
  • 筛选回收:(Cleanup, STW)筛选回收阶段首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来指定回收计划。
    • 可以用JVM参数 -XX:MaxGCPauseMillis来指定最大GC停顿时间。
回收算法

回收算法主要用到的是复制算法,将一个Region中存活的对象复制到另一个空的Region,存活的对象复制出去后剩下的就是垃圾对象然后再把整个Region清空掉,这种不会像CMS那样回收完因为有很多内存碎片还需要整理一次,G1采用复制算法回收几乎不会有太多的内存碎片。

回收的时间耗时,是根据要回收的Region中的存活对象数量有密切关系,假设复制一个对象要1ms,要回收的Region里面有10个存活对象,那么就需要10ms的时间进行复制到其他空的Region。

筛选回收

G1收集器在后台维护了一个优先列表,比如回收Region-1需要100毫秒,能释放10M内存,而回收Region-2需要50毫秒,能释放20M内存。那么必然Region-2是优先回收的。

假设通过JVM参数( -XX:MaxGCPauseMillis)指定了最大GC停顿时间为200ms,那么G1会根据回收成本计算,回收这200ms以内能回收价值最大的一部分Region。

比如在GC过程中,发现有1000个Region都是需要回收的,都有垃圾对象,假设全部进行回收需要400ms,但是参数指定了最大是200ms, 那么这时候就会优先收集回收价值最大的Region(这也是它的名字,Garbage-First的由来),当时间到了200ms后,那么剩余的region则不会回收。

这种使用Region划分内存空间以及有优先级的区域回收方式,保证了G1收集器在优先时间内尽可能高的收集效率。

G1具备的特点
  • 并行与并发:G1能充分利用CPU、多核环境下的硬件优势,使用duogeCPU(CPU或者CPU核心)来缩短Stop-The-World的停顿时间。部分其他收集器原本需要停顿Java线程来执行GC动作,G1收集器仍然可以通过并发的方式让Java程序继续执行。
  • 分代收集:虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但是还是保留了分代的概念。
  • 空间整合:与CMS的"标记-清理"算法不同。
    • G1从整体来看是基于"标记整理"算法实现的收集器;
    • 从局部上来看是基于"复制"算法实现的。
  • 可预测的停顿:这是G1相对于CMS的另一个大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为N毫秒的时间片段内完成垃圾收集。

需要注意的是,虽然G1提供可预测的停顿时间参数,但是这里设置的"期望值"必须是符合实际的,不能异想天开,毕竟G1是要暂停应用程序工作线程来复制对象的,这个停顿时间再怎么低也得有个限度,它默认的停顿时间是200ms,一般来说回收阶段占到几十到接近两百毫秒都很正常,但如果我们把停顿时间调的非常低,比如设置为20ms,很可能出现的结果就是,停顿目标时间太短,导致每次选出来的回收结果集只占堆内存的很小一部分,很可能内存回收的速度会逐渐跟不上产生新对象的速度,应用运行时间一长的话,容易造成堆满,引发Full GC反而降低性能,所以通常把期望停顿时间设置为一两百毫秒或者两三百毫秒都是比较合理的。

G1垃圾收集分类

YoungGC

YoungGC并不是说现有的Eden区放满了就会马上触发,G1会计算下现在Eden区回收大概要多久时间,如果回收时间远远小于参数 -XX:MaxGCPauseMills 设定的值,那么会增加年轻代的Region,继续给新对象存放,不会马上做YoungGC,直到下一次Eden区放慢,G1计算回收时间接近参数 -XX:MaxGCPauseMill设定的值,那么就会触发YoungGC。

MixedGC

不是FullGC,老年代的堆占有率达到参数(-XX:InitiatingHeapOccupancyPercent设定的值则触发,回收所有Young和部分Old(根据期望的GC停顿时间确定Old区垃圾收集的优先顺序)以及大对象区,正常情况G1的垃圾收集是先做MixedGC,主要是使用复制算法,需要将各个Region中存活的对象拷贝到其他空的Region里去,拷贝过程中如果发现没有足够的空Region能够承载拷贝对象就会触发一次Full GC。

Full GC

停止应用程序工作线程(STW),然后采用单线程进行标记、清理和压缩整理,好空闲出来一批Region来供下一次MixedGC使用,这个过程是非常耗时的。(有点像CMS当老年代满了后采用单线程的垃圾收集器)

G1收集器参数设置
  • -XX:+UseG1GC:使用G1收集器
  • -XX:ParallelGCThreads:指定GC工作的线程数量
  • -XX:G1HeapRegionSize:指定Region区域大小(1M~32M 必须是2的N次幂等),默认将整堆划分为2048个分区
  • -XX:MaxGCPauseMillis:目标暂停时间(默认200ms)
  • -XX:G1NewSizePercent:新生代内存初始空间(默认整堆的5%)
  • -XX:G1MaxNewSizePercent:新生代内存最大空间
  • -XX:TargetSurvivorRatio:对象动态年龄判断,默认是(50%)
    • 举个例子,年龄为1的对象占用%33,年龄为2的对象占用%33,它们总和为66%,所以会把年龄大于等于2的对象全部挪动到老年代。
  • -XX:MaxTenuringThreshold:最大年龄阈值(默认15)
  • -XX:InitiatiingHeapOccupancyPercent:老年代占用空间达到整堆内存阈值(默认45%),则执行新生代和老年代的混合收集(MixedGC)。
    • 比如我们之前说的堆默认有2048个Region,如果又接近1000个Region都是老年代的Region,则可能就要触发MixedGC了。
  • -XX:G1MixedGCLiveThresholdPercent(默认85%)Region中的存活对象低于这个值时才会回收该Region,如果超过这个值,存活对象过多,回收的意义不大。
  • -XX:G1MixedGCCountTarget:在一次回收过程中指定做几次筛选回收(默认8次),在筛选回收阶段可以回收一会,然后暂停回收,恢复应用程序线程,一会再暂停应用程序,开始回收.... 如此循环,可以避免单次停顿应用程序线程时间过长。
  • -XX:G1HeapWastePercent:(默认5%)gc过程中空出来的Region是否充值阈值,在混合回收的时候,对Region回收都是基于复制算法进行的,都是把要回收的Region里的存活对象放入其他Region,然后这个Region中的垃圾对象全部清理掉,这样的话在回收过程就会不断空出来新的Region,一旦空闲出来的Region数量达到了总Region数量的5%,此时就会立即停止混合回收,意味着本次混合回收就结束了。
G1垃圾收集器优化建议

假设参数-XX:MaxGCPauseMillis 设置的值很大,导致系统运行很久,年轻代可能都占用了堆内存的60%了,此时才触发年轻代GC(YoungGC)。

那么存活下来的对象可能就会很多,此时就会导致Survivor区域放不下那么多的对象,就会进入老年代中。

或者是你年轻代GC过后,存活下来的对象过多,导致进入Survivor区域后触发了动态年龄判断机制,达到了Survivor区域的50%,也会快速导致一些对象进入老年代中。

所以这里核心还是在于调节-XX:MaxGCPauseMillis 这个参数的值,在保证他的年轻代GC别太频繁的同时,还得考虑每次GC过后的存活对象有多少,避免存活对象太多快速进入老年代,频繁触发Mixed GC。

什么场景适合使用G1
  • 50%以上的堆被存活对象占用
  • 对象分配和晋升的速度变化非常大
  • 垃圾回收时间特别长,超过1秒
  • 8G以上的堆内存
  • 期望停顿时间是500ms以内

如何选择垃圾收集器

1、优先调整堆的大小让服务器自己来选择

2、如果内存小于100M,使用串行收集器

3、如果是单核,并且没有停顿时间的要求,串行或者JVM自己选择

4、如果运行停顿时间超过1秒,选择并行或者JVM自己选

5、如果响应时间最重要,并且不能超过1秒,使用并发收集器

6、4G以下可以用Parallel,4-8G可以用ParNew+CMS,8G以上可以用G1,几百G以上可以用ZGC

安全点与安全区域

安全点

安全点就是指代码中一些特定的位置,当线程运行到这些位置时,它的状态是确定的,这样JVM就可以安全的进行一些操作,比如GC等。

所以GC不是想什么时候做就立即出发的,是需要等待所有线程运行到安全点后才能触发。

特定的安全点位置主要有以下几种:

  • 方法返回之前
  • 调用某个方法之后
  • 抛出异常的位置
  • 循环的末尾

大体实现思想是当垃圾收集器需要STW的时候,不直接对线程操作,仅仅简单的设置一个标志位,当各个线程执行到安全点的时候就会去检查这个标志位,一旦发现了这个标志位就会在安全点这里挂起线程。

主要目的是为了防止破坏部分代码的原子性,比如 i++ 这种原子性的指令。

安全区域

安全点是对正在执行的线程设定的。

如果一个线程处于Sleep或中断状态,它就不能响应到JVM的中断请求,再运行到安全点上。

因此JVM引入了Safe Region(安全区域)。

安全区域是指在一段代码片段中,引用关系不会发生变化。在这个区域的任意地方开始GC都是安全的。