关于生产环境改用G1垃圾收集器的思考

简介: 关于生产环境改用G1垃圾收集器的思考

背景


   由于我们的业务量非常大,响应延迟要求高。目前沿用的老的ParNew+CMS已经不能支撑业务的需求。平均一台机器在1个月内有1次秒级别的stop the world。对系统来说是个巨大的隐患。所以我们采用测试环境压测和逐渐在一些小的试点项目中生产环境引用G1来验证是否可以解决问题以及可能会引入的风险。

 

预备知识


   垃圾回收首先要判断一个对象是不是垃圾,Java里不用引用计数器算法,都是用从GC root开始的可达性分析算法,在实际实现的时候就是标记。所以不管是什么新生代、老年代回收,都有标记的步骤。因为目前市面上能见到的版本都是从分代垃圾收集器开始的,所以更原始的这里就不再提了。


1112728-20210404165522630-157887697.png


  上图中的Serial、ParNew、Parallel Scavenge都是年轻代算法,CMS、Serial Old、Parallel Old是老年代算法。直接连接的线之间才可以配合使用。一般年轻代和老年代的总空间比例是1:2。小的年轻代可以保证更快的进行Young GC。

 

   年轻代算法都是基于复制算法,准确的说是标记-复制算法。因为第一步是要先标记可达对象,然后把可达对象复制到一块空区域,再把原来的区域清空。区别只是Serial是串行的,Serial工作过程中用户线程都是停掉的。ParNew和Paralled Scavenge是并行的。所谓并行是指多个线程同时做垃圾收集的事情,但是仍然是要停下用户线程的工作的。Paralled Scavenge比ParNew的一个优势在于Paralled Scavenge可以设置自适应调节Eden与Survivor区的比例、晋升老年代的比例。

 

   Serial Old老年代算法采用的是标记整理算法,Paralled Old老年代算法采用的也是标记整理算法,不同点只是一个是完全串行的,Paralled Old垃圾回收的时候有多个线程来跑,但是不可以跟用户线程一起跑。但是不管年轻代、老年代,以及目前市面上的所有算法都不能避免STW(stop the world停止用户线程)。


   CMS是Concurrent Mark Sweep的缩写,就是并发标记清除算法,它与其他两种老年代算法不同的是它是只标记清除,不整理。目标是减少STW。上面的图中标记了CMS不能配合Paralled Scavenge使用,只能用ParNew。大家想想为啥咧。


   上面说了Paralled Scavenge的优势在于可以自动调节。而CMS是只清除操作,不整理。这种算法没有办法应对空间的变化。我看到的文章都没有对它们为何不能配合使用做解释。所以这里强调下。


CMS过程分为下面4步:


1112728-20210404165552749-1691719960.png


 上面4步中,初始标记和重新标记其实是一个东西执行两次,就是为了避免在并发标记过程中对象关系有变化。通常来讲STW引用线程的停顿时间:


Serial Old > Paralled Old > CMS。但是CMS有个致命的弱点,CMS必须要在老代码堆内存用尽之前完成垃圾回收,否则会触发担保机制,退化成Serial Old来垃圾回收,这时会造成较大的STW停顿。所以JDK1.8默认的垃圾收集器是Paralled Scavenge+Paralled Old方式。

 

G1垃圾回收


   G1的设计目标是为了替代CMS,它不存在退化为Serial的问题,声称STW时间不超过10ms。主要的特点如下:


1112728-20210404165628672-989663865.png


在15年16年的时候,很多公司都有使用G1的需求,但是那时候G1由于算法复杂,设计开发困难,所以还不成熟。在17年以后,已经被JDK9选为默认垃圾收集器。注意JDK8的默认垃圾收集器是Paralled Scanvenge,不采用CMS是因为CMS不稳定可能会退化成Serial Old。所以能被选为默认收集器说明它的稳定性是受官方认可的。


1112728-20210404165707567-116088673.png


G1的原理是分治法,将堆分成若干个等大的区域。优先回收垃圾多的区域。


1112728-20210404165734025-1041753294.png


但是划分的区域之间有可能有相互引用。所以引出了Card Table和Rememberd Set的概念。Rememberd Set(RS)里存的是区域之间的引用。Card Table是把区域进一步细分。搜引用的时候只需要搜索很小的子区域。RS可以看成是一个哈希表,就是存引用关系的。是一种典型的空间换时间的做法。


1112728-20210404170019438-761111319.png


  对于每个区域使用的垃圾收集算法,实际上G1没有什么创新,年轻代还是并行拷贝,老年代主要采用并发标记配合增量压缩。算法方面也比较成熟了。


1112728-20210404170050112-1197131990.png


各种垃圾收集器的对比


1112728-20210404170214059-221512987.png


怎样选择合适自己业务的垃圾收集器


   从理论上,G1是为了替代CMS。我们这边的本质需求也是降低STW,也已经很成熟了。并发量大稳定性高的公司也在用。公司内部也有使用的经验。没有什么问题。


   那就从实际上试验一把看看实际运行是否符合预期,并且要测试对G1专门的参数做微调。特别是MaxGCPauseMillis这个参数,因为这个参数设置的是预期每次GC的最大停顿时间。如果设置的不合理,比如太小就会造成GC频繁。如果太大,业务响应时间会很长。


实际上我有用实际代码模拟,但是为了信息安全这里自己用demo来说明。


JVM参数设置为:


-Xms4096m  //最大堆设置


-Xmx4096m  //最小堆设置


-XX:+UseG1GC  //使用G1垃圾收集器


-XX:MaxGCPauseMillis=20 //最大GC停顿时间,默认是200ms,这里设置20ms


-XX:+PrintGCDetails //打印GC详情日志


-XX:+PrintStringTableStatistics //打印字符串常量、引用常量统计


-XX:+PrintSafepointStatistics  //打印停顿原因


-XX:+PrintGCApplicationStoppedTime //停顿时间输出到GC日志中


上面参数中除了堆大小设置、使用G1和设置预期最大停顿时间外都是便于观察的统计信息。设置好之后可以根据自己的业务构造合适的案例。调整参数观察效果,同时也需要用cms的结果做对比。


[GC pause (G1 Humongous Allocation) (young), 0.0021237 secs]
   [Parallel Time: 1.4 ms, GC Workers: 8]
      [GC Worker Start (ms): Min: 3885.1, Avg: 3885.5, Max: 3886.4, Diff: 1.3]
      [Ext Root Scanning (ms): Min: 0.0, Avg: 0.2, Max: 0.5, Diff: 0.5, Sum: 1.3]
      [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.0, Max: 0.4, Diff: 0.4, Sum: 0.4]
      [Object Copy (ms): Min: 0.0, Avg: 0.7, Max: 1.0, Diff: 1.0, Sum: 5.3]
      [Termination (ms): Min: 0.0, Avg: 0.0, Max: 0.1, Diff: 0.1, Sum: 0.3]
         [Termination Attempts: Min: 1, Avg: 6.5, Max: 14, Diff: 13, Sum: 52]
      [GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
      [GC Worker Total (ms): Min: 0.0, Avg: 0.9, Max: 1.3, Diff: 1.3, Sum: 7.4]
      [GC Worker End (ms): Min: 3886.4, Avg: 3886.4, Max: 3886.4, Diff: 0.0]
   [Code Root Fixup: 0.0 ms]
   [Code Root Purge: 0.0 ms]
   [Clear CT: 0.1 ms]
   [Other: 0.6 ms]
      [Choose CSet: 0.0 ms]
      [Ref Proc: 0.1 ms]
      [Ref Enq: 0.0 ms]
      [Redirty Cards: 0.1 ms]
      [Humongous Register: 0.2 ms]
      [Humongous Reclaim: 0.1 ms]
      [Free CSet: 0.0 ms]
   [Eden: 4096.0K(200.0M)->0.0B(202.0M) Survivors: 4096.0K->2048.0K Heap: 3405.5M(4096.0M)->3402.0M(4096.0M)]
 [Times: user=0.00 sys=0.00, real=0.00 secs]
Total time for which application threads were stopped: 0.0027839 seconds, Stopping threads took: 0.0000253 seconds
[Full GC (Allocation Failure)  3401M->3401M(4096M), 0.0487029 secs]
   [Eden: 0.0B(202.0M)->0.0B(204.0M) Survivors: 2048.0K->0.0B Heap: 3402.0M(4096.0M)->3401.4M(4096.0M)], [Metaspace: 5853K->5853K(1056768K)]
 [Times: user=0.06 sys=0.00, real=0.05 secs]
[Full GC (Allocation Failure)  3401M->3401M(4096M), 0.0376090 secs]
   [Eden: 0.0B(204.0M)->0.0B(204.0M) Survivors: 0.0B->0.0B Heap: 3401.4M(4096.0M)->3401.4M(4096.0M)], [Metaspace: 5853K->5849K(1056768K)]
 [Times: user=0.03 sys=0.00, real=0.04 secs]
Total time for which application threads were stopped: 0.0868891 seconds, Stopping threads took: 0.0000192 seconds
Heap
 garbage-first heap   total 4194304K, used 3483005K [0x00000006c0000000, 0x00000006c0204000, 0x00000007c0000000)
  region size 2048K, 1 young (2048K), 0 survivors (0K)
 Metaspace       used 5924K, capacity 6074K, committed 6144K, reserved 1056768K
  class space    used 687K, capacity 722K, committed 768K, reserved 1048576K
         vmop                    [threads: total initially_running wait_to_block]    [time: spin block sync cleanup vmop] page_trap_count
1.121: no vm operation                  [      12          0              1    ]      [     0     7     7     0     0    ]  0  
1.226: Deoptimize                       [      12          0              0    ]      [     0     0     0     0     0    ]  0  
1.326: Deoptimize                       [      12          0              0    ]      [     0     0     0     0     0    ]  0  
1.399: Deoptimize                       [      12          0              0    ]      [     0     0     0     0     0    ]  0  
1.497: Deoptimize                       [      12          0              0    ]      [     0     0     0     0     0    ]  0  
2.409: G1IncCollectionPause             [      12          0              0    ]      [     0     0     0     0     4    ]  0  
2.417: CGC_Operation                    [      12          0              1    ]      [     0     1     1     0     5    ]  0  
2.425: CGC_Operation                    [      12          0              1    ]      [     0     4     4     0     1    ]  0  
3.431: no vm operation                  [      12          0              1    ]      [     0    17    17     0     0    ]  0  
3.885: G1IncCollectionPause             [      12          0              0    ]      [     0     0     0     0     2    ]  0  
3.888: G1CollectForAllocation           [      12          0              0    ]      [     0     0     0     0    86    ]  0  
4.037: Exit                             [      12          0              1    ]      [     0     0     0     0   339    ]  0  
Polling page always armed
Deoptimize                         4
CGC_Operation                      2
G1CollectForAllocation             1
G1IncCollectionPause               2
Exit                               1
    0 VM operations coalesced during safepoint
Maximum sync time     17 ms
Maximum vm operation time (except for Exit VM operation)     86 ms
SymbolTable statistics:
Number of buckets       :     20011 =    160088 bytes, avg   8.000
Number of entries       :     22112 =    530688 bytes, avg  24.000
Number of literals      :     22112 =    932040 bytes, avg  42.151
Total footprint         :           =   1622816 bytes
Average bucket size     :     1.105
Variance of bucket size :     1.111
Std. dev. of bucket size:     1.054
Maximum bucket size     :         8
StringTable statistics:
Number of buckets       :     60013 =    480104 bytes, avg   8.000
Number of entries       :      2944 =     70656 bytes, avg  24.000
Number of literals      :      2944 =    238320 bytes, avg  80.951
Total footprint         :           =    789080 bytes
Average bucket size     :     0.049
Variance of bucket size :     0.049
Std. dev. of bucket size:     0.222
Maximum bucket size     :         3


 G1日志会打印GC的详细过程,便于观察分析。PrintStringTableStatistics这个JVM参数打印出字符串常量信息,在JDK7之后,字符串常量从永久代被移到堆内存中了,所以也会影响GC。

 

   建议做完调优之后,再用优化后的参数重跑用例,并jvisualvm这个jdk自带工具观察一段时间的GC情况。

 

总结


   我总结是否采用一个工具或技术,常规思路是这样:


  1. 明确目标。这里的目标就是要降低STW造成的延迟。


  1. 调查学习。要理解原理、优缺点,多个技术之间对比。


  1. 测试验证。至少要用试验报告的形式给出测试过程和结论。


  1. 做出调整。根据测试结果做出可能的是大局上的调整,比如和目前的系统不兼容,或者是细节调整比如修改参数。


   上面这个思路也就是完整的PDCA的过程。


1112728-20210404170252041-788172215.png


一句话总结就是:目标先行,回绕目标来做事。

相关文章
|
1月前
|
Arthas 监控 Java
JVM知识体系学习七:了解JVM常用命令行参数、GC日志详解、调优三大方面(JVM规划和预调优、优化JVM环境、JVM运行出现的各种问题)、Arthas
这篇文章全面介绍了JVM的命令行参数、GC日志分析以及性能调优的各个方面,包括监控工具使用和实际案例分析。
43 3
|
3月前
|
JavaScript Java
hyengine垃圾回收问题之过程卡顿如何解决
hyengine垃圾回收问题之过程卡顿如何解决
|
4月前
|
Java BI 运维
开发与运维配置问题之升级机器配置后出现频繁的GC问题和超长的GC时间如何解决
开发与运维配置问题之升级机器配置后出现频繁的GC问题和超长的GC时间如何解决
34 1
|
4月前
|
算法 Java UED
开发与运维内存问题之Serial Old收集器用途如何解决
开发与运维内存问题之Serial Old收集器用途如何解决
19 0
|
4月前
|
Java 运维
开发与运维内存问题之触发Full GC,类加载检查如何解决
开发与运维内存问题之触发Full GC,类加载检查如何解决
32 0
|
5月前
|
Java
Java垃圾回收器:版本差异、使用技巧与最佳实践
Java垃圾回收器:版本差异、使用技巧与最佳实践
133 1
|
5月前
|
运维 Java Shell
手工触发Full GC:JVM调优实战指南
本文是关于Java应用性能调优的指南,重点介绍了如何使用`jmap`工具手动触发Full GC。Full GC是对堆内存全面清理的过程,通常在资源紧张时进行以缓解内存压力。文章详细阐述了Full GC的概念,并提供了两种使用`jmap`触发Full GC的方法:通过`-histo:live`选项获取存活对象统计信息,或使用`-dump`选项生成堆转储文件以分析内存状态。同时,文中也提醒注意手动Full GC可能带来的性能开销,建议在生产环境中谨慎操作。
1549 1
|
6月前
|
存储 算法 Java
工作5年,我竟发现JVM只用这4个技巧就可以轻松调优
Java虚拟机中,数据类型可以分为两类:基本类型和引用类型。基本类型的变量保存原始值,即:他代表的值就是数值本身;而引用类型的变量保存引用值。“引用值”代表了某个对象的引用,而不是对象本身,对象本身存放在这个引用值所表示的地址的位置。
32 0
|
6月前
|
Java
jvm性能调优 - 07线上应用部署JVM实战_栈内存与永久代预估与设置
jvm性能调优 - 07线上应用部署JVM实战_栈内存与永久代预估与设置
109 0
|
SQL canal 运维
JVM第六讲:线上环境 FGC 频繁,如何解决?
JVM第六讲:线上环境 FGC 频繁,如何解决?
293 0