G1 (Garbage-First) 是一款面向服务器的垃圾收集器,主要针对多颗处理器及大容量内存的机器,具备极短的 GC 停顿时间和高吞吐量的特征.
G1 堆内存划分
G1 不像 CMS 那样,老年代和年轻代不再有明显的区分。而是将内存分为很多和区域(Region),JVM 最多可以有 2048 个区域,一般一个区域的大小等于堆大小 / 2048 ,比如堆的大小是 4096,那么一块区域的大小有 2M, 也可以通过参数指定:-XX:G1HeapRegionSize, 但是推荐默认模式
G1 中的年轻代和老年代
G1 保留了年轻代和老年代的概念,但是不再有物理上的隔阂了,他们都是 Region 组成,可以不连续。
年轻代占比
默认年轻代占堆的 5%,可以通过参数 -XX:G1NewSizePercent 设置年轻代的初始占比,在系统运行中会不断的增加年轻代的 Region,但是最多年轻代的占比不会超过 60%,这个最大值可以通过参数设置: -XX:G1MaxNewSizePercent
年轻代中的 Eden 和 Survivor 的比例也是 8 :1: 1 , 一个 Region 之前是年轻代,经过垃圾回收后可能变成老年代,也就是说 Region 区域功能是动态变化的
大对象区域
G1 收集器中对象什么时候会转移到老年代,和之前 CMS, Parallel 一样,唯一不同的是,G1 设置了专门存放大对象的区域: Humongous, 而不是让大对象直接进入老年代。
在 G1 中,当对象超过一个 Region 的 50% ,就会判定为大对象,如果一个对象太大,会连续使用多个区域存放。
Humongous 专门存放短期的大对象,不用直接进入老年代,节省了老年代的空间,降低了 GC 次数。
Full GC 的时候,老年代,年轻代, Humongous 会一并清理
G1 运行过程
初始标记 (Initial Mark, STW)
暂停用户所有线程,并标记 GC Root 所有直接引用的对象,速度很快
并发标记 (Concurrent Mark)
同 CMS 的并发标记
最终标记 (Remark, STW)
同 CMS 的重新标记
筛选回收 (cleanup, STW)
不会回收所有的被标记的 Region, 先对各个 Region 回收的价值和成本进行排序,根据用户期望的 GC 停顿时间来制定回收计划。可以通过参数 -XX:MaxGCPauseMillis 可以指定 GC 停顿时间
因为 G1 通常运行在内存比较大的机器上,如果对所有被标记的空间都进行回收,势必会花费很多时间,所以仅仅回收部分区域,满足用户对 GC 停顿时间的要求。
回收算法主要使用复制算法,将一个 Region 中存活的对象复制到另一个 Region 中,不会像 CMS 那样回收完还有很多内存碎片进行整理,G1 采用复制算法,几乎不会有太多的碎片。
-
单线程回收
CMS 在回收阶段是可以和用户线程并发执行,但是 G1 内部实现太复杂,暂时没有实现并发回收,到了 Shenandoah 实现了并发收集,可以看做是 G1 的升级版本 -
选择哪些区域回收?
G1 内部维护了一个优先级列表,每次根据允许的收集时间,优先选择回收价值最大的 Region, 这也是它名字的由来 Garbage First , 比如一个 Region 回收需要花费 200ms, 能释放 10M 的空间,回收另一个 Region 需要花费 50ms, 能释放 20M 空间,G1 会优先回收后面的 Region -
示意图
G1 重要的特征
并行与并发
G1 能充分利用 CPU、多核环境下的硬件优势,使用多个 CPU(CPU 或者 CPU 核心)来缩短 StopThe-World 停顿时间。部分其他收集器原本需要停顿 Java 线程来执行 GC 动作,G1 收集器仍然可以通过并发的方式让 java 程序继续执行。
分代收集
虽然 G1 不需要与其它收集器配合使用就能独自管理整个堆,但还是保留了分代的概念。
空间整合
与 CMS 的 “标记-清理” 不同,G1 从整体上看,采用了标记-整理算法,局部使用了标记-复制算法
可预测的停顿
这是 G1 与 CMS 另外一个大优势, CMS 和 G1 都特别重视降低停顿时间,但是 G1 除了追求降低停顿时间外,还可以让用户指定停顿时间,在指定时间内完成垃圾收集: -XX:MaxGCPauseMillis
这使得 G1 在不同的场景中可以获得最佳的停顿时间和吞吐量。这个最佳值必须是合理的,不能无限低,否则每次垃圾回收的空间很小,回收的速度追赶不上垃圾生成的速度,最终也会频繁 FullGC 反而降低性能,通常把 GC 时间设置为 200~300 ms 是比较合理的
G1 垃圾收集分类
YoungGC
不是说现有的 Eden 区域放满之后会马上触发 YoungGC , G1 会计算回收现在的 Eden 区域需要花费多少时间,如果这个回收时间远远小于 MaxGCPauseMillis 值,那么就会增加年轻代的 Region, 继续存放对象,直到下一次预估的回收时间接近 MaxGCPauseMillis , 才会触发 Young GC
MixedGC
不是 Full GC
不是 Full GC
不是 Full GC
老年代占有率达到设定值的时候会触发,回收所有的 Young 和部分 Old(根据指定的 GC 停顿时间和回收优先级进行选择),一般会先触发 Mixed GC, 在 GC 过程中,把各个 Region 存活的对象复制到别的 Region 中,如果没有足够的 Region 存放对象,就会触发 Full GC
Full GC
这个过程会暂停用户程序,使用单线程进行标记,清理,压缩整理,以便空闲出一批 Region 供下一次 MixedGC 使用,这个过程非常耗时,在 Shenandoah 时优化为多线程收集了
G1 垃圾收集器参数设置
- -XX:+UseG1GC 使用 G1
- -XX:ParallelGCthreads GC 工作的线程数量
- -XX:G1HeapRegionSize 指定分区大小 (1MB ~ 32 MB), 必须是 2 的 N 次幂,默认将堆划分为 2048 个区域
- -XX:MaxGCPauseMillis 指定 GC 暂停时间,默认时 200ms
- -XX:G1NewSizePercent 新生代初始空间,默认 5%
- -XX:G1MaxNewSizePercent 新生代最大空间,
- -XX:TargetSurvivorRatio Survivor 区填充容量,默认时 50%, Survivor 区域中的一批对象(年龄 1+年龄 2+年龄 n 的多个年龄对象) 总和超过了 Survivor 区域的 50%,此时就会把年龄 n (含) 以上的对象都放入老年代
- -XX:MaxTenuringThreshold 最大年龄阀值,默认 15
- -XX:InitiatingHeapOccupancyPercent 老年代占用空间达到阀值时 (默认 45%),则执行老年代和新生代的混合收集(MixedGC), 比如我们之前说的堆默认有 2048 个 region,如果有接近 1000 个 region 都是老年代的 region,则可能就要触发 MixedGC 了
- -XX:G1MixedGCLiveThreasholdPercent region 中存活对象空间占比低于这个值(默认 85%),才会回收该 Region, 如果超过这个比例,说明这个 Region 中存活的对象过多,回收意义不大
- -XX:G1MixedGCCountTarget 指定在一次回收过程中,做几次筛选回收(默认 8 次),在最后一个筛选回收阶段,可以暂停一会再回收,不至于单次停顿时间过长
- -XX:G1HeapWastePercent 默认 5%, GC 过程中空出来的 Region 是否充足阀值,在混合回收的时候,是基于复制算法的,需要把要回收的 Region 中的存活对象复制到其他的 Region 中,然后把这个 Region 清空,这样在 GC 过程中就不断有新的 Region 空闲出来,一旦空闲的数量占堆总内存的 5%时,就会终止 MixedGC
G1 收集器优化建议
假设参数 -XX:MaxGCPauseMills 设置的值很大,导致系统运行很久,年轻代可能都占用了堆内存的 60%了,此时才触发年轻代 gc。那么存活下来的对象可能就会很多,此时就会导致 Survivor 区域放不下那么多的对象,就会进入老年代中。
或者是你年轻代 gc 过后,存活下来的对象过多,导致进入 Survivor 区域后触发了动态年龄判定规则,达到了 Survivor 区域的 50%,也会快速导致一些对象进入老年代中。
所以这里核心还是在于调节 -XX:MaxGCPauseMills 这个参数的值,在保证他的年轻代 gc 别太频繁的同时,还得考虑每次 gc 过后的存活对象有多少, 避免存活对象太多快速进入老年代,频繁触发 mixed gc
适合使用 G1 的场景
- 50% 以上的内存被存活对象使用
- 对象分配和晋升变化非常大
- 垃圾回收时间特别长,超过 1 秒
- 8G 以上的内存,经验值
- 期望停顿时间在 500ms 以内
每秒几十万并发的系统如何优化 JVM
Kafka 类似的支撑高并发消息系统大家肯定不陌生,对于 kafka 来说,每秒处理几万甚至几十万消息时很正常的,一般来说部署 kafka 需要用大内存机器 (比如 64G),也就是说可以给年轻代分配个三四十 G 的内存用来支撑高并发处理,这里就涉及到一个问题了,我们以前常说的对于 eden 区的 younggc 是很快的,这种情况下它的执行还会很快吗?很显然,不可能,因为内存太大,处理还是要花不少时间的,假设三四十 G 内存回收可能最快也要几秒钟,按 kafka 这个并发量放满三四十 G 的 eden 区可能也就一两分钟吧,那么意味着整个系统每运行一两分钟就会因为 younggc 卡顿几秒钟没法处理新消息,显然是不行的。那么对于这种情况如何优化了,我们可以使用 G1 收集器,设置-XX:MaxGCPauseMills 为 50ms,假设 50ms 能够回收三到四个 G 内存,然后 50ms 的卡顿其实完全能够接受,用户几乎无感知,那么整个系统就可以在卡顿几乎无感知的情况下一边处理业务一边收集垃圾。
G1 天生就适合这种大内存机器的 JVM 运行,可以比较完美的解决大内存垃圾回收时间过长的问题
安全点与安全区域
安全点就是指代码中一些特定的位置, 当线程运行到这些位置时它的状态是确定的, 这样 JVM 就可以安全的进行一些操作, 比如 GC 等,所以 GC 不是想什么时候做就立即触发的,是需要等待所有线程运行到安全点后才能触发。
这些特定的安全点位置主要有以下几种:
- 方法返回之前
- 调用某个方法之后
- 抛出异常的位置
- 循环的末尾
大体实现思想是当垃圾收集需要中断线程的时候,不直接对线程操作,仅仅简单地设置一个标志位,各个线程执行过程时会不停地主动去轮询这个标志,一旦发现中断标志为真时就自己在最近的安全点上主动中断挂起。轮询标志的地方和安全点是重合的。
- 安全区域又是什么?
Safe Point 是对正在执行的线程设定的。
如果一个线程处于 Sleep 或中断状态,它就不能响应 JVM 的中断请求,再运行到 Safe Point 上。因此 JVM 引入了 Safe Region。
Safe Region 是指在一段代码片段中,引用关系不会发生变化。在这个区域内的任意地方开始 GC 都是安全的