此文是笔者对于 GC 系列调研的收尾。本次工作全面总结了 PS、G1 等常见 GC 的工作原理、可调参数,解析了 GC 日志读法、可观测手段,还从延迟、吞吐两个角度分别总结了调优方法论。内容覆盖从 GC 基本知识、观测手段、日志解析到调优方法,完成了 GC 诊断、定位、调优的全链路调研解析。
文章大量参考互联网资料,感谢!
基础知识1:对于 GC,无论其具体实现和策略,都有标记-清除、复制、标记-整理等基本算法,只是不同的 GC 使用不同的基本算法。此处不再赘述,详细可以参考往期博客
基础知识2:对于大部分 GC,都会对堆内存进行分代划分,可以参考往期博客。对于更详细的 java 内存划分概念,同样可以参考往期博客
基础知识3:对于 GC,在 footprint(内存占用)、throughout(吞吐)、latency(延迟)三者不可能同时满足。最多三者满足其二。(类似 CAP 定理)
吞吐量跟低延时是无法兼得的,低延时意味着GC工作会更加频繁,相对的,会占用应用的资源,吞吐量降低。需要大吞吐量,那么GC工作就会减少,相对的,每次回收的垃圾就会多,暂停时间就会增加,延时就会增加。
我们经常遇到的情况是高吞吐的 GC 单次 stw 时间较长;而 stw 优秀的 GC 往往 GC 频率高、占用线程资源多,导致吞吐低。因此我们调优需要有的放矢,一般是针对吞吐和延迟分别调优,而不是同时追求吞吐和延迟。
在 jdk 7 才推出,9 中正式作为默认 GC ,注重停顿时间。
本文主要从 oracle 官网文档和 G1 的论文进行宏观上的调研。
【摘自官网】
G1 垃圾收集器是一个服务端的垃圾收集器,它针对的是拥有较大内存的多处理器的机器。
G1 垃圾收集器是在 Oracle 的 JDK7 update 4 及以后的版本得到了完全的支持。G1 垃圾收集器是针对这样的应用来进行设计的:
Oracle 官方说明:如果应用程序具有以下一个或多个特征,则当前使用 CMS 或 PS 运行的应用程序将有利于切换到 G1:
【笔者经验总结】
也就是说,G1 是一款相对通用,在 footprint(内存占用)、throughout(吞吐)、latency(延迟)三者达成一个较好平衡的回收器,相对更侧重 latency(延迟,或者我们常说的暂停时间)的优化。相比于 PS,G1 的 FGC 将显著减少。
因此 G1 不经过调优,实测在 IoTDB 的某些场景下,吞吐等指标不如 PS 等侧重优化吞吐的 GC
补充:CMS 被废弃的原因有部分原因是源于 G1 更可控、更易于维护、在大多数场景下有更好的表现。
Oracle 官网专门提到:G1 的吞吐量目标是 90% 应用 + 10% GC,而 PS 是 99% 应用 + 1% GC。因此 G1 的吞吐肯定是不如 PS 的,但是 G1 的 stw 表现比 PS 更优秀。
一般的 GC (像 serial、PS、CMS)采用分代内存模型,如下图:
image-20240401161844376
G1 在内存空间的划分上:堆被划分了一组相等大小堆区域(region),区域之间可能是离散的,但每一个区域都是连续虚拟内存的地址空间, 这就提供了内存使用当中最大的灵活性。区域大小由-XX:G1HeapRegionSize决定。一般来说,JVM 大约划分 2000 个 regions,它们的大小范围为 [1, 32] MB
但是在概念上仍采取分代内存的思想,即这些 regions 会分别映射到 eden、survivor、old。
image-20240401161859543
此外,还有一种上图没标出的 region,称为巨型区域(huge region)。 这些区域用于容纳尺寸为标准区域 50% 或更大的对象,它们存储为一组连续的区域。
在 YGC(Young GC) 中,live objects 被搬运(evacuated,即复制或移动)到一个或多个 survivor 区域。 如果满足老化阈值,则某些对象将被提升到老一代区域。YGC 将 stw 。
-XX:MaxGCPauseMillis)之类的事情都会被考虑在内。这种方法可以根据需要放大或缩小 Eden 和 Survivor 的大小。image-20240401161917881
在 YGC 的末尾,live objects 已被搬运到幸存者区域或晋升到老年代区域。
image-20240401161929260
综上所述:
G1 收集器在堆的老年代执行以下阶段。
| 阶段 | 描述 |
|---|---|
| (1) Initial Mark (Stop the World Event) | 这是一个 stw 事件。 对于 G1,它搭载在正常的 YGC 上。 标记可能引用老年代区域对象的 survivor 区域(根区域)。 |
| (2) Root Region Scanning | 扫描 survivor 区域以获取对老年代的引用。当应用程序继续运行时会发生这种情况。该阶段必须在 YGC 之前完成。 |
| (3) Concurrent Marking | 在整个堆上查找 live objects。 这发生在应用程序运行时。此阶段可能会被 YGC 中断。 |
| (4) Remark (Stop the World Event) | 完成堆中存 live objects 的标记。使用称为开始快照 (SATB) 的算法,该算法比 CMS 收集器中使用的算法快得多。 |
| (5) Cleanup (Stop the World Event and Concurrent) | (stw)对 live objects 和完全空闲的 region 执行 accounting(这个词不知道咋翻译)(stw)刷洗 remembered sets。 (concurrent)重置空 region 并将它们返回到 freelist。 |
| (*) Copying (Stop the World Event) | 该阶段将 stw 以搬运或将活动对象复制到新的未使用 regions。 这可以通过日志记录为[GC pause (young)]的年轻代区域,或者记录为[GC Pause (mixed)]的年轻代和老年代区域来完成。 |
接下来看看这些 phase 是如何和 G1 的老年代交互的
活动对象的初始标记是在 YGC 的基础上进行的。 在日志中,这被标记为 GC pause (young)(inital-mark).
image-20240401162004224
如果发现空区域(如"X"所示),则在 marking 阶段立即将其删除。 此外,还计算确定 liveness 的"accounting"信息。
image-20240401162019150
空区域被删除并回收。 所有区域的 liveness 将被计算
image-20240401162032792
G1 选择"liveness"最低的区域,即可以最快收集的区域。 然后这些区域会与 YGC 同时收集。 这在日志中表示为 [GC pause (mixed)]。 所以年轻代和老年代都会同时被收集。
image-20240401162043900
所选区域已被收集并压缩为图中所示的深蓝色区域和深绿色区域。
image-20240401162055015
综上所述,关于老年代的 G1 垃圾回收:
JDK 8 的默认 GC,又叫 throughout GC,注重吞吐
中英文互联网都没搜到特别详实的、原理层次的解析。因此这里主要参考国内 system 领域著名实验室 iPads 的论文中关于 PS 原理的论述。感兴趣的读者也可以直接阅读论文原文:
对于 YGC,PS 基于标记-复制算法(copy-based),论文没有做详细的描述。
大多数对象一开始分配在 eden 区(一些大对象是例外,会直接分配到 old 区)。当 YGC 后,一些对象被移动到 survivor 区,同时一些其他年老的对象被晋升到老年代。
对于 FGC,PS 基于标记-整理算法(compaction-based)
PS 的 FGC 实现分为三个阶段,包括 marking phase, summary phase, 和 compacting phase。 这三个阶段都是 region-based: PS 将 JVM 堆划分为相等大小的连续 regions(默认大小为 512KB),并将相同 region 内的 live objects 一同整理(compact)
class Region {
// Fields in vanilla PSGC
int dcount;
int live_obj_size;
char* dest_addr;
Region*[] src_regions;
...
}
去 openjdk 源码求证了一下,论文给出的源码应该是简化过的,并不是完整、标准的源码。
- 实际对比后,论文的
class Region应该对应:openjdk - RegionData
PS 的 FGC 可以划分为以下三阶段:
live_obj_size 中。live_obj_size 计算出一个 heap summary。src_regions 中。dest_addr,GC 线程可以用它来得到 region 内 live objects的目标地址。该 process order 通过维护一个名为 dcount 或者下图所示 destination_count 的变量来保持。该变量记录了该 region 作为 source,有多少 destination regions。Summary phase 会计算该变量的值。
image-20240401162140442
在 Compacting phase,一旦某个 GC 线程 fills up 了一个 destination region,它将相应 update 该 destination region 关联的所有 source region 的 dcount(减1)。当某个 region 的 dcount 减为 0 时,意味着它作为 source region 的职责结束,可以 serve as a destination region 了。该 region 会被 push 到 GC 线程的 working stack 中,并准备好接受其他 source regions 中的 live objects。
image-20240401162113697
论文提供了 Figure 2 来帮助理解,下面详细解说:
dcount 是 0(因为它们不需要将自己的 live objects 拷贝出去)。而其他 region 都有相应的 destination regionsdcount 为 0 的 regions)会被分配到 GC thread 0、1 的 working stack 中(如 Figure 2.a 所示)受GC影响请求占比=(接口响应时间+GC时间)×ばつN/T
举例:假设单位时间T内发生一次持续25ms的GC,接口平均响应时间为50ms,且请求均匀到达,根据下图所示:
image-20240401162150663
那么有 (50ms+25ms)/T 比例的请求会受GC影响,其中GC前的 50ms 内到达的请求都会增加 25ms,GC期间的25ms 内到达的请求,会增加 0-25ms 不等,如果时间 T 内发生 N 次GC,受GC影响请求占比=(接口响应时间+GC时间)×ばつN/T 。可见无论降低单次 GC 时间还是降低 GC 次数 N 都可以有效减少 GC 对响应时间的影响。
以下参数均来自 oracle JDK 8、11、17 官方文档 GC VM options 板块,本文档实现了对 oracle 官网的全覆盖。
很多参数背后都有 trade-off。所以 JDK 团队已经为我们选取了最合适的 default 值,大多数情况不需要修改。
-XX:InitiatingHeapOccupancyPercent,如果该值过小,比如 Mixed GC 周期结束后老年代使用率还是超过 IHOP,那么会再次触发全局并发标记过程,这样就会导致频繁的老年代 GC,影响应用吞吐量。如果此值太高,会导致老年代回收不及时(容易导致年轻代晋升失败而触发 Full GC) + 老年代一次回收的任务量较重(造成尖刺)| JVM 选项 | 类别 (控制哪方面) | 描述 | 调优建议 | 支持的 LTS version |
|---|---|---|---|---|
| -XX:+UseG1GC | - | 指定使用 G1 GC | 无 | 8、11、17 |
| -XX:MaxGCPauseMillis(越低延迟越低) | 自适应 | 指定最大的 GC pause time。指定的 target 是一个软目标(soft goal),JVM 会用一些机制尽最大努力来达到它。对于 G1,默认值为 200(0.2 秒) | 这是一个软性目标,G1 会尽量达成,如果达不成,会逐渐做自我调整。该参数是停顿时间和吞吐量的取舍对于 Young GC 来说,会逐渐减少 Eden 区个数,减少 Eden 空间那么 Young GC 的处理时间就会相应减少;对于 Mixed GC,G1 会调整每次 Choose Cset 的比例,默认最大值是 10%,当然每次选择的 Cset 少了,所要经历的 Mixed GC 的次数会相应增加。同时减少 Eden 的总空间时,就会更加频繁的触发 Young GC,也就是会加快 Mixed GC 的执行频率,因为 Mixed GC 是由Young GC 触发的,或者说借机同时执行的。频繁 GC 会对应用的吞吐量造成影响,每次 Mixed GC 回收时间太短,回收的垃圾量太少,可能最后 GC 的垃圾清理速度赶不上应用产生的速度,那么可能会造成串行的 Full GC,这是要极力避免的。所以暂停时间肯定不是设置的越小越好,当然也不能设置的偏大,转而指望 G1 自己会尽快的处理,这样可能会导致一次全部并发标记后触发的 Mixed GC 次数变少,但每次的时间变长, STW 时间变长,对应用的影响更加明显。该参数与 JVM GC 的自适应调优(GC ergonomics)相关 | 8、11、17 |
| -XX:GCTimeRatio(越高吞吐越高) | 自适应 | 参数的值应当是一个大于 0 且小于 100 的整数,也就是垃圾收集时间占总时间的比率,相当于是吞吐量的倒数。如果把此参数设置为 19,那允许的最大 GC 时间就占总时间的 5%(即 1 /(1+19))G1 的默认值为 12,就是允许最大大约 8%( 1 /(1+12))的垃圾收集时间。 | 根据需求妥善设置,如果该值设置的过高,那么可能会导致 heap 一直在 minimum 的 size。该参数与 JVM GC 的自适应调优(GC ergonomics)相关 | |
| -XX:NewRatio | 内存 | young/old generation 大小的比例。默认值是 2 | G1 不建议手动设置,原因参考 oracle 官网文章 | 8、11、17 |
| -XX:SurvivorRatio | 内存 | eden/survivor 区大小的比例,默认值是 8 | G1 不建议手动设置,原因参考 oracle 官网文章 | 8、11、17 |
| -XX:G1ReservePercent | 内存 | G1 会将堆中的某一部分空间预先保留作为虚天花板(false ceiling)来减少晋升失败的情况(晋升失败会导致 FGC),默认值是 10补注:比如默认情况下,老年代会预留 10% 的空间来给新生代的对象晋升,如果经常发生新生代晋升失败而导致 FGC,那么可以适当调高此阈值。但是调高此值同时也意味着降低了老年代的实际可用空间 | 如果经常发生新生代晋升失败而导致Full GC,那么可以适当调高此阈值。但是调高此值同时也意味着降低了老年代的实际可用空间官方文档特别提醒:如果增加或减少该参数比例,最好确保同步调整整个 Java heap 的大小 | 8、11、17 |
| -XX:G1HeapRegionSize | 内存 | G1 的内存模型是把 heap 分成一个个 region,该参数指定了 region 的大小。默认值根据 heap size 决定,最小值是 1M,最大值是 32M | Region的大小主要是关系到Humongous Object的判定,当一个对象超过Region大小的一半时,则为巨型对象,那么其会至少独占一个Region,如果一个放不下,会占用连续的多个Region。当一个Humongous Region放入了一个巨型对象,可能还有不少剩余空间,但是不能用于存放其他对象,这些空间就浪费了。所以如果应用里有很多大小差不多的巨型对象,可以适当调整Region的大小,尽量让他们以普通对象的形式分配,合理利用Region空间。 | 8、11、17 |
| -XX:G1NewSizePercent | 内存 | Young generation 内存大小的下限(以占堆大小的百分比来表示)。默认值是 5%。This is an experimental flag. See "How to unlock experimental VM flags" for an example. This setting replaces the -XX:DefaultMinNewGenPercent setting. |
新生代比例有两个数值指定,下限:-XX:G1NewSizePercent,默认值 5%,上限:-XX:G1MaxNewSizePercent,默认值 60%。G1 会根据实际的 GC 情况(主要是暂停时间)来动态的调整新生代的大小,主要是 Eden Region 的个数。最好是 Eden 的空间大一点,毕竟 Young GC 的频率更大,大的 Eden 空间能够降低 Young GC 的发生次数。但是 Mixed GC 是伴随着 Young GC 一起的,如果暂停时间短,那么需要更加频繁的 Young GC,同时也需要平衡好 Mixed GC 中新生代和老年代的 Region,因为新生代的所有 Region 都会被回收,如果 Eden 很大,那么留给老年代回收空间就不多了,最后可能会导致 Full GC。 | 11、17 |
| -XX:G1MaxNewSizePercent | 内存 | Young generation 内存大小的上限(以占堆大小的百分比来表示)。默认值是 60%。This is an experimental flag. See "How to unlock experimental VM flags" for an example. This setting replaces the -XX:DefaultMaxNewGenPercent setting. This setting is not available in Java HotSpot VM, build 23. |
见上 | 11、17 |
| -XX:ParallelGCThreads | 效率 | stop-the-world (STW) worker 线程数。该参数将被用作设置 logical processors 数。默认值取决于宿主机 CPU 的核数。 | 当 GC 时间过长时,可以尝试调大 GC 工作线程数,但是这也意味着此期间应用所占的线程数减少,会对吞吐量有一定影响。 | 8、11、17 |
| -XX:ConcGCThreads | 效率 | concurrent GC 阶段(即非 STW 阶段)所用到的线程数。根据官方文档,推荐设置为-XX:ParellelGCThreads的四分之一 |
该值同样是吞吐和停顿时间的取舍,需要根据实际需求测试调整。 | 8、11、17 |
| -XX:InitiatingHeapOccupancyPercent | 行为/时机 | 当老年代占用的整个 heap 的比例达到该值时,开启一个 concurrent GC cycle。默认值是 45%如果为 0,意味着 G1 将不停进行并发 GC,直到 G1 adaptively set this value(17 特性,8、11 没有该特性) | 如果该值过小,比如 Mixed GC 周期结束后老年代使用率还是超过 45%,那么会再次触发全局并发标记过程,这样就会导致频繁的老年代 GC,影响应用吞吐量如果老年代空间不大,Mixed GC 回收的空间偏少。可以适当调高 IHOP 的值如果此值太高,很容易导致年轻代晋升失败而触发 Full GC,所以需要多次调整测试进行取舍 | 8、11、17 |
| -XX:MaxTenuringThreshold | 行为/时机 | Tenuring 的阈值(补注:一般新生对象经过 15 次 Young GC 会晋升到老年代,巨型对象会直接分配在老年代),除了 CMS 默认值是 6,其他默认值是 15,最大值也是 15 | 一般这个值不需要额外调整,但如果观察到对象在达到某个年龄 n 后,不怎么晋升,直到15岁,那么可以适当调小 MaxTenuringThreshold 到 n,以减少 ygc 中复制对象的开销。 | 8、11、17 |
| -XX:G1HeapWastePercent | 行为/时机 | 开发者可接受浪费的 heap percentage。当可回收(reclaimable)的比例小于该值时,JVM 将不会触发 Mixed GC。默认值是 5% | 在全局标记结束后能够统计出所有 Cset 内可被回收的垃圾占整对的比例值,如果超过 5%,那么就会触发之后的多轮 Mixed GC,如果不超过,那么会在之后的某次 Young GC 中重新执行全局并发标记。可以尝试适当的调高此阈值,能够适当的降低 Mixed GC 的频率。 | 11、17 |
| -XX:G1MixedGCLiveThresholdPercent | 行为/时机 | Mixed GC 选择回收 old region 的 occupancy 阈值。在全局并发标记阶段,如果一个 Region 的 live objects 的空间占比低于此值,则会被纳入 Cset。此值直接影响到 Mixed GC 选择回收的区域。11 和 17 的默认值是 85This is an experimental flag. See "How to unlock experimental VM flags" for an example. This setting replaces the -XX:G1OldCSetRegionLiveThresholdPercent setting. |
当发现 GC 时间较长时,可以尝试调低此阈值,尽量优先选择回收垃圾占比高的Region,但此举也可能导致垃圾回收的不够彻底,最终触发 Full GC。 | 11、17 |
| -XX:G1OldCSetRegionThresholdPercent | 行为/时机 | 每轮 Mixed GC 回收的 Region 最大比例。默认值是 10% | 一般这个值不需要额外调整。 | 11、17 |
| -XX:G1MixedGCCountTarget | 行为/时机 | 回收超过G1MixedGCLIveThresholdPercent的老年代时,最多进行的 mixed gc 次数。默认值是 8,mixed gc 的目标是要控制在此目标次数以内 |
在一次全局并发标记后,最多接着 8 次 Mixed GC,即会把全局并发标记阶段生成的 Cset 里的 Region 拆分为最多 8 部分,然后在每轮 Mixed GC 里收集一部分。这个值要和上一个参数配合使用,8*10%=80%,应该来说会大于每次标记阶段的 Cset 集合了。一般此参数也不需额外调整。 | 11、17 |
| JVM 选项 | 类别 (控制哪方面) | 描述 | 调优建议 | 支持的 LTS version |
|---|---|---|---|---|
| -XX:+UseParallelGC | - | 使用 PS,JDK 默认使用该参数,use Parallel Scavenge + Parallel Old as a combination. | 无 | 8,11,17 |
| -XX:MaxGCPauseMillis | 自适应 | 指定最大的 GC pause time。指定的 target 是一个软目标(soft goal),JVM 会用一些机制尽最大努力来达到它。默认不使用它,值为 18446744073709551615可以通过设置来使用它 | 同上 | 8,11,17 |
| -XX:GCTimeRatio | 自适应 | 参数的值应当是一个大于 0 且小于 100 的整数,也就是垃圾收集时间占总时间的比率,相当于是吞吐量的倒数。如果把此参数设置为 19,那允许的最大 GC 时间就占总时间的 5%(即 1 /(1+19))PS 的默认值为 99,就是允许最大 1%(即 1 /(1+99))的垃圾收集时间。 | 根据需求妥善设置,如果该值设置的过高,那么可能会导致 heap 一直在 minimum 的 size。该参数与 JVM GC 的自适应调优(GC ergonomics)相关 | 8,11,17 |
| -XX:+UseAdaptiveSizePolicy | 自适应 | 一个开关参数,默认打开。当这个参数打开之后,就不需要手工指定新生代的大小(-Xmn)、Eden 与 Survivor 区的比例(-XX:SurvivorRatio)、晋升老年代对象年龄(-XX:PretenureSizeThreshold)等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量,这种调节方式称为 GC 自适应的调节策略(GC ergonomics)。 | 如果对于收集器运作原来不太了解,手工优化存在困难的时候,使用 Parallel Scavenge 收集器配合自适应调节策略,把内存管理的调优任务交给虚拟机去完成将是一个不错的选择。只需要把基本的内存数据设置好(如-Xmx 设置最大堆),然后使用 MaxGCPauseMillis 参数(更关注最大停顿时间)或 GCTimeRatio(更关注吞吐量)参数给虚拟机设立一个优化目标,那具体细节参数的调节工作就由虚拟机完成了。自适应调节策略也是 Parallel Scavenge 收集器与 ParNew 收集器的一个重要区别。 | 8,11,17 |
| -XX:SurvivorRatio | 内存 | Eden 区和 survivor 区大小的比值,默认为 8 | 如果大量对象都在一次 YoungGC 后就会回收清理,那么新生代 Eden:From:To 为默认的 8:1:1 就比较合适。如果说很大部分对象的年龄都超过1,即需要在Survivor 的 From, To 中来回转换几次之后才能被回收,那么此时可以适当增大一下 Survivor 区的空间,并且可以将 Survivor 的空间使用率增大,避免对象年龄增长过快,从而被移动到老年代,造成 FullGC | 8,11,17 |
| -XX:NewRatio | 内存 | young/old generation 大小的比例。默认值是 2 | 如果 old 区有空余,可以增大 young 区的比例,减少 ygc 的次数,减缓对象晋升的速度。 | 8,11,17 |
| -XX:PretenureSizeThreshold | 内存 | 直接进入老年代的对象的大小阈值,大于这个值的对象将直接在老年代分配没在 8、11、17 的官方文档上找到该参数的说明,所以没找到默认值是多少。但是实测 8、11、17 均支持该参数 | 暂时没搜到相关资料 | 8,11,17 |
| -XX:ParallelGCThreads | 效率 | stop-the-world (STW) worker 线程数。该参数将被用作设置 logical processors 数。默认值取决于宿主机 CPU 的核数。 | 当 GC 时间过长时,可以尝试调大 GC 工作线程数,但是这也意味着此期间应用所占的线程数减少,会对吞吐量有一定影响。 | 8,11,17 |
| -XX:+UseNUMA | 效率 | 通过增加应用程序对低延迟内存的使用,可以优化具有 NUMA 的计算机上的应用程序的性能。 默认情况下,该选项处于禁用状态,并且不会对 NUMA 进行优化。 该选项仅在使用 PS (-XX:+UseParallelGC)时可用。 | 支持 NUMA 可以打开 | 8,11,17 |
| -XX:TargetSurvivorRatio | 行为/时机 | 该参数设置了 YGC 后,期望 survivor used space 占比多少。根据这篇 blog 的实验结果,如果 survivor space 的 usage 占比超过了该参数,那么某些 live objects 会被直接晋升到 tenured generation,即使他们的 age 可能还没到 MaxTenuringThreshold。这种现象叫做 premature tenuring (or premature promotion)默认值为 50% | 该参数一定程度上决定了对象的晋升。 | 8,11,17 |
我们调优的时候,通常从以下事实出发:
我们的终极目标是
首先对于减少次数:我们从 GC 的成因拆解。
其次对于减少耗时,这里要复杂很多,我们仍然从成因分析:
MaxTenuringThreshold 要尽量合理,不能设置太大,否则有些长寿对象在每次 GC 时都会在两个 Survivor 区之间来回复制,无疑是增加了复制阶段的耗时各分区的大小对 GC 的性能影响很大。如何将各分区调整到合适的大小,分析活跃数据的大小是很好的切入点。
活跃数据的大小是指,应用程序稳定运行时长期存活对象在堆中占用的空间大小,也就是 Full GC 后堆中老年代占用空间的大小。可以通过 GC 日志中 Full GC 之后老年代数据大小得出,比较准确的方法是在程序稳定后,多次获取 GC 数据,通过取平均值的方式计算活跃数据的大小。活跃数据和各分区之间的比例关系如下:
| 内存区域 | 推荐设置大小 |
|---|---|
| 总大小 | 3-4 倍活跃数据的大小 |
| 新生代 | 1-1.5 活跃数据的大小 |
| 老年代 | 2-3 倍活跃数据的大小 |
| 永久代 | 1.2-1.5 倍Full GC后的永久代空间占用 |
例如,根据GC日志获得老年代的活跃数据大小为300M,那么各分区大小可以设为:
总堆:1200MB = 300MB ×ばつ 4
新生代:450MB = 300MB ×ばつ 1.5
老年代: 750MB = 1200MB - 450MB
这部分设置仅仅是堆大小的初始值,后面的优化中,可能会调整这些值,具体情况取决于应用程序的特性和需求。
Parallel GC会尽量去满足如下目标:(优先级由高到低)
对 ParallelGC 的调优,其目标应尽可能避免 Full GC, 这就需要优化对象老年化的频率,
使用 ParallelGC 时,垃圾收集的资源开销应小于 5%,如果已经减少到 1% 甚至更少,基本上已经达到极限了。
-XX:GCTimeRatio=99 。其值为1-100的整数,表示吞吐量,默认值是99,表示允许1%的垃圾回收时间占比。暂停时间越长,那么垃圾回收占用的时间比越大,可能会超过前面的设定比例。-XX:ParallelGCThreads ,给用户线程让出更多的 CPU 资源-XX:MaxGCPauseMills等-Xmn-XX:NewRatio-XX:SurvivorRatio-Xmn-XX:NewRatio-XX:MaxGCPauseMills=<N> ,值大于0的毫秒数。-Xmn(设置新生代大小) 、-XX:-UseAdaptiveSizePolicy(关闭自适应调整)-XX:SurvivorRatio 可以调整新生代中 Survivor 与 Eden 区的比例,例如 -XX:SurvivorRatio=6 表示S(From) : S(To) : Eden = 1: 1: 6 。其默认值为8-XX:ParallelGCThreads=<N>。此参数设置年轻代并行收集器的线程数,一般与 CPU数量相等,过多的线程数量会影响垃圾回收以及整个程序的性能。无论以延迟还是吞吐为目标,Oracle 官网不建议我们显式通过-Xmn设置新生代区域大小(仅管前面 PS 我们提到过这种做法),因为这与 G1 的默认表现相悖:
想达到吞吐目标,建议优先考虑 PS。
如果想使用 G1 的同时优化吞吐,建议:
-XX:ParallelGCThreads 和 -XX:ConcGCThreads,给用户线程让出更多的 CPU 资源XX:MaxGCPauseMillis等方法 -XX:G1NewSizePercent 并调高 young 区的上限 size -XX:G1MaxNewSizePercent-XX:G1HeapWastePercent。也可以调整 -XX:InitiatingHeapOccupancyPercent,来提前开启并发标记,用更多的小代价 mixed gc 换取更少的高代价 fgc-XX:G1RSetUpdatingPauseTimePercent,将并发工作移到 gc pause 中。-XX:+AlwaysPreTouch,将 -Xmx 和 -Xms 设置为同样的值以禁止堆自动扩缩容根据 oracle 官网的建议,对于延迟我们主要关注 XX:MaxGCPauseMillis=<N>参数,考虑将XX:MaxGCPauseMillis 设置为能满足 90% 场景需求的值。也就是说,90% 的 gc 停顿不超过 XX:MaxGCPauseMillis
如果我们在日志中看到很多 Evacuation Failure,那么可以考虑下面一些方案:
What is an Evacuation Failure?
A promotion failure that happens when a JVM runs out of heap regions during the GC for either survivors and promoted objects. The heap can’t expand because it is already at max. This is indicated in the GC logs when using
-XX:+PrintGCDetailsbyto-space overflow. This is expensive!
- GC still has to continue, so space has to be freed up.
- Unsuccessfully copied objects have to be tenured in place.
- Any updates to RSets of regions in the CSet have to be regenerated.
- All of these steps are expensive.
增加 heap size
-XX:G1ReservePercent=n**, 默认值是 10。这样会流出更多的空间来供晋升更早开启 marking cycle,调低 -XX:InitiatingHeapOccupancyPercent(提前开启 mixed GC)。 Evacuation Failure 的代价比多执行一些 marking cycle 周期高很多。
增加 marking 阶段的线程,设置 -XX:ConcGCThreads
优化 ygc 时间。
优化 mixed gc 时间(此段参考 oracle 官网)
-XX:G1MixedGCCountTarget 将老年代区域回收分散到更多垃圾回收中。-XX:G1MixedGCLiveThresholdPercent ,不将花费大量时间的区域放入候选收集集中,可以避免收集这些区域。 在许多情况下,高度占用的区域需要花费大量时间来收集。-XX:G1HeapWastePercent,尽早停止老年代的空间回收,这样 G1 就不会收集那么多高度占用的区域。JDK8
-Xloggc:${IOTDB_HOME}/logs/gc.log
-XX:+PrintGCDateStamps
-XX:+PrintGCDetails
-XX:+PrintHeapAtGC
-XX:+PrintReferenceGC
-XX:+PrintSafepointStatistics
-XX:PrintSafepointStatisticsCount=1
-XX:+PrintTenuringDistribution
-XX:+PrintGCApplicationStoppedTime
-XX:+PrintPromotionFailure
-XX:+UseGCLogFileRotation
-XX:NumberOfGCLogFiles=10
-XX:GCLogFileSize=100M
JDK9 及以上 JVM 重构了日志框架。大大简化了日志参数,下面的参数基本与上面等价:
-Xlog:
gc*=debug,
heap*=debug,
age*=trace,
metaspace*=info,
safepoint*=debug,
promotion*=info:
file=${IOTDB_HOME}/logs/gc.log:time,uptime,pid,tid,level,tags:filecount=10,filesize=100M
默认的 JVM 日志包括:
[启动经过时间][日志级别][日志标签,可能包含多个] 日志内容
比如:
[2024-01-15T15:14:32.025+0800][278.288s][3381829][3381900][debug][gc,alloc,region ] GC(418) Mutator Allocation stats, regions: 136, wasted size: 2952B ( 0.0%)
其中一行日志,可能包含多个标签。如上面的例子里标签就有 gc、alloc、region。之后关于 JVM 日志相关的配置,也是围绕着这些标签进行配置。
gc 日志有很多标签与组合,大部分以 gc 标签为开始,混合搭配其他一些标签。一般,有如下几个标签我们会经常用到:
gcgc 总体描述日志,一般设置 info 级别查看 gc 的发生时间,消耗时间还有内存大小。例如:
[2024-01-15T15:14:32.112+0800][278.375s][3381829][3381900][info ][gc ] GC(418) Pause Young (Normal) (G1 Evacuation Pause) 20934M->20078M(25088M) 86.631ms
包含了 gc 类型,gc 原因,收集内存大小,持续时间等信息
目前 System 面板较好的集成了该项日志信息,也推荐直接看 System 面板。
gc,agegc 中 age 相关信息,age 比较高的对象会进入老年代。debug 级别只会输出最高级别的 age 以及期望大小。如果是 trace 级别,会输出:
例如:
[2024-01-15T15:14:44.595+0800][290.858s][3381829][3381900][trace][gc,age ] GC(441) Age table with threshold 15 (max threshold 15)
[2024-01-15T15:14:44.595+0800][290.858s][3381829][3381900][trace][gc,age ] GC(441) - age 1: 164913704 bytes, 164913704 total
[2024-01-15T15:14:44.595+0800][290.858s][3381829][3381900][trace][gc,age ] GC(441) - age 2: 36313680 bytes, 201227384 total
这个标签可以有效分析对象的晋升年龄情况,从而调整 -XX:MaxTenuringThreshold等选项
gc,alloc,gc,alloc,region这两个参数仅对 G1 gc 有效 gc,alloc 在 gc 完成的时候,打印 trace 级别日志记录触发 gc 的线程是哪一个以及返回的 gc 结果地址,一般是在 debug 调试 gc 的时候才需要看这个日志,平时开到 debug 级别即可。
[2020-02-28T02:14:02.694+0000][trace][gc,alloc ] sdk-27692-2-amqp-t-4: Successfully scheduled collection returning 0x00000007ffc00000
gc,alloc,region 统计每次 gc 的 Regions 信息,打印 debug 级别日志。
[2024-01-15T15:14:46.431+0800][292.694s][3381829][3381900][debug][gc,alloc,region ] GC(445) Mutator Allocation stats, regions: 136, wasted size: 536B ( 0.0%)
gc,cpu 这个是大多数 gc 问题定位需要查看的日志,info 级别打印每次gc真正耗时:注意到日志中时间分为了三块:user, sys,和 real,三者的区别和含义如下:
real:指的是在此次 GC 事件中 CPU 实际所花费的时间,跟钟表上的时间是一致的(wall clock time);user:指的是 CPU 工作在用户态所花费的时间;sys:指的是 CPU 工作在内核态所花费的时间。User + sys 就是 CPU 花费的实际时间,注意这个值统计了所有CPU上的时间。如果 CPU 是多核的,User + sys 可能会比自然流逝的绝对时间大
[2024-01-15T15:23:39.132+0800][825.395s][3381829][3381900][info ][gc,cpu ] GC(1449) User=1.41s Sys=0.00s Real=0.11s
一般来说,如果看到 real 所显示的时间 远 大于 User + sys,表明 GC 过程存在某些问题(等待计算资源),原因可能是下面这两种:
如果 sys 特别高,说明虚拟机没有得到足够的 CPU,主机的负载可能太高了。可能是 io、内存分配等方面出了问题;
gc,ergo,gc,ergo,cset,gc,ergo,ihop,gc,ergo,refine这是 GC 自适应调节(ergonomics)相关的日志,记录了 GC 是如何自适应调节相关参数的。一般的 debug 级别信息就够了
[2024-01-15T15:23:39.439+0800][825.702s][3381829][3381900][debug][gc,ergo ] GC(1450) Running G1 Clear Card Table Task using 9 workers for 9 units of work for 538 regions.
[2024-01-15T15:23:39.343+0800][825.606s][3381829][3381900][debug][gc,ergo,cset ] GC(1450) Finish choosing CSet. old: 0 regions, predicted old region time: 0.00ms, time remaining: 68.65
[2024-01-15T15:23:02.240+0800][788.503s][3381829][3381900][debug][gc,ergo,ihop ] GC(1378) Request concurrent cycle initiation (occupancy higher than threshold) occupancy: 12398362624B allocation request: 0B threshold: 11838003609B (45.00) source: end of GC
[2024-01-15T15:23:02.240+0800][788.503s][3381829][3381900][debug][gc,ergo,refine ] GC(1378) Updated Refinement Zones: green: 82, yellow: 246, red: 410
gc,heap,gc,heap,regiongc,heap的 debug 级别会显示 gc 的时候堆的概况,对于 g1 gc gc,heap,region的 trace 级别,会打印每一个 region 的详细情况,这个一般供 gc 调试使用。
我们一般只需要关心gc,heap的日志就行了:关注年轻代和年老代堆回收前后的情况,观察 GC 效率、堆空间增长速度等。
目前 System 面板较好的集成了该项日志信息,也推荐直接看 System 面板。
[2024-01-15T15:23:02.240+0800][788.503s][3381829][3381900][info ][gc,heap ] GC(1378) Eden regions: 251->0(229)
[2024-01-15T15:23:02.240+0800][788.503s][3381829][3381900][info ][gc,heap ] GC(1378) Survivor regions: 32->35(35)
[2024-01-15T15:23:02.240+0800][788.503s][3381829][3381900][info ][gc,heap ] GC(1378) Old regions: 1040->1148
[2024-01-15T15:23:02.240+0800][788.503s][3381829][3381900][info ][gc,heap ] GC(1378) Humongous regions: 334->330
[2024-01-15T15:23:02.240+0800][788.503s][3381829][3381900][debug][gc,heap ] GC(1378) Heap after GC invocations=1343 (full 36): garbage-first heap total 25690112K, used 12333688K [0x00000001e0000000, 0x0000000800000000)
[2024-01-15T15:23:02.240+0800][788.503s][3381829][3381900][debug][gc,heap ] GC(1378) region size 8192K, 35 young (286720K), 35 survivors (286720K)
[2024-01-15T15:23:02.240+0800][788.503s][3381829][3381900][debug][gc,heap ] GC(1378) Metaspace used 66936K, capacity 68918K, committed 69760K, reserved 1110016K
[2024-01-15T15:23:02.240+0800][788.503s][3381829][3381900][debug][gc,heap ] GC(1378) class space used 7554K, capacity 8389K, committed 8780K, reserved 1048576K
gc,humongous如果使用的是 G1 gc,并且经常出现 Evacuation Failure 或者 Humongous Allocation ,并且不知道是什么原因的话,可以考虑看看这个标签相关的日志:
记载了大对象的分配情况和大对象 size。至于后面被哪个 remset 标记,在哪个 code root,如果不是调试 gc 的话一般不用关注
[2024-01-15T15:23:02.748+0800][789.011s][3381829][3381900][debug][gc,humongous ] GC(1379) Live humongous region 0 object size 22420064 start 0x00000001e0000000 with remset 0 code roots 0 is marked 0 reclaim candidate 0 type array 1
[2024-01-15T15:23:02.748+0800][789.011s][3381829][3381900][debug][gc,humongous ] GC(1379) Live humongous region 1052 object size 22420064 start 0x00000003ee000000 with remset 1 code roots 0 is marked 0 reclaim candidate 0 type array 1
[2024-01-15T15:23:02.748+0800][789.011s][3381829][3381900][debug][gc,humongous ] GC(1379) Live humongous region 1057 object size 22420064 start 0x00000003f0800000 with remset 0 code roots 0 is marked 0 reclaim candidate 0 type array 1
[2024-01-15T15:23:02.748+0800][789.011s][3381829][3381900][debug][gc,humongous ] GC(1379) Live humongous region 1065 object size 22420064 start 0x00000003f4800000 with remset 0 code roots 0 is marked 0 reclaim candidate 0 type array 1
[2024-01-15T15:23:02.748+0800][789.011s][3381829][3381900][debug][gc,humongous ] GC(1379) Live humongous region 1070 object size 8968208 start 0x00000003f7000000 with remset 0 code roots 0 is marked 0 reclaim candidate 0 type array 0
gc,metaspace查看 metaspace 相关的 gc 日志,gc,metaspace的 info 级别会输出每次 gc 涉及的 metaspace 内存变化。
[2024-01-15T15:23:03.645+0800][789.908s][3381829][3381900][info ][gc,metaspace ] GC(1381) Metaspace: 66936K(69760K)->66936K(69760K) NonClass: 59381K(60980K)->59381K(60980K) Class: 7554K(8780K)->7554K(8780K)
gc,phases,gc,phases,ref,gc,phases,task,gc,ref,gc,start,gc,ref,start,gc,task 等这些标签与 gc 步骤相关,详细记录了 GC 每步的耗时、软/弱等各类引用处理等信息。可以查看这些标签的日志,来了解 gc 的步骤以及原理。
一般可以通过该类日志查看 GC 在哪步耗时显著大,从而针对性优化。
[2024-01-15T15:23:04.104+0800][790.366s][3381829][3381900][info ][gc,phases ] GC(1382) Pre Evacuate Collection Set: 0.1ms
[2024-01-15T15:23:04.104+0800][790.366s][3381829][3381900][debug][gc,phases ] GC(1382) Prepare TLABs: 0.0ms
[2024-01-15T15:23:04.104+0800][790.366s][3381829][3381900][debug][gc,phases ] GC(1382) Choose Collection Set: 0.0ms
[2024-01-15T15:23:04.104+0800][790.366s][3381829][3381900][debug][gc,phases ] GC(1382) Humongous Register: 0.1ms
[2024-01-15T15:23:04.104+0800][790.366s][3381829][3381900][info ][gc,phases ] GC(1382) Evacuate Collection Set: 144.4ms
[2024-01-15T15:23:04.104+0800][790.366s][3381829][3381900][debug][gc,phases ] GC(1382) Ext Root Scanning (ms): Min: 0.3, Avg: 0.3, Max: 0.5, Diff: 0.3, Sum: 4.3, Workers: 13
[2024-01-15T15:23:04.104+0800][790.366s][3381829][3381900][debug][gc,phases ] GC(1382) Update RS (ms): Min: 4.0, Avg: 16.6, Max: 56.5, Diff: 52.5, Sum: 215.4, Workers: 13
[2024-01-15T15:23:04.104+0800][790.366s][3381829][3381900][debug][gc,phases ] GC(1382) Processed Buffers: Min: 3, Avg: 12.4, Max: 24, Diff: 21, Sum: 161, Workers: 13
[2024-01-15T15:23:04.104+0800][790.366s][3381829][3381900][debug][gc,phases ] GC(1382) Scanned Cards: Min: 767, Avg: 2468.7, Max: 4190, Diff: 3423, Sum: 32093, Workers: 13
[2024-01-15T15:23:04.104+0800][790.366s][3381829][3381900][debug][gc,phases ] GC(1382) Skipped Cards: Min: 0, Avg: 0.4, Max: 1, Diff: 1, Sum: 5, Workers: 13
[2024-01-15T15:23:04.104+0800][790.366s][3381829][3381900][debug][gc,phases ] GC(1382) Scan RS (ms): Min: 7.2, Avg: 34.5, Max: 62.1, Diff: 54.9, Sum: 448.7, Workers: 13
[2024-01-15T15:23:04.104+0800][790.366s][3381829][3381900][debug][gc,phases ] GC(1382) Scanned Cards: Min: 1501, Avg: 11120.2, Max: 21225, Diff: 19724, Sum: 144563, Workers: 13
[2024-01-15T15:23:04.104+0800][790.366s][3381829][3381900][debug][gc,phases ] GC(1382) Claimed Cards: Min: 1792, Avg: 13903.3, Max: 30483, Diff: 28691, Sum: 180743, Workers: 13
[2024-01-15T15:23:04.104+0800][790.366s][3381829][3381900][debug][gc,phases ] GC(1382) Skipped Cards: Min: 43974, Avg: 105456.0, Max: 143149, Diff: 99175, Sum: 1370928, Workers: 13
[2024-01-15T15:23:04.104+0800][790.366s][3381829][3381900][debug][gc,phases ] GC(1382) Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0, Workers: 13
[2024-01-15T15:23:04.104+0800][790.366s][3381829][3381900][debug][gc,phases ] GC(1382) AOT Root Scanning (ms): skipped
[2024-01-15T15:23:04.104+0800][790.366s][3381829][3381900][debug][gc,phases ] GC(1382) Object Copy (ms): Min: 71.7, Avg: 92.3, Max: 129.0, Diff: 57.3, Sum: 1199.7, Workers: 13
[2024-01-15T15:23:04.104+0800][790.366s][3381829][3381900][debug][gc,phases ] GC(1382) Termination (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0, Workers: 13
[2024-01-15T15:23:04.104+0800][790.366s][3381829][3381900][debug][gc,phases ] GC(1382) Termination Attempts: Min: 1, Avg: 1.0, Max: 1, Diff: 0, Sum: 13, Workers: 13
[2024-01-15T15:23:04.104+0800][790.366s][3381829][3381900][debug][gc,phases ] GC(1382) GC Worker Other (ms): Min: 0.1, Avg: 0.3, Max: 0.6, Diff: 0.6, Sum: 4.5, Workers: 13
[2024-01-15T15:23:04.104+0800][790.366s][3381829][3381900][debug][gc,phases ] GC(1382) GC Worker Total (ms): Min: 143.8, Avg: 144.1, Max: 144.3, Diff: 0.5, Sum: 1873.1, Workers: 13
[2024-01-15T15:23:04.104+0800][790.366s][3381829][3381900][info ][gc,phases ] GC(1382) Post Evacuate Collection Set: 1.4ms
[2024-01-15T15:23:04.104+0800][790.366s][3381829][3381900][debug][gc,phases ] GC(1382) Code Roots Fixup: 0.0ms
[2024-01-15T15:23:04.104+0800][790.366s][3381829][3381900][debug][gc,phases ] GC(1382) Clear Card Table: 0.3ms
[2024-01-15T15:23:04.104+0800][790.366s][3381829][3381900][debug][gc,phases ] GC(1382) Reference Processing: 0.1ms
[2024-01-15T15:23:04.104+0800][790.366s][3381829][3381900][debug][gc,phases,ref ] GC(1382) Reconsider SoftReferences: 0.0ms
[2024-01-15T15:23:04.104+0800][790.366s][3381829][3381900][debug][gc,phases,ref ] GC(1382) SoftRef (ms): skipped
[2024-01-15T15:23:04.104+0800][790.366s][3381829][3381900][debug][gc,phases,ref ] GC(1382) Notify Soft/WeakReferences: 0.0ms
[2024-01-15T15:23:04.104+0800][790.366s][3381829][3381900][debug][gc,phases,ref ] GC(1382) SoftRef (ms): skipped
[2024-01-15T15:23:04.104+0800][790.367s][3381829][3381900][debug][gc,phases,ref ] GC(1382) WeakRef (ms): skipped
[2024-01-15T15:23:04.104+0800][790.367s][3381829][3381900][debug][gc,phases,ref ] GC(1382) FinalRef (ms): skipped
[2024-01-15T15:23:04.104+0800][790.367s][3381829][3381900][debug][gc,phases,ref ] GC(1382) Total (ms): skipped
[2024-01-15T15:23:04.104+0800][790.367s][3381829][3381900][debug][gc,phases,ref ] GC(1382) Notify and keep alive finalizable: 0.0ms
[2024-01-15T15:23:04.104+0800][790.367s][3381829][3381900][debug][gc,phases,ref ] GC(1382) FinalRef (ms): skipped
[2024-01-15T15:23:04.104+0800][790.367s][3381829][3381900][debug][gc,phases,ref ] GC(1382) Notify PhantomReferences: 0.0ms
[2024-01-15T15:23:04.104+0800][790.367s][3381829][3381900][debug][gc,phases,ref ] GC(1382) PhantomRef (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0, Workers: 1
[2024-01-15T15:23:04.104+0800][790.367s][3381829][3381900][debug][gc,phases,ref ] GC(1382) SoftReference:
[2024-01-15T15:23:04.104+0800][790.367s][3381829][3381900][debug][gc,phases,ref ] GC(1382) Discovered: 0
[2024-01-15T15:23:04.104+0800][790.367s][3381829][3381900][debug][gc,phases,ref ] GC(1382) Cleared: 0
[2024-01-15T15:23:04.104+0800][790.367s][3381829][3381900][debug][gc,phases,ref ] GC(1382) WeakReference:
[2024-01-15T15:23:04.104+0800][790.367s][3381829][3381900][debug][gc,phases,ref ] GC(1382) Discovered: 0
[2024-01-15T15:23:04.104+0800][790.367s][3381829][3381900][debug][gc,phases,ref ] GC(1382) Cleared: 0
[2024-01-15T15:23:04.104+0800][790.367s][3381829][3381900][debug][gc,phases,ref ] GC(1382) FinalReference:
[2024-01-15T15:23:04.104+0800][790.367s][3381829][3381900][debug][gc,phases,ref ] GC(1382) Discovered: 0
[2024-01-15T15:23:04.104+0800][790.367s][3381829][3381900][debug][gc,phases,ref ] GC(1382) Cleared: 0
[2024-01-15T15:23:04.104+0800][790.367s][3381829][3381900][debug][gc,phases,ref ] GC(1382) PhantomReference:
[2024-01-15T15:23:04.104+0800][790.367s][3381829][3381900][debug][gc,phases,ref ] GC(1382) Discovered: 2
[2024-01-15T15:23:04.104+0800][790.367s][3381829][3381900][debug][gc,phases,ref ] GC(1382) Cleared: 2
[2024-01-15T15:23:04.104+0800][790.367s][3381829][3381900][debug][gc,phases ] GC(1382) Weak Processing: 0.1ms
[2024-01-15T15:23:04.104+0800][790.367s][3381829][3381900][debug][gc,phases ] GC(1382) Merge Per-Thread State: 0.0ms
[2024-01-15T15:23:04.104+0800][790.367s][3381829][3381900][debug][gc,phases ] GC(1382) Code Roots Purge: 0.0ms
[2024-01-15T15:23:04.104+0800][790.367s][3381829][3381900][debug][gc,phases ] GC(1382) Redirty Cards: 0.1ms
[2024-01-15T15:23:04.104+0800][790.367s][3381829][3381900][debug][gc,phases ] GC(1382) DerivedPointerTable Update: 0.0ms
[2024-01-15T15:23:04.104+0800][790.367s][3381829][3381900][debug][gc,phases ] GC(1382) Free Collection Set: 0.3ms
[2024-01-15T15:23:04.104+0800][790.367s][3381829][3381900][debug][gc,phases ] GC(1382) Humongous Reclaim: 0.6ms
[2024-01-15T15:23:04.104+0800][790.367s][3381829][3381900][debug][gc,phases ] GC(1382) Start New Collection Set: 0.0ms
[2024-01-15T15:23:04.104+0800][790.367s][3381829][3381900][debug][gc,phases ] GC(1382) Resize TLABs: 0.0ms
[2024-01-15T15:23:04.104+0800][790.367s][3381829][3381900][debug][gc,phases ] GC(1382) Expand Heap After Collection: 0.0ms
[2024-01-15T15:23:04.104+0800][790.367s][3381829][3381900][info ][gc,phases ] GC(1382) Other: 0.3ms
[2024-01-15T15:23:08.433+0800][794.696s][3381829][3381900][info ][gc,start ] GC(1389) Pause Young (Normal) (G1 Evacuation Pause)
[2024-01-15T15:23:08.433+0800][794.696s][3381829][3381900][info ][gc,task ] GC(1389) Using 13 workers of 13 for evacuation
[2024-01-15T15:23:08.433+0800][794.696s][3381829][3381900][debug][gc,tlab ] GC(1389) TLAB totals: thrds: 47 refills: 1704 max: 181 slow allocs: 56 max 29 waste: 1.1% gc: 15548672B max: 1563232B slow: 1233720B max: 197560B fast: 0B max: 0B
[2024-01-15T15:23:08.433+0800][794.696s][3381829][3381900][debug][gc,alloc,region ] GC(1389) Mutator Allocation stats, regions: 180, wasted size: 0B ( 0.0%)
safepoint我们知道只有到达 safepoint,我们才可以进行 gc,如果我们对这些 safepoint 感兴趣,可以查看这个标签的 debug 级别的日志。
关于 safepoint 的解析、日志观测、调优,感兴趣的同学可以参考这篇文章:
https://zhuanlan.zhihu.com/p/161710652?spm=a2c6h.12873639.article-detail.15.7a8b1da3Hy4DoP
[2024-03-12T17:57:57.360+0800][1388.942s][558968][559235][info ][safepoint,stats ] G1CollectForAllocation [ 1542 3 ][ 578703 173202 13655482720 13656234625 ] 6
[2024-03-12T17:57:57.360+0800][1388.942s][558968][559235][info ][safepoint ] Safepoint "G1CollectForAllocation", Time since last: 153683087494 ns, Reaching safepoint: 578703 ns, Cleanup: 173202 ns, At safepoint: 13655482720 ns, Total: 13656234625 ns
[2024-03-08T11:10:51.558+0800][1624.879s][121648][121979][warning][safepoint ] # SafepointSynchronize::begin: Timeout detected:
[2024-03-08T11:10:51.558+0800][1624.879s][121648][121979][warning][safepoint ] # SafepointSynchronize::begin: Timed out while spinning to reach a safepoint.
[2024-03-08T11:10:51.558+0800][1624.879s][121648][121979][warning][safepoint ] # SafepointSynchronize::begin: Threads which did not reach the safepoint:
[2024-03-08T11:10:51.558+0800][1624.879s][121648][121979][warning][safepoint ] # "Pipe-Async-Connector-Client-Pool-selector-299" #299 daemon prio=5 os_prio=0 cpu=24998.13ms elapsed=1435.09s tid=0x00007e63dc2e1d00 nid=0x1deb7 runnable [0x0000000000000000]
[2024-03-08T11:10:51.559+0800][1624.879s][121648][121979][warning][safepoint ] java.lang.Thread.State: RUNNABLE
[2024-03-08T11:10:51.559+0800][1624.879s][121648][121979][warning][safepoint ]
[2024-03-08T11:10:51.559+0800][1624.879s][121648][121979][warning][safepoint ] # "pool-29-IoTDB-Pipe-Connector-Executor-Pool-69" #1476 prio=5 os_prio=0 cpu=18305.09ms elapsed=1320.80s tid=0x00007e63fc376ee0 nid=0x1e842 runnable [0x0000000000000000]
[2024-03-08T11:10:51.559+0800][1624.879s][121648][121979][warning][safepoint ] java.lang.Thread.State: RUNNABLE
[2024-03-08T11:10:51.559+0800][1624.879s][121648][121979][warning][safepoint ]
[2024-03-08T11:10:51.559+0800][1624.879s][121648][121979][warning][safepoint ] # "pool-29-IoTDB-Pipe-Connector-Executor-Pool-81" #1488 prio=5 os_prio=0 cpu=17034.13ms elapsed=1320.80s tid=0x00007e63fc385400 nid=0x1e84e runnable [0x0000000000000000]
[2024-03-08T11:10:51.559+0800][1624.879s][121648][121979][warning][safepoint ] java.lang.Thread.State: RUNNABLE
[2024-03-08T11:10:51.559+0800][1624.879s][121648][121979][warning][safepoint ]
[2024-03-08T11:10:51.559+0800][1624.879s][121648][121979][warning][safepoint ] # "pool-26-IoTDB-ClientRPC-Processor-913" #1564 prio=5 os_prio=0 cpu=104459.15ms elapsed=1300.41s tid=0x00007e63e44113c0 nid=0x1e8cb runnable [0x00007e5254478000]
[2024-03-08T11:10:51.559+0800][1624.879s][121648][121979][warning][safepoint ] java.lang.Thread.State: RUNNABLE
[2024-03-08T11:10:51.559+0800][1624.879s][121648][121979][warning][safepoint ]
[2024-03-08T11:10:51.559+0800][1624.879s][121648][121979][warning][safepoint ] # SafepointSynchronize::begin: (End of list)
非常强大的 GC 日志可视化分析报告,强大之处在于自动整合耗时、内存、cause、年龄等各类日志分析,并给出一些调优思路(实测不是特别靠谱,但可以参考)。
国外原版需收费。下面是国内镜像,实测免费:
https://gceasy.ycrash.cn/gc-index.jsp
笔者还调研了 Spark、Hadoop、Alluxio、Hbase、Ratis 等大数据系统的 JVM 默认参数或调优建议,这里贴出提供了相关信息的项目。
Spark 的目标是确保只有长寿的 RDD 存储在老年代,这样新生代便足以存放短命对象。这将有助于避免 fgc
Spark 建议:
E,那么可以设置 -Xmn=4/3*Espark.memory.fraction,这里不赘述)。此外,还可以减小新生代大小。如果已经减小过 -Xmn,可以试试 NewRatio为了低延迟而调优:
-XX:+UseConcMarkSweepGC-XX:CMSInitiatingOccupancyFraction=70-Xmn512m-XX:+UseParNewGC-XX:+UseCMSInitiatingOccupancyOnlyhbase-site.xml, set hbase.client.scanner.max.result.size to 1/8th of eden space (with -Xmn512m this is ~51MB )max.result.size x handler.count less than survivor spaceOS-Level 调优:
echo never > /sys/kernel/mm/transparent_hugepage/enabled
echo never > /sys/kernel/mm/transparent_hugepage/defrag
vm.swappiness = 0(禁用 swap)vm.min_free_kbytes to at least 1GB (8GB on larger memory systems)(调整内存水位线)vm.zone_reclaim_mode = 0贴出 9 个典型场景,并概述他们的场景、解决方案和效果
解决方案:调整 young 区大小、比例(优化 ygc 频率);调整晋升年龄阈值(优化 ygc 耗时);优化偏向锁(safepoint)(优化 ygc 耗时)
效果:Young GC 的频率减少了大概 1/3。GC 的吞量提高了 3.8%,达到了 96.8%
https://zhuanlan.zhihu.com/p/165382850
https://blog.csdn.net/hilaryfrank/article/details/109598582
解决方案:调整 young 区大小,防止大量临时对象进入 old 区,让 ygc 更彻底。
效果:通过扩容新生代为为原来的三倍,单次Minor GC时间增加小于5ms,频率下降了60%,服务响应时间TP90,TP99都下降了10ms+,服务可用性得到提升。
解决方案:设法让 CMS 在 Remark 前执行一次 ygc,使得新生代的对象数量减少,进而减少 remark 工作量
效果:通过扩容新生代为为原来的三倍,单次Minor GC时间增加小于5ms,频率下降了60%,服务响应时间TP90,TP99都下降了10ms+,服务可用性得到提升。
解决方案:固定 Perm 区大小,防止 Perm 扩容导致的 fgc
效果:调整参数后,服务不再有 Perm 区扩容导致的 STW GC 发生
https://tech.meituan.com/2017/12/29/jvm-optimize.html
解决方案:通过 -XX:SurvivorRatio 扩容 survivor 区
效果:调整参数后,再也没有切量后长暂停的问题了,Young GC 稳定在 30-100ms,成功解决
https://segmentfault.com/a/1190000044467479
解决方案:调整参数 -XX:+ParallelRefProcEnabled、-XX:SoftRefLRUPolicyMSPerMB=0,并优化代码减少 Finialize 对象。
效果:mixed gc 稳定到 85ms,耗时大幅降低
https://zhuanlan.zhihu.com/p/463122674
解决方案:增大 GC 的速度,使其超过垃圾增长的速度:让G1更早得启动混合式垃圾收集周期,调小 -XX:InitiatingHeapOccupancyPercent;增加每次混合式垃圾收集收集的Old分区数量,调整-XX:G1MixedGCCountTarget;
https://zhuanlan.zhihu.com/p/79643783
解决方案:调大 heapRegionSize
效果:将 Region 大小调到 16M 或者 32M 时,GC 情况会有明显的好转
https://www.jianshu.com/p/0955113a12db
结论:只有极少部分【managable】的参数才可以被热修改。这部分参数大多不是我们 GC 调优常用的,因此 GC 目前还是依赖冷调优。
jinfo 命令可以实现部分参数热修改。且只有被标记为【managable】的参数才可以被热修改image-20240401164237934
比如 JDK8 下,我们可以通过 jinfo 命令热修改 -XX:PrintGC 参数
jinfo -flag +PrintGC pid
所有的 JVM 中 manageable 的参数可以从 openjdk 源码(openjdk ./hotspot/src/share/vm/runtime/globals.hpp)中获取
image-20240401164300666
也可以通过 console 命令java -XX:+PrintFlagsFinal -version | grep "manageable" 获得:
JDK8:
image-20240401164311823
JDK11:
image-20240401164328950
JDK17:
image-20240401164337903
所有发行商的 JDK 都是基于开源的 openjdk 进行修改,因此在 VM options 上都保证了向后兼容,可以放心使用。
👉 使用 oracle、openjdk、correto 三款最常用的 JDK 进行验证,常用的参数均能 work
结论:如果 -XX:MaxGCPauseMillis 和 -XX:GCTimeRatio 设置产生的效果发生矛盾,会一定程度影响 GC 的表现
实验数据敏感,在此省略。
AlwaysPreTouch 对 IoTDB 启动性能影响的实验:
结论:
https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/index.html
https://www.oracle.com/technical-resources/articles/java/g1gc.html
https://www.oracle.com/webfolder/technetwork/tutorials/obe/java/G1GettingStarted/index.html
https://docs.oracle.com/en/java/javase/17/
https://docs.oracle.com/en/java/javase/11/
https://docs.oracle.com/javase/8/
https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html#BABFAFAE
https://masteranyfield.com/2021/05/13/gcs-of-jvm-tuning-pspo-vs-cms-vs-g1/
https://www.oracle.com/technical-resources/articles/javame/garbagecollection2.html
https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/parallel.html
https://xie.infoq.cn/article/317ccdfabd7694a093925f7fe
https://sematext.com/blog/java-garbage-collection-tuning/#starting-gc-tuning
https://zhuanlan.zhihu.com/p/161710652?spm=a2c6h.12873639.article-detail.15.7a8b1da3cPF1W0
https://developer.aliyun.com/article/978505
https://zhuanlan.zhihu.com/p/111886882
https://www.cnblogs.com/senlinyang/p/8717611.html
https://www.cnblogs.com/lindev/articles/15467748.html
https://blog.csdn.net/feiyanaffection/article/details/124019544
https://www.cnblogs.com/hongdada/p/14578950.html
https://zhuanlan.zhihu.com/p/628682897
https://juejin.cn/post/7068858769706319879
https://segmentfault.com/a/1190000039411521
https://hesey.wang/2018/11/gc-oriented-java-programming.html#comments