如何解决Java并行GC:不必要的完整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的数据,并且有一些数据移至了老一代。这导致旧发电机逐渐变满,而Full GC最终发生了清理旧发电机的麻烦。为什么在每个软GC中将部分数据移至旧一代?幸存者空间似乎足够大。
- 如何避免不必要的Full GC,以提高整体吞吐量?
解决方法
我将只解决第4点,因为Sachith的答案是关于前3个的。您选择的GC直到真正需要时才执行旧的(或完整的)GC。 Full gc是最昂贵的,而CPU周期专用于您的工作。使用并发的gc:s会删除部分或全部的完整gc:s,但是您将失去cpu周期。因此,不能保证并发gc实际上会更快。
另外,从您的标志-Xms31200m -Xmx31200m
。您将堆的最小和最大大小设置为相同,这意味着VM将不会对堆执行任何人体工程学(调整)操作。
根据您的应用程序性能的重要性以及您是否拥有良好的测试环境,我建议您测试不同的gc:s并查看获得什么样的性能。除了最大堆以外,我还将所有其他设置都使用出厂设置,看看能达到多远。
好吧,仅凭简单的解释就无法回答问题;
-
JVM使用
-XX:SurvivorRatio
参数来定义幸存者的生成大小。其默认值为-XX:SurvivorRatio=8
。这是一个比率,平均生存空间是伊甸园空间的八分之一。对于您的情况,这给您的幸存者空间大小-1/8 * 20GB
。根据{{3}}文档,这通常对性能并不重要。由于您为年轻一代设置了固定的大尺寸,因此老一代保持不变。对ParallelGC使用-XX:+UseAdaptiveSizePolicy
可能有助于调整新旧边界周围的大小。同样,年轻一代较大,GC次要收藏发生的频率较低。似乎这些较小的收藏是您看到生存空间略有缩小和增长的情况。 -
threshold
已由JVM选择用于ParallelGC。按照此this,
如果幸存者空间太小,则复制集合会溢出 直接进入终身制。如果幸存者空间也是如此 大时,它们将是无用的空的。在每个垃圾收集中, 虚拟机选择阈值数,即次数 可以在使用权之前复制对象。选择此阈值 让幸存者半饱。
这似乎是一种攻击性行为。但是次要的收集周期相距很远,而且如果允许的阈值也可以更改为15,那么看来。
-
如果某些对象在年轻一代中生活了所需数量的垃圾回收周期(如ParallelGC设计),则它们注定要迁移到老一代。您无法保证,年轻一代有多大,长寿的物体将永远留在年轻一代中。年轻一代用于快速分配和解除分配对象,而不适用于寿命长的对象。因此,正如您所观察到的,最终老一辈人被填满并清理干净。
-
假设您使用Java 8或更高版本,以提高程序的吞吐量,我想说的是,使用article代替ParallelGC。由于堆非常大,因此G1GC将是理想的选择。 G1GC算法设计为以非常短的暂停时间在非常大的terra字节(TB)堆空间上执行。 G1GC建议在大于6GB(G1GC)的堆上使用。使用G1GC时,如果您的程序可以处理大型
-XX:+UseStringDeduplication
对象,那么String
会很有帮助。该GC将整个堆空间划分为多个小区域,并使用并行线程和并发线程执行收集过程。
还有另外两个实验性GC(Garbage First Garbage Collector Tuning和ZGC),它们分别随Java 11和Java 12发布。这些垃圾回收通过大量垃圾收集大大减少了暂停时间。
更新: ZGC和Shenandoah稳定版本随2020年9月发布的Java 15一起提供。
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。