Java 并行 GC:不必要的 Full GC
Java parallel GC: unnecessary Full GC
我有一项服务可以从源读取数据,对数据执行一些转换,然后将转换后的内容上传到目标位置。在选择 GC 算法时,我正在寻找具有高吞吐量的算法,这就是我选择并行 GC 的原因。让我很困惑的部分是为什么我看到了大量的 Full GC。服务的性质使得大多数对象随着数据的来来去去而短暂存在。这是我的 GC 配置:
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintTenuringDistribution -verbose:GC -XX:+UseParallelGC -XX:NewSize=21200m -XX:MaxNewSize=21200m -server -Xms31200m -Xmx31200m
基本上,我将总堆大小设置为 30GB,新生代大小设置为 20GB。
这是一段 GC 日志:
2020-11-08T08:31:07.863+0000: 215.876: [Full GC (Ergonomics) [PSYoungGen: 1233347K->0K(18729472K)] [ParOldGen: 9065862K->6633660K(10240000K)] 10299209K->6633660K(28969472K), [Metaspace: 107588K->107588K(1144832K)], 1.1350824 secs] [Times: user=21.03 sys=0.00, real=1.14 secs]
2020-11-08T08:31:10.627+0000: 218.640: [GC (GCLocker Initiated GC)
Desired survivor size 2699034624 bytes, new threshold 1 (max 15)
[PSYoungGen: 15874560K->1274938K(19073024K)] 22513996K->7914375K(29313024K), 0.1073842 secs] [Times: user=3.10 sys=0.00, real=0.11 secs]
2020-11-08T08:31:12.319+0000: 220.331: [GC (GCLocker Initiated GC)
Desired survivor size 2587885568 bytes, new threshold 1 (max 15)
[PSYoungGen: 17602106K->1307000K(18962944K)] 24253865K->8618788K(29202944K), 0.2492961 secs] [Times: user=7.16 sys=0.00, real=0.25 secs]
2020-11-08T08:31:14.197+0000: 222.210: [GC (GCLocker Initiated GC)
Desired survivor size 2480930816 bytes, new threshold 1 (max 15)
[PSYoungGen: 17634168K->1333816K(19286016K)] 24952891K->9297010K(29526016K), 0.2524904 secs] [Times: user=7.07 sys=0.00, real=0.25 secs]
2020-11-08T08:31:16.165+0000: 224.178: [GC (GCLocker Initiated GC)
Desired survivor size 2386558976 bytes, new threshold 1 (max 15)
[PSYoungGen: 18092600K->1313137K(19181568K)] 26062932K->9992006K(29421568K), 0.2845171 secs] [Times: user=7.85 sys=0.00, real=0.29 secs]
2020-11-08T08:31:18.084+0000: 226.096: [GC (GCLocker Initiated GC)
Desired survivor size 2312110080 bytes, new threshold 1 (max 15)
[PSYoungGen: 18071921K->1242981K(19450880K)] 26751020K->10584632K(29690880K), 0.2523254 secs] [Times: user=6.79 sys=0.00, real=0.26 secs]
2020-11-08T08:31:18.336+0000: 226.349: [Full GC (Ergonomics) [PSYoungGen: 1242981K->0K(19450880K)] [ParOldGen: 9341651K->6896991K(10240000K)] 10584632K->6896991K(29690880K), [Metaspace: 107625K->107625K(1144832K)], 1.0198299 secs] [Times: user=18.34 sys=0.08, real=1.02 secs]
2020-11-08T08:31:21.049+0000: 229.062: [GC (GCLocker Initiated GC)
Desired survivor size 2221408256 bytes, new threshold 1 (max 15)
[PSYoungGen: 17120256K->1356565K(19378176K)] 24043241K->8279559K(29618176K), 0.1089915 secs] [Times: user=3.38 sys=0.00, real=0.11 secs]
2020-11-08T08:31:22.887+0000: 230.899: [GC (GCLocker Initiated GC)
Desired survivor size 2155872256 bytes, new threshold 1 (max 15)
[PSYoungGen: 18476821K->1265473K(19603456K)] 25426058K->8896652K(29843456K), 0.2524566 secs] [Times: user=7.14 sys=0.00, real=0.25 secs]
2020-11-08T08:31:24.888+0000: 232.901: [GC (GCLocker Initiated GC)
Desired survivor size 2092433408 bytes, new threshold 1 (max 15)
[PSYoungGen: 18699585K->1388375K(19539456K)] 26345045K->9562491K(29779456K), 0.2113546 secs] [Times: user=5.59 sys=0.00, real=0.21 secs]
2020-11-08T08:31:26.819+0000: 234.832: [GC (GCLocker Initiated GC)
Desired survivor size 2030043136 bytes, new threshold 1 (max 15)
[PSYoungGen: 18822487K->1308016K(19726336K)] 27003840K->10002863K(29966336K), 0.2078162 secs] [Times: user=6.10 sys=0.00, real=0.21 secs]
2020-11-08T08:31:28.868+0000: 236.881: [GC (GCLocker Initiated GC)
Desired survivor size 2030043136 bytes, new threshold 1 (max 15)
[PSYoungGen: 18990960K->1521040K(19665408K)] 27712283K->10780549K(29905408K), 0.2373748 secs] [Times: user=6.60 sys=0.00, real=0.23 secs]
2020-11-08T08:31:29.106+0000: 237.119: [Full GC (Ergonomics) [PSYoungGen: 1521040K->0K(19665408K)] [ParOldGen: 9259509K->7378423K(10240000K)] 10780549K->7378423K(29905408K), [Metaspace: 107653K->107653K(1144832K)], 1.0809680 secs] [Times: user=20.55 sys=0.00, real=1.09 secs]
日志中有几件事让我很困惑:
- JVM 如何确定所需的幸存者大小?为什么大约是 2.5 GB?为什么它在每次软 GC 中都会发生一点变化?为什么老一代的总大小从未改变 (10240000K) 但年轻一代的总大小一直在变化?
- 为什么*新阈值总是1?将东西移到旧世代是不是太激进了?
- 在每次软 GC 之后,年轻一代很可能有大约 1.3GB 的数据,并且一些数据被移动到老一代。这导致 old gen 逐渐变满,Full GC 最终碰巧清理了 old gen。为什么每次 soft GC 都会将一部分数据移动到老年代?看来幸存者space够大了。
- 我可以做些什么来避免不必要的 Full GC,从而提高整体吞吐量?
好吧,简单的解释无法回答您的问题;
JVM 使用 -XX:SurvivorRatio
参数来定义幸存者生成大小。默认值为 -XX:SurvivorRatio=8
。这是一个比率,这个平均幸存者 space 是伊甸园 space 大小的 one-eighth。对于你的情况,这会给你的幸存者 space 大小 - 1/8 * 20GB
。根据 this 文档,这通常对性能并不重要。由于您为年轻一代设置了固定的大尺寸,因此老一代保持不变。对 ParallelGC 使用 -XX:+UseAdaptiveSizePolicy
可能有助于调整 young/old 边界附近的大小。此外,年轻一代越大,GC minor collections 发生的频率就越低。似乎这些较小的 collection 就是你看到生存 space 略微收缩和增长的情况。
threshold
已由 JVM 选择用于 ParallelGC。根据这个 article,
If survivor spaces are too small, copying collection overflows
directly into the tenured generation. If survivor spaces are too
large, they will be uselessly empty. At each garbage collection, the
virtual machine chooses a threshold number, which is the number of times
an object can be copied before it is tenured. This threshold is chosen
to keep the survivors half full.
这似乎是一种攻击性行为。但是次要的 collection 周期明显不同,看来,如果需要的阈值也可以更改为 15。
如果一些 objects 在新生代中生活了期望数量的垃圾 collection 周期,作为 ParallelGC 设计,他们注定要移动到老年代。你不能保证,年轻代有多大,long survived objects永远留在年轻代。年轻一代是为了快速分配和释放objects,而不是为了长期生存objects。所以,正如你所观察到的,老年代最终会被填满并清理干净。
假设您正在使用 Java 8 或更高版本,为了提高程序的吞吐量,我会说,使用 G1GC instead of ParallelGC. Since your heap is significantly large, G1GC would be the ideal choice. G1GC algorithm designed to execute on very large terra byte (TB) heap spaces with minimal pause time. G1GC recommend to use on heaps larger than 6GB (Garbage First Garbage Collector Tuning)。使用 G1GC 时,如果您的程序使用大型 String
objects,-XX:+UseStringDeduplication
将是一个很大的帮助。该GC将整个堆space划分为多个小区域,并使用并行和并发线程执行collection进程。
那里还有另外两个实验性 GC (ZGC and Shenandoah),分别以 Java 11 和 Java 12 发布。这些 GC 使用更多垃圾显着减少了暂停时间 collection.
更新:
ZGC 和 Shenandoah 稳定版随 Java 15 于 2020 年 9 月发布。
我将只解决第 4 点,因为 Sachith 的回答涉及前 3 个。您选择了一个 GC,该 GC 在真正需要之前不会执行旧代(或完整)gc。 Full gc 是最昂贵的,cpu 周期专门用于您的工作。使用并发 gc:s 将删除部分或全部完整 gc:s 但您将失去 cpu 周期。因此,并不能保证并发 gc 实际上会更快。
另外,从你的旗帜 -Xms31200m -Xmx31200m
。您将堆的最小和最大大小设置为相同,这意味着 VM 不会在堆上执行任何人体工程学(适应)。
根据您的应用程序性能的重要性以及您拥有不错的测试环境,我建议测试不同的 gc:s 并查看您获得的性能。除了最大堆外,我还会对所有内容使用出厂设置,看看你能走多远。
我有一项服务可以从源读取数据,对数据执行一些转换,然后将转换后的内容上传到目标位置。在选择 GC 算法时,我正在寻找具有高吞吐量的算法,这就是我选择并行 GC 的原因。让我很困惑的部分是为什么我看到了大量的 Full GC。服务的性质使得大多数对象随着数据的来来去去而短暂存在。这是我的 GC 配置:
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintTenuringDistribution -verbose:GC -XX:+UseParallelGC -XX:NewSize=21200m -XX:MaxNewSize=21200m -server -Xms31200m -Xmx31200m
基本上,我将总堆大小设置为 30GB,新生代大小设置为 20GB。
这是一段 GC 日志:
2020-11-08T08:31:07.863+0000: 215.876: [Full GC (Ergonomics) [PSYoungGen: 1233347K->0K(18729472K)] [ParOldGen: 9065862K->6633660K(10240000K)] 10299209K->6633660K(28969472K), [Metaspace: 107588K->107588K(1144832K)], 1.1350824 secs] [Times: user=21.03 sys=0.00, real=1.14 secs]
2020-11-08T08:31:10.627+0000: 218.640: [GC (GCLocker Initiated GC)
Desired survivor size 2699034624 bytes, new threshold 1 (max 15)
[PSYoungGen: 15874560K->1274938K(19073024K)] 22513996K->7914375K(29313024K), 0.1073842 secs] [Times: user=3.10 sys=0.00, real=0.11 secs]
2020-11-08T08:31:12.319+0000: 220.331: [GC (GCLocker Initiated GC)
Desired survivor size 2587885568 bytes, new threshold 1 (max 15)
[PSYoungGen: 17602106K->1307000K(18962944K)] 24253865K->8618788K(29202944K), 0.2492961 secs] [Times: user=7.16 sys=0.00, real=0.25 secs]
2020-11-08T08:31:14.197+0000: 222.210: [GC (GCLocker Initiated GC)
Desired survivor size 2480930816 bytes, new threshold 1 (max 15)
[PSYoungGen: 17634168K->1333816K(19286016K)] 24952891K->9297010K(29526016K), 0.2524904 secs] [Times: user=7.07 sys=0.00, real=0.25 secs]
2020-11-08T08:31:16.165+0000: 224.178: [GC (GCLocker Initiated GC)
Desired survivor size 2386558976 bytes, new threshold 1 (max 15)
[PSYoungGen: 18092600K->1313137K(19181568K)] 26062932K->9992006K(29421568K), 0.2845171 secs] [Times: user=7.85 sys=0.00, real=0.29 secs]
2020-11-08T08:31:18.084+0000: 226.096: [GC (GCLocker Initiated GC)
Desired survivor size 2312110080 bytes, new threshold 1 (max 15)
[PSYoungGen: 18071921K->1242981K(19450880K)] 26751020K->10584632K(29690880K), 0.2523254 secs] [Times: user=6.79 sys=0.00, real=0.26 secs]
2020-11-08T08:31:18.336+0000: 226.349: [Full GC (Ergonomics) [PSYoungGen: 1242981K->0K(19450880K)] [ParOldGen: 9341651K->6896991K(10240000K)] 10584632K->6896991K(29690880K), [Metaspace: 107625K->107625K(1144832K)], 1.0198299 secs] [Times: user=18.34 sys=0.08, real=1.02 secs]
2020-11-08T08:31:21.049+0000: 229.062: [GC (GCLocker Initiated GC)
Desired survivor size 2221408256 bytes, new threshold 1 (max 15)
[PSYoungGen: 17120256K->1356565K(19378176K)] 24043241K->8279559K(29618176K), 0.1089915 secs] [Times: user=3.38 sys=0.00, real=0.11 secs]
2020-11-08T08:31:22.887+0000: 230.899: [GC (GCLocker Initiated GC)
Desired survivor size 2155872256 bytes, new threshold 1 (max 15)
[PSYoungGen: 18476821K->1265473K(19603456K)] 25426058K->8896652K(29843456K), 0.2524566 secs] [Times: user=7.14 sys=0.00, real=0.25 secs]
2020-11-08T08:31:24.888+0000: 232.901: [GC (GCLocker Initiated GC)
Desired survivor size 2092433408 bytes, new threshold 1 (max 15)
[PSYoungGen: 18699585K->1388375K(19539456K)] 26345045K->9562491K(29779456K), 0.2113546 secs] [Times: user=5.59 sys=0.00, real=0.21 secs]
2020-11-08T08:31:26.819+0000: 234.832: [GC (GCLocker Initiated GC)
Desired survivor size 2030043136 bytes, new threshold 1 (max 15)
[PSYoungGen: 18822487K->1308016K(19726336K)] 27003840K->10002863K(29966336K), 0.2078162 secs] [Times: user=6.10 sys=0.00, real=0.21 secs]
2020-11-08T08:31:28.868+0000: 236.881: [GC (GCLocker Initiated GC)
Desired survivor size 2030043136 bytes, new threshold 1 (max 15)
[PSYoungGen: 18990960K->1521040K(19665408K)] 27712283K->10780549K(29905408K), 0.2373748 secs] [Times: user=6.60 sys=0.00, real=0.23 secs]
2020-11-08T08:31:29.106+0000: 237.119: [Full GC (Ergonomics) [PSYoungGen: 1521040K->0K(19665408K)] [ParOldGen: 9259509K->7378423K(10240000K)] 10780549K->7378423K(29905408K), [Metaspace: 107653K->107653K(1144832K)], 1.0809680 secs] [Times: user=20.55 sys=0.00, real=1.09 secs]
日志中有几件事让我很困惑:
- JVM 如何确定所需的幸存者大小?为什么大约是 2.5 GB?为什么它在每次软 GC 中都会发生一点变化?为什么老一代的总大小从未改变 (10240000K) 但年轻一代的总大小一直在变化?
- 为什么*新阈值总是1?将东西移到旧世代是不是太激进了?
- 在每次软 GC 之后,年轻一代很可能有大约 1.3GB 的数据,并且一些数据被移动到老一代。这导致 old gen 逐渐变满,Full GC 最终碰巧清理了 old gen。为什么每次 soft GC 都会将一部分数据移动到老年代?看来幸存者space够大了。
- 我可以做些什么来避免不必要的 Full GC,从而提高整体吞吐量?
好吧,简单的解释无法回答您的问题;
JVM 使用
-XX:SurvivorRatio
参数来定义幸存者生成大小。默认值为-XX:SurvivorRatio=8
。这是一个比率,这个平均幸存者 space 是伊甸园 space 大小的 one-eighth。对于你的情况,这会给你的幸存者 space 大小 -1/8 * 20GB
。根据 this 文档,这通常对性能并不重要。由于您为年轻一代设置了固定的大尺寸,因此老一代保持不变。对 ParallelGC 使用-XX:+UseAdaptiveSizePolicy
可能有助于调整 young/old 边界附近的大小。此外,年轻一代越大,GC minor collections 发生的频率就越低。似乎这些较小的 collection 就是你看到生存 space 略微收缩和增长的情况。threshold
已由 JVM 选择用于 ParallelGC。根据这个 article,
If survivor spaces are too small, copying collection overflows directly into the tenured generation. If survivor spaces are too large, they will be uselessly empty. At each garbage collection, the virtual machine chooses a threshold number, which is the number of times an object can be copied before it is tenured. This threshold is chosen to keep the survivors half full.
这似乎是一种攻击性行为。但是次要的 collection 周期明显不同,看来,如果需要的阈值也可以更改为 15。
如果一些 objects 在新生代中生活了期望数量的垃圾 collection 周期,作为 ParallelGC 设计,他们注定要移动到老年代。你不能保证,年轻代有多大,long survived objects永远留在年轻代。年轻一代是为了快速分配和释放objects,而不是为了长期生存objects。所以,正如你所观察到的,老年代最终会被填满并清理干净。
假设您正在使用 Java 8 或更高版本,为了提高程序的吞吐量,我会说,使用 G1GC instead of ParallelGC. Since your heap is significantly large, G1GC would be the ideal choice. G1GC algorithm designed to execute on very large terra byte (TB) heap spaces with minimal pause time. G1GC recommend to use on heaps larger than 6GB (Garbage First Garbage Collector Tuning)。使用 G1GC 时,如果您的程序使用大型
String
objects,-XX:+UseStringDeduplication
将是一个很大的帮助。该GC将整个堆space划分为多个小区域,并使用并行和并发线程执行collection进程。
那里还有另外两个实验性 GC (ZGC and Shenandoah),分别以 Java 11 和 Java 12 发布。这些 GC 使用更多垃圾显着减少了暂停时间 collection.
更新: ZGC 和 Shenandoah 稳定版随 Java 15 于 2020 年 9 月发布。
我将只解决第 4 点,因为 Sachith 的回答涉及前 3 个。您选择了一个 GC,该 GC 在真正需要之前不会执行旧代(或完整)gc。 Full gc 是最昂贵的,cpu 周期专门用于您的工作。使用并发 gc:s 将删除部分或全部完整 gc:s 但您将失去 cpu 周期。因此,并不能保证并发 gc 实际上会更快。
另外,从你的旗帜 -Xms31200m -Xmx31200m
。您将堆的最小和最大大小设置为相同,这意味着 VM 不会在堆上执行任何人体工程学(适应)。
根据您的应用程序性能的重要性以及您拥有不错的测试环境,我建议测试不同的 gc:s 并查看您获得的性能。除了最大堆外,我还会对所有内容使用出厂设置,看看你能走多远。