JVM G1 垃圾收集器及参数优化
- 1. JVM G1 垃圾收集器及参数优化
1. JVM G1 垃圾收集器及参数优化
1.1. 版本说明
软件 | 版本 |
---|---|
JDK | 1.8.0_402 |
1.2. HotSpot JVM 技术架构
- JVM 的主要组件包括类加载器(Class Loader Subsystem)、运行时数据区(Runtime Data Areas)和执行引擎(Execution Engine)。
- JVM 运行时数据区包括方法区(Method Area)、堆(Heap)、栈(Java Threads)、程序计数器(Program Counter Registers)、本地方法栈(Native Internal Threads)。
- 性能调优时重点关注堆(Heap)、JIT 编译器(JIT Compiler)、垃圾收集器(Garbage Collector)。
1.3. 性能指标
JVM 调优时,重点关注的是两个指标之一:响应能力或吞吐量。
1.3.1. 响应能力
响应能力指应用或系统对一个请求数据的响应有多快。如:
- 桌面 UI 对事件响应有多快。
- 网站返回页面有多快。
- 数据库查询的响应有多快。
1.3.2. 吞吐量
吞吐量注重应用或系统在一段时间内能完成的最大工作量。如:
- 在给定时间内完成的交易数量
- 批处理程序在一个小时内能处理的作业量。
- 数据库在一个小时内能处理的查询数量。
对于注重于吞吐量的应用而言,较长的暂停时间是可以接受的。由于高吞吐量的应用注重较长时间段的基准,因此不考虑快速响应时间。
1.4. G1 垃圾收集器介绍
G1 垃圾收集器是一种服务器式垃圾收集器,针对具有大内存的多处理器计算机。它尝试以高概率满足 GC 暂停时间目标,同时实现高吞吐量。全堆操作(例如全局标记)与应用程序线程同时执行。
1.5. G1 垃圾收集器适用场景
G1 的首要重点是为运行需要大堆且 GC 延迟有限的应用程序的用户提供解决方案。这意味着堆大小约为 6 GB 或更大,并且稳定且可预测的暂停时间低于 0.5 秒。
- 超过 50% 的 Java 堆被实时数据占用。
- 对象分配和升级的速率可能随时间的推移而发生显着变化。
- 堆中存在大量碎片。
- 可预测的暂停时间目标不超过几百毫秒,避免长时间的垃圾收集暂停。
1.6. G1 垃圾收集器下的堆内存
1.6.1. G1 垃圾收集器堆布局
G1 收集器将整个堆划分为多个大小相等的 Region,新生代和老年代不再是物理隔离的了,它们都是一部分 Region(不需要连续)的集合。
Region 大小由 JVM 在启动时确定。通常 JVM Region 目标数量为 2048 个,大小在 1Mb 到 32Mb 不等。
Region 大小计算公式:\(RegionSize=\frac{\frac{(Xmx+Xms)}{2}}{2048}\)
根据计算结果,Region 最大值为 32M,最小值为 1M,例如:指定-Xms4G -Xmx4G
则 Region 大小为2M
。
也可以通过-XX:G1HeapRegionSize
参数指定,数值必须为 2 的 n 次方且大小在 1 ~ 32 之间,例如:-XX:G1HeapRegionSize=4M
。针对存在大量大对象的应用可以适当调整 Region 大小,避免造成老年代空间碎片。
堆被划分为一组大小相等的 Region,每个 Region 都是连续的虚拟内存范围。某些 Region 集被分配与旧垃圾收集器中相同的角色(eden、survivor、old),但它们没有固定的大小。这为内存使用提供了更大的灵活性。
- 年轻代包含 Eden Region(红色)和 Survivor Region(红色带有“S”)。这些 Region 提供与其他收集器中各自连续空间相同的功能,不同之处在于,在 G1 中,这些 Region 通常在内存中以不连续的模式布局。
- 老年代包含 Old Region(浅蓝色)和 Humongous Region(带有“H”的浅蓝色)。
- 灰色部分则是堆中未使用的 Region。
注意:G1 的 Survivor 区是一个逻辑概念,不再像 CMS 等旧垃圾收集器那般分为 S0 和 S1 两个区,通过jstat -gc <PID>
命令也可以看到S0
永远为 0。
执行垃圾收集时,G1 的操作方式与 CMS 收集器类似。G1 执行并发全局标记阶段来确定整个堆中对象的活跃度。标记阶段完成后,G1 知道哪些 Region 大部分是空的。它首先在这些 Region 收集,这通常会产生大量的可用空间。这就是为什么这种垃圾收集方法被称为垃圾优先的原因。顾名思义,G1 将其收集和压缩活动集中在堆中可能充满可回收对象(即垃圾)的区域。G1 使用暂停预测模型来满足用户定义的暂停时间目标,并根据指定的暂停时间目标选择要收集的 Region 数量。
G1 识别为适合回收的 Region 通过疏散(复制或移动)进行垃圾收集。G1 将对象从堆的一个或多个 Region 复制到堆上的单个 Region,并在此过程中压缩并释放内存。疏散过程在多处理器上并行执行,以减少暂停时间并提高吞吐量。因此,每次垃圾收集时,G1 都会在用户定义的暂停时间内持续工作以减少碎片。这超越了前面两种垃圾收集器的能力。CMS 垃圾收集器不进行压缩。ParallelOld 垃圾收集器仅执行整个堆压缩,这会导致相当长的暂停时间。
需要注意的是,G1 不是实时收集器。它以高概率但不是绝对确定地满足设定的暂停时间目标。根据之前垃圾收集的数据,G1 预估在用户指定的目标时间内可以收集多少个 Region。因此,G1 拥有收集 Region 时间成本的相当准确的模型,它使用该模型来确定目标暂停时间内收集哪些 Region 以及多少个 Region。
注意:G1 具有并发(与应用程序线程一起运行,例如 refinement、marking、cleanup)和并行(多线程垃圾收集,例如 stop the world)阶段。Full GC 仍然是单线程的,但如果调整得当,应该避免 Full GC。
1.6.2. G1 额外内存空间占用
使用 G1 的 JVM 进程比 CMS 等旧垃圾收集器占用更多的内存空间,这与 G1 的 RSet 和 CSet 等核算数据结构有关。
Remembered Set(简称 RSet):跟踪指定 Region 中的对象引用。堆中的每个 Region 有一个 RSet。RSet 实现了 Region 的并行且独立的垃圾收集。RSet 的总体内存空间影响小于 5%。
Collection Set(简称 CSet):将在一次 GC 中被收集的 Region 集合。CSet 中的所有存活数据都会在 GC 期间被疏散(复制或移动)。Region 集可以是 Eden、Survivor 或老年代。CSet 对 JVM 内存空间的影响不到 1%。
1.6.3. G1 中的年轻代
堆被分为 2048 个区域。最小大小为 1Mb,最大大小为 32Mb。蓝色 Region 保存老年代对象,绿色 Region 保存年轻代对象。
1.6.4. G1 中的年轻代 GC
存活对象被疏散(即复制或移动)到一个或多个 Survivor Region。如果满足进入老年代阈值,则某些对象将被提升到 Old Region。
这是 Stop The World (STW) 级别暂停。
1.6.5. 年轻代 GC 结束
存活对象已被疏散(复制或移动)到 Servivor Region 或老年代 Region。
最近提升的对象以深蓝色表示。Servivor Region 以绿色表示。
综上所述,关于 G1 的年轻代可以这样说:
- 堆是划分为多个 Region 的单个内存空间。
- 年轻代内存由一组不连续的 Region 组成。这使得在需要时可以轻松调整大小。
- 年轻代垃圾收集(Young GC)是 Stop The World (STW) 级别事件。所有应用程序线程都会因该操作而暂停。
- 年轻代 GC 使用多个线程并行完成。
- 存活对象被复制到新的 Survivor Region 或老年代 Region。
1.6.6. G1 中的老年代
老年代包含 Old Region 和 Humongous Region。
经历多次年轻代 GC 的年轻代对象达到年轻阈值(-XX:MaxTenuringThreshold
)后会晋升到老年代。
老年代占堆内存的比例达到阈值(-XX:InitiatingHeapOccupancyPercent
)后,将触发并发标记周期。
大小超过 Region 50% 的对象定性为大对象,大对象专门存放在老年代跨越多个连续 Region 的 Humongous Region 中,StartsHumongous
标记一组 Humongous Region 开始,ContinuesHumongous
标记 Humongous Region 延续。
一个大对象单独占据一个 Humongous Region,因此 Humongous Region 末尾部分未使用空间将造成内存碎片,例如一个 Region 大小 2M,一个大对象大小 3M,那么大对象所在的 Humongous Region 大小为 4M,末尾将有 1M 内存碎片。
在分配 Humongous Region 之前,G1 将检查触发并发标记周期阈值(-XX:InitiatingHeapOccupancyPercent
),如果超过将启动并发标记周期。
JDK8u40 以后版本对 G1 做了优化,可以在年轻代 GC 时回收大对象。
因此,应尽量避免大对象产生或适当调整 Region 大小。
1.6.7. G1 中的老年代 GC
与 CMS 一样,G1 被设计为老年代对象的低暂停垃圾收集器。
G1 在堆的老年代执行以下阶段。请注意,某些阶段是年轻代 GC 的一部分。
阶段 | 描述 |
---|---|
1. 初始标记(Stop The World 事件) | 对于 G1,它搭载在正常的年轻代 GC 上。标记可能引用老年代对象的 Survivor Region(Root Region)。 |
2. Root Region 扫描 | 扫描 Survivor Region 以获取对老年代的引用。这个过程与应用线程并行进行。该阶段必须在年轻代 GC 发生之前完成。 |
3. 并发标记 | 在整个堆上查找存活对象。这个过程与应用线程并行进行。此阶段可能会被年轻代 GC 中断。 |
4. 重新标记(Stop The World 事件) | 使用称为开始快照 (SATB) 的算法完成堆中存活对象的标记,该算法比 CMS 收集器中使用的算法快得多。 |
5. 清理(Stop The World 以及并发事件) | 1. 核算存活对象和完全空闲的 Region(Stop The World)。 2. 清除 Rset(Stop The World)。3. 重置空 Region 并放回空闲列表(并行)。 |
*. 复制(Stop The World 事件) | 将存活对象疏散(复制或移动)到新的未使用的 Region 中。这可以通过年轻代 GC(GC 日志记录为GC pause (young) )或年轻代老年代混合 GC(GC 日志记录[GC Pause (mixed)])来完成 |
1.6.7.1. 初始标记阶段
存活对象的初始标记是在年轻代 GC 的基础上进行的。在 GC 日志记录GC pause (young)(inital-mark)
。
1.6.7.2. 并发标记阶段
如果发现有空 Region(图中标记为“X”),则在“重新标记”阶段立即将其清除回收。
并发标记可以被年轻代 GC 打断,GC 日志记录为:
2024-06-20T15:18:28.715+0800: 0.223: #2: [GC concurrent-mark-abort]
1.6.7.3. 重新标记阶段
空 Region 被清除并回收。同时计算所有 Region 的活跃度。
1.6.7.4. 复制/清理阶段
G1 选择“活跃度”最低的 Region,即可以最快收集的 Region。然后这些 Region 会在年轻 GC 同时收集。GC 日志记录为GC pause (mixed)
。所以年轻代和老年代都会同时被收集。
1.6.7.5. 复制/清理阶段之后
所选 Region 已被收集并压缩为图中所示的深蓝色区域和深绿色区域。
1.6.8. 老年代 GC 总结
综上所述,关于老年代的 G1 垃圾回收,我们可以提出几个关键点。
- 并发标记阶段
- Region 活跃度信息是在应用程序运行时并行计算的。
- 在疏散暂停期间活跃度信息可识别哪些是最适合回收的 Region。
- 没有像 CMS 那样的清除阶段。
- 重新标记阶段
- 使用开始快照 (SATB) 算法,该算法比 CMS 所使用的算法快得多。
- 完全空的 Region 被回收。
- 复制/清理阶段
年轻代和老年代同时被回收。
老年代 Region 是根据其活跃度来选择的。
一次完整的老年代 GC 日志:
2024-06-20T15:18:29.011+0800: 0.520: Application time: 0.0362232 seconds
2024-06-20T15:18:29.011+0800: 0.520: #11: [GC pause (G1 Evacuation Pause) (young) (initial-mark)
Desired survivor size 524288 bytes, new threshold 1 (max 15)
- age 1: 2092320 bytes, 2092320 total
, 0.0049683 secs]
[Parallel Time: 4.7 ms, GC Workers: 8]
[GC Worker Start (ms): Min: 519.6, Avg: 519.6, Max: 519.7, Diff: 0.1]
[Ext Root Scanning (ms): Min: 0.0, Avg: 0.1, Max: 0.2, Diff: 0.2, Sum: 0.5]
[Update RS (ms): Min: 0.2, Avg: 0.8, Max: 4.4, Diff: 4.2, Sum: 6.6]
[Processed Buffers: Min: 0, Avg: 1.8, Max: 5, Diff: 5, Sum: 14]
[Scan RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Object Copy (ms): Min: 0.1, Avg: 3.5, Max: 4.1, Diff: 4.0, Sum: 27.8]
[Termination (ms): Min: 0.0, Avg: 0.2, Max: 0.5, Diff: 0.5, Sum: 1.6]
[Termination Attempts: Min: 1, Avg: 426.2, Max: 594, Diff: 593, Sum: 3410]
[GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.1]
[GC Worker Total (ms): Min: 4.5, Avg: 4.6, Max: 4.7, Diff: 0.1, Sum: 36.7]
[GC Worker End (ms): Min: 524.2, Avg: 524.2, Max: 524.2, Diff: 0.0]
[Code Root Fixup: 0.0 ms]
[Code Root Purge: 0.0 ms]
[Clear CT: 0.0 ms]
[Other: 0.2 ms]
[Choose CSet: 0.0 ms]
[Ref Proc: 0.2 ms]
[Ref Enq: 0.0 ms]
[Redirty Cards: 0.0 ms]
[Humongous Register: 0.0 ms]
[Humongous Reclaim: 0.0 ms]
[Free CSet: 0.0 ms]
[Eden: 5120.0K(5120.0K)->0.0B(3072.0K) Survivors: 2048.0K->1024.0K Heap: 23179.8K(32768.0K)->21262.3K(32768.0K)]
[Times: user=0.03 sys=0.00, real=0.00 secs]
2024-06-20T15:18:29.016+0800: 0.525: Total time for which application threads were stopped: 0.0050872 seconds, Stopping threads took: 0.0000085 seconds
2024-06-20T15:18:29.016+0800: 0.525: #12: [GC concurrent-root-region-scan-start]
2024-06-20T15:18:29.017+0800: 0.525: #12: [GC concurrent-root-region-scan-end, 0.0003023 secs]
2024-06-20T15:18:29.017+0800: 0.525: #12: [GC concurrent-mark-start]
2024-06-20T15:18:29.018+0800: 0.526: #12: [GC concurrent-mark-end, 0.0011098 secs]
2024-06-20T15:18:29.018+0800: 0.526: Application time: 0.0014360 seconds
2024-06-20T15:18:29.018+0800: 0.526: #12: [GC remark 2024-06-20T15:18:29.018+0800: 0.526: #12: [Finalize Marking, 0.0001219 secs] 2024-06-20T15:18:29.018+0800: 0.526: #12: [GC ref-proc, 0.0000213 secs] 2024-06-20T15:18:29.018+0800: 0.526: #12: [Unloading, 0.0004393 secs], 0.0007042 secs]
[Times: user=0.00 sys=0.00, real=0.00 secs]
2024-06-20T15:18:29.019+0800: 0.527: Total time for which application threads were stopped: 0.0007692 seconds, Stopping threads took: 0.0000329 seconds
2024-06-20T15:18:29.019+0800: 0.527: Application time: 0.0000517 seconds
2024-06-20T15:18:29.019+0800: 0.527: #12: [GC cleanup 23864K->15000K(32768K), 0.0001437 secs]
[Times: user=0.00 sys=0.00, real=0.00 secs]
2024-06-20T15:18:29.019+0800: 0.527: Total time for which application threads were stopped: 0.0001866 seconds, Stopping threads took: 0.0000249 seconds
2024-06-20T15:18:29.019+0800: 0.527: #12: [GC concurrent-cleanup-start]
2024-06-20T15:18:29.019+0800: 0.527: #12: [GC concurrent-cleanup-end, 0.0000044 secs]
...略
2024-06-20T15:18:29.055+0800: 0.563: Application time: 0.0001703 seconds
2024-06-20T15:18:29.055+0800: 0.563: #14: [GC pause (G1 Evacuation Pause) (mixed)
Desired survivor size 524288 bytes, new threshold 15 (max 15)
- age 1: 395640 bytes, 395640 total
, 0.0040526 secs]
[Parallel Time: 3.8 ms, GC Workers: 8]
[GC Worker Start (ms): Min: 562.9, Avg: 563.0, Max: 563.3, Diff: 0.4]
[Ext Root Scanning (ms): Min: 0.0, Avg: 0.1, Max: 0.1, Diff: 0.1, Sum: 0.6]
[Update RS (ms): Min: 0.0, Avg: 0.1, Max: 0.3, Diff: 0.3, Sum: 0.7]
[Processed Buffers: Min: 0, Avg: 1.4, Max: 3, Diff: 3, Sum: 11]
[Scan RS (ms): Min: 0.0, Avg: 0.1, Max: 0.1, Diff: 0.1, Sum: 0.4]
[Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Object Copy (ms): Min: 3.1, Avg: 3.2, Max: 3.3, Diff: 0.2, Sum: 25.8]
[Termination (ms): Min: 0.0, Avg: 0.1, Max: 0.2, Diff: 0.2, Sum: 1.1]
[Termination Attempts: Min: 1, Avg: 408.9, Max: 544, Diff: 543, Sum: 3271]
[GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.1]
[GC Worker Total (ms): Min: 3.3, Avg: 3.6, Max: 3.7, Diff: 0.4, Sum: 28.7]
[GC Worker End (ms): Min: 566.6, Avg: 566.6, Max: 566.6, Diff: 0.0]
[Code Root Fixup: 0.0 ms]
[Code Root Purge: 0.0 ms]
[Clear CT: 0.0 ms]
[Other: 0.2 ms]
[Choose CSet: 0.0 ms]
[Ref Proc: 0.2 ms]
[Ref Enq: 0.0 ms]
[Redirty Cards: 0.0 ms]
[Humongous Register: 0.0 ms]
[Humongous Reclaim: 0.0 ms]
[Free CSet: 0.0 ms]
[Eden: 1024.0K(1024.0K)->0.0B(1024.0K) Survivors: 1024.0K->1024.0K Heap: 14305.8K(32768.0K)->10386.3K(32768.0K)]
[Times: user=0.02 sys=0.00, real=0.00 secs]
2024-06-20T15:18:29.059+0800: 0.567: Total time for which application threads were stopped: 0.0041505 seconds, Stopping threads took: 0.0000025 seconds
1.7. JVM 参数优化
1.7.1. 查看 JVM 默认参数
java -XX:+PrintFlagsInitial
1.7.2. 关键 JVM 参数
JVM 总体上可以分成三类:
-
:标准参数,比如 -verbose:gc 这类表示标准实现,所有的虚拟机都需要实现这些参数的功能,且向后兼容;-X
:非标准参数,默认 JVM 会实现这些参数的功能,但是不保证所有的 JVM 实现都满足,且不保证向后兼容;-XX
:非 Stable 参数,这些参数在不同的 JVM 上会有不同的实现,这些参数不推荐在生产环境中使用,以后很有可能会被取消,需要慎重使用;
JVM 参数类型:
- 布尔型参数:
-XX:+
表示打开,-XX:-
表示关闭。(比如-XX:+PrintGCDetails); - 数字型参数:通过
-XX:=
设定。数字可以是 m/M(兆字节),k/K(千字节),g/G(G 字节)。 - 字符行参数:通过
-XX:=
设定,通常用来指定一个文件,路径,或者一个命令列表。(比如-XX:HeapDumpPath=./java_pid.hprof)
-server
服务器模式下运行-Xms
用于设置 Java 虚拟机(JVM)启动时的初始堆内存大小。
示例:-Xms8G
。-Xmx
用于指定 JVM 可以使用的最大堆内存大小。
示例:-Xmx8G
。-Xss
用于指定线程栈大小,单位可以是 K、M 等,在使用递归场景可以适当调大该参数。
示例:-Xss2M
。-XX:MetaspaceSize
这个参数是初始化的 Metaspace 大小,该值越大触发 Metaspace GC 的时机就越晚。默认约 20.8 M。
示例:-XX:MetaspaceSize=256M
。-XX:MaxMetaspaceSize
这个参数用于限制 Metaspace 增长的上限,防止因为某些情况导致 Metaspace 无限的使用本地内存,影响到其他程序。默认无上限。
示例:-XX:MaxMetaspaceSize=256M
。-XX:NewRatio
老年代与年轻代的比例,默认值:2,即 \(\frac{老年代}{年轻代}=2\)-XX:SurvivorRatio
Eden 与 Survivor 的比例,默认值:8,即 \(\frac{Eden}{Survivor}=8\)-XX:MaxTenuringThreshold
存活对象晋升到老年代的年龄,经过一次年轻代 GC 对象年龄加 1。分代年龄存储在对象头中,占 4 位,因此分代年龄最多 15。默认值:15。-XX:+UseG1GC
启用 G1 (Garbage-First) 垃圾回收器。-XX:MaxGCPauseMillis
设置一个目标值,指定 G1 GC 努力达到的最大垃圾回收暂停时间。默认 200 毫秒。
示例:-XX:MaxGCPauseMillis=200
。-XX:InitiatingHeapOccupancyPercent
这个参数按 JDK 版本不同有不同的含义。
JDK8b12 之前版本:当堆内存(整个堆,包括年轻代 + 老年代)占用达到指定百分比时,触发并发 GC 周期。默认值:45。
JDK8b12 之后版本:当老年代内存占用达到整个堆指定百分比时,触发并发 GC 周期。默认值:45。
示例:-XX:InitiatingHeapOccupancyPercent=45
。-XX:ParallelGCThreads
设置并行垃圾回收器(Parallel Garbage Collector)使用的线程数。一般是在 Stop-the-World 阶段。最佳值取决于系统的硬件配置和应用程序的具体需求。如果设置的值过高,可能会导致 CPU 资源过载,从而影响应用程序的性能。通常,线程数可以设置为与可用处理器核心数相等的值。
示例:-XX:ParallelGCThreads=8
。-XX:ConcGCThreads
设置并发垃圾收集器线程数。一般是在并发标记阶段。-XX:G1ReservePercent
设置堆保留的内存百分比,作为假的上限,用于减少存活对象升级失败的可能性。默认值:10。-XX:G1HeapRegionSize
设置 Region 的大小,对于存在大对象的应用,可以适当调整 Region 大小,避免大对象进入老年代造成空间碎片。-XX:+DisableExplicitGC
用于禁用 System.gc() 方法的显式垃圾回收功能。开发者可能会调用 System.gc() 来尝试管理内存或者优化性能。然而,频繁的垃圾回收可能会导致应用程序的性能下降,因为垃圾回收是一个计算密集型的过程,它会暂停应用程序的执行来回收不再使用的对象。使用该参数避免了可能的性能问题。-Xloggc
启用此选项时,JVM 会将每次垃圾回收的详细信息输出到指定的文件中。GC 日志文件名可使用通配符,%t
表示日期时间;%p
表示进程ID。
示例:-Xloggc:logs/gc_%t_%p.log
。-XX:+UseGCLogFileRotation
启用垃圾收集(GC)日志文件的轮换功能。启用此功能后,当 GC 日志文件达到一定大小时,JVM 会自动创建新的日志文件,并将旧的日志文件进行轮换和归档,从而避免单个日志文件无限增长。-XX:GCLogFileSize
用于设置垃圾收集日志文件大小。设置该参数后,JVM会根据该参数值控制每个垃圾收集日志文件的大小,一旦达到设置的文件大小,JVM会自动创建一个新的文件,同时保留旧文件。
示例:-XX:GCLogFileSize=50M
。-XX:NumberOfGCLogFiles
设置 JVM 应该保留的 GC 日志文件的最大数量。
示例:-XX:NumberOfGCLogFiles=5
。-XX:+PrintGCDateStamps
用于在垃圾回收 (GC) 日志中添加时间戳。当启用此选项时,每次垃圾回收事件都会被记录,并且在记录中包含自 JVM 启动以来的绝对时间(以秒或毫秒为单位,取决于具体实现)-XX:+PrintGCApplicationConcurrentTime
打印应用程序的并发时间。应用程序的并发时间是指在垃圾收集过程中,应用程序的线程与 GC 线程同时执行的时间。-XX:+PrintGCApplicationStoppedTime
打印应用程序在垃圾收集期间的停顿时间。-XX:+PrintGCCause
打印导致垃圾收集发生的原因。-XX:+PrintGCID
打印每次垃圾收集事件的唯一标识符(ID)。-XX:+PrintGCDetails
启用此选项时,JVM 会在每次垃圾回收发生时,输出包含有关回收的详细数据的日志信息。-XX:+PrintTenuringDistribution
用于在每次垃圾回收事件后输出对象晋升老年代(Tenured Generation)的分布情况。帮助了解对象在年轻代(Young Generation)和老年代之间的年龄分布,从而对垃圾回收器的行为和内存管理策略进行分析和优化。-XX:+HeapDumpOnOutOfMemoryError
用于在发生内存溢出错误(OutOfMemoryError)时自动生成堆内存转储文件(heap dump)。这个文件包含了所有活跃对象的详细信息,可以用来分析内存使用情况,查找内存泄漏或识别占用大量内存的对象。-XX:HeapDumpPath
指定生成的堆转储文件(heap dump)的存储路径和文件名。
示例:-XX:HeapDumpPath=logs
。-XX:+PrintConcurrentLocks
打印 java.util.concurrent 的锁信息,在 SIGTERM 时触发。默认关闭,等价于运行 jstack -l-Dfile.encoding
设置文件的字符编码。选项设置的是系统属性 file.encoding 的值。这个属性不仅影响文件的读写操作,还可能影响其他与字符编码相关的操作,如网络通信、数据库连接等。
示例:-Dfile.encoding=UTF-8
。-Djava.security.egd
SecureRandom 在 java 各种组件中使用广泛,可以可靠的产生随机数。但在大量产生随机数的场景下,性能会较低。这时可以使用-Djava.security.egd=file:/dev/./urandom
加快随机数产生过程。
示例:-Djava.security.egd=file:/dev/./urandom
。
1.7.3. 最佳实践
MetaspaceSize
和MaxMetaspaceSize
设置一样大小,对于大部分项目 256M 即可。- 初始堆内存
-Xmx
和最大堆内存-Xms
设置一样大小。避免堆内存扩大或缩小导致的应用停顿。 - G1 垃圾收集器会根据
MaxGCPauseMillis(默认值:200)
参数设置的 GC 暂停时间目标在G1MaxNewSizePercent(默认值:60)
和G1NewSizePercent(默认值:5)
之间动态调整新生代占整个堆的百分比大小,因此不要使用MaxNewSize
、NewSize
、Xmn
等参数限定新生代大小,否则将无法达到MaxGCPauseMillis
设定的目标时间。 loggc
、UseGCLogFileRotation
、GCLogFileSize
、NumberOfGCLogFiles
、PrintGCDateStamps
、PrintGCApplicationConcurrentTime
、PrintGCApplicationStoppedTime
、PrintGCCause
、PrintGCID
、PrintGCDetails
、PrintTenuringDistribution
一起配合使用,尽可能打印详尽的日志,便于分析问题。- HeapDumpOnOutOfMemoryError、HeapDumpPath 一起配合使用。
- UseG1GC、MaxGCPauseMillis、ParallelGCThreads 一起配合使用。
- JDK8b12 以后的版本默认情况下,老年代已使用内存占整个堆内存 45% 时会触发并发 GC(Mixed GC),在一些吞吐量比较大的应用中,年轻代占整个堆的比例可能会自动扩容到 55% 以上直至最大值 60% 来满足暂停时间目标,这时老年代占整个堆的比例只有 45% 以下,那么无法触发并发 GC 周期,老年代使用率持续上升直到内存耗尽触发 Full GC,这时可以使用
InitiatingHeapOccupancyPercent
参数把触发并发 GC 周期的阈值调低一些,如 RocketMQ 官方推荐调低到 30%。参考 RocketMQ 官方文档:JVM/OS 配置。
1.7.4. 通用项目 JVM 参数
java -server \
-Xms8G \
-Xmx8G \
-Xss2M \
-XX:MetaspaceSize=256M \
-XX:MaxMetaspaceSize=256M \
-XX:+UseG1GC \
-XX:InitiatingHeapOccupancyPercent=30
-XX:+DisableExplicitGC \
-Xloggc:logs/gc_%t_%p.log \
-XX:+UseGCLogFileRotation \
-XX:GCLogFileSize=50M \
-XX:NumberOfGCLogFiles=5 \
-XX:+PrintGCDateStamps \
-XX:+PrintGCApplicationConcurrentTime \
-XX:+PrintGCApplicationStoppedTime \
-XX:+PrintGCCause \
-XX:+PrintGCID \
-XX:+PrintGCDetails \
-XX:+PrintTenuringDistribution \
-XX:MaxGCPauseMillis=200 \
-XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=logs \
-XX:+PrintConcurrentLocks \
-Dfile.encoding=UTF-8 \
-Djava.security.egd=file:/dev/./urandom \
-jar demo.jar
1.8. JVM 参数优化实例
1.8.1. 实例一:大对象直接进入老年代
示例代码:
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
/**
* -Xms1G
* -Xmx1G
* -Xss2M
* -XX:MetaspaceSize=128M
* -XX:MaxMetaspaceSize=128M
* -XX:G1HeapRegionSize=2M
* -XX:+UseG1GC
* -XX:+PrintGCDateStamps
* -XX:+PrintGCApplicationConcurrentTime
* -XX:+PrintGCApplicationStoppedTime
* -XX:+PrintGCCause
* -XX:+PrintGCID
* -XX:+PrintGCDetails
* -XX:+PrintTenuringDistribution
* -Xloggc:logs/gc_%t_%p.log
*/
public class HumongousObjectDemo {
public static void main(String[] args) {
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);
scheduledExecutorService.scheduleAtFixedRate(() -> {
generateHumongousObject();
}, 0L, 100L, TimeUnit.MILLISECONDS);
}
public static void generateHumongousObject() {
byte[] bytes = new byte[1024 * 1024];
}
}
Region 大小为 2M,每 0.1 秒产生一个大小为 1M 的大对象。
对象大小达到 Region 一半以上时,直接分配在老年代的 Humongous Region 中,由于大对象单独占据连续的一组 Region,Region 末尾部分将造成内存碎片。
通过jstat
可以看出年轻代大小不变,老年代不断增长。并且触发年轻代 GC。
% jstat -gcutil 86306 1s
S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
0.00 100.00 3.85 74.25 90.69 78.74 1 0.016 0 0.000 0.016
0.00 100.00 3.85 75.79 90.69 78.74 1 0.016 0 0.000 0.016
0.00 100.00 3.85 77.34 90.69 78.74 1 0.016 0 0.000 0.016
0.00 0.00 3.70 0.32 90.69 78.74 2 0.034 0 0.000 0.034
0.00 0.00 3.70 1.87 90.69 78.74 2 0.034 0 0.000 0.034
0.00 0.00 3.70 3.42 90.69 78.74 2 0.034 0 0.000 0.034
0.00 0.00 3.70 4.96 90.69 78.74 2 0.034 0 0.000 0.034
0.00 0.00 3.70 6.51 90.69 78.74 2 0.034 0 0.000 0.034
0.00 0.00 3.70 8.06 90.69 78.74 2 0.034 0 0.000 0.034
0.00 0.00 3.70 9.60 90.69 78.74 2 0.034 0 0.000 0.034
0.00 0.00 3.70 11.30 90.69 78.74 2 0.034 0 0.000 0.034
0.00 0.00 3.70 12.85 90.69 78.74 2 0.034 0 0.000 0.034
0.00 0.00 3.70 14.40 90.69 78.74 2 0.034 0 0.000 0.034
0.00 0.00 3.70 15.94 90.69 78.74 2 0.034 0 0.000 0.034
通过 GC 日志可以看到 Humongous Region 分配触发年轻代 GC(GC pause (G1 Humongous Allocation) (young)
),由于内存碎片的存在,虽然老年代有剩余内存空间,还是空间不足((to-space exhausted), 0.0172939 secs
)疏散失败([Evacuation Failure: 2.6 ms]
)。
2024-07-02T17:16:39.058+0800: 74.151: Application time: 50.9762927 seconds
2024-07-02T17:16:39.058+0800: 74.151: #2: [GC pause (G1 Humongous Allocation) (young)
Desired survivor size 4194304 bytes, new threshold 15 (max 15)
- age 1: 916752 bytes, 916752 total
(to-space exhausted), 0.0172939 secs]
[Parallel Time: 10.7 ms, GC Workers: 8]
[GC Worker Start (ms): Min: 74152.4, Avg: 74152.6, Max: 74152.9, Diff: 0.5]
[Ext Root Scanning (ms): Min: 0.0, Avg: 2.2, Max: 10.6, Diff: 10.5, Sum: 17.5]
[Update RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Processed Buffers: Min: 0, Avg: 0.0, Max: 0, Diff: 0, Sum: 0]
[Scan RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Code Root Scanning (ms): Min: 0.0, Avg: 0.9, Max: 7.0, Diff: 7.0, Sum: 7.0]
[Object Copy (ms): Min: 0.0, Avg: 1.9, Max: 7.7, Diff: 7.7, Sum: 15.1]
[Termination (ms): Min: 0.0, Avg: 5.4, Max: 10.1, Diff: 10.1, Sum: 42.9]
[Termination Attempts: Min: 1, Avg: 1.0, Max: 1, Diff: 0, Sum: 8]
[GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.2, Diff: 0.2, Sum: 0.3]
[GC Worker Total (ms): Min: 10.1, Avg: 10.3, Max: 10.6, Diff: 0.5, Sum: 82.8]
[GC Worker End (ms): Min: 74163.0, Avg: 74163.0, Max: 74163.0, Diff: 0.0]
[Code Root Fixup: 0.0 ms]
[Code Root Purge: 0.0 ms]
[Clear CT: 0.1 ms]
[Other: 6.5 ms]
[Evacuation Failure: 2.6 ms]
[Choose CSet: 0.0 ms]
[Ref Proc: 0.2 ms]
[Ref Enq: 0.0 ms]
[Redirty Cards: 0.2 ms]
[Humongous Register: 1.1 ms]
[Humongous Reclaim: 2.3 ms]
[Free CSet: 0.0 ms]
[Eden: 2048.0K(49152.0K)->0.0B(51200.0K) Survivors: 2048.0K->0.0B Heap: 767.2M(1024.0M)->2199.0K(1024.0M)]
[Times: user=0.03 sys=0.01, real=0.02 secs]
2024-07-02T17:16:39.076+0800: 74.169: Total time for which application threads were stopped: 0.0184152 seconds, Stopping threads took: 0.0000223 seconds
解决方案:
调整 Region 大小至 4M(-XX:G1HeapRegionSize=4M
),使大对象在 Eden 分区分配内存。
调整后通过jstat
命令查看 GC 情况,可以看到老年代不再增长,对象直接在 Eden 分区分配,由年轻代 GC 回收 Eden 分区内存。
观察 GC 日志也恢复正常。
% jstat -gcutil 96128 1s
S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
0.00 100.00 83.75 0.00 89.67 78.61 2 0.007 0 0.000 0.007
0.00 100.00 86.87 0.00 89.67 78.61 2 0.007 0 0.000 0.007
0.00 100.00 90.00 0.00 89.67 78.61 2 0.007 0 0.000 0.007
0.00 100.00 93.12 0.00 89.67 78.61 2 0.007 0 0.000 0.007
0.00 100.00 1.25 0.00 90.12 78.61 3 0.012 0 0.000 0.012
0.00 100.00 4.37 0.00 90.12 78.61 3 0.012 0 0.000 0.012
0.00 100.00 7.50 0.00 90.12 78.61 3 0.012 0 0.000 0.012
0.00 100.00 10.62 0.00 90.12 78.61 3 0.012 0 0.000 0.012
0.00 100.00 13.75 0.00 90.12 78.61 3 0.012 0 0.000 0.012
0.00 100.00 16.87 0.00 90.12 78.61 3 0.012 0 0.000 0.012
0.00 100.00 20.00 0.00 90.12 78.61 3 0.012 0 0.000 0.012
0.00 100.00 23.12 0.00 90.12 78.61 3 0.012 0 0.000 0.012
0.00 100.00 26.25 0.00 90.12 78.61 3 0.012 0 0.000 0.012
0.00 100.00 29.37 0.00 90.12 78.61 3 0.012 0 0.000 0.012
1.9. 参考资料
[1] Oracle 官方文档:Getting Started with the G1 Garbage Collector
[2] Oracle 官方文档:Garbage First Garbage Collector Tuning
[3] 官方文档竟然有坑!关于 G1 参数 InitiatingHeapOccupancyPercent 的正确认知
本文来自博客园,作者:Jason,转载请注明原文链接:https://www.cnblogs.com/jason207010/p/18215711