背景

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

预备知识

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

上图中的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步:

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

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

G1垃圾回收

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

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

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

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

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

各种垃圾收集器的对比

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

从理论上,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造成的延迟。

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

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

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

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

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

相关阅读

JAVA SPI(Service Provider Interface)原理、设计及源码解析

专治不会看源码的毛病--spring源码解析AOP篇

线上问题排查的四类方法

稳定性的海因里希法则

关于生产环境改用G1垃圾收集器的思考的更多相关文章

  1. G1 垃圾收集器入门

    最近在复习Java GC,因为G1比较新,JDK1.7才正式引入,比较艰难的找到一篇写的很棒的文章,粘过来mark下.总结这篇文章和其他的资料,G1可以基本稳定在0.5s到1s左右的延迟,但是并不能保 ...

  2. 转 G1垃圾收集器入门

    转自:http://blog.csdn.net/zhanggang807/article/details/45956325 最近在复习Java GC,因为G1比较新,JDK1.7才正式引入,比较艰难的 ...

  3. G1垃圾收集器入门-原创译文

    G1垃圾收集器入门-原创译文 原文地址 Getting Started with the G1 Garbage Collector 概览 目的 本文介绍了如何使用G1垃圾收集器以及如何与Hotspot ...

  4. G1 垃圾收集器架构和如何做到可预测的停顿(阿里)

    CMS垃圾回收机制 参考:图解 CMS 垃圾回收机制原理,-阿里面试题 CMS与G1的区别 参考:CMS收集器和G1收集器优缺点 写这篇文章是基于阿里面试官的一个问题:众所周期,G1跟其他的垃圾回收算 ...

  5. 转:详解G1垃圾收集器

    G1垃圾收集器入门 说明 concurrent: 并发, 多个线程协同做同一件事情(有状态) parallel: 并行, 多个线程各做各的事情(互相间无共享状态) 参考: What’s the dif ...

  6. java面试-G1垃圾收集器

    一.以前收集器的特点 年轻代和老年代是各自独立且连续的内存块 年轻代收集器使用 eden + S0 + S1 进行复制算法 老年代收集必须扫描整个老年代区域 都是以尽可能的少而快速地执行 GC 为设计 ...

  7. 13.G1垃圾收集器

    G1收集器是一款面向服务器的垃圾收集器,也是HotSpot在JVM上力推的垃圾收集器,并赋予取代CMS的使命.为什么对G1收集器给予如此高的期望呢?既然对G1收集器寄予了如此高的期望,那么他一定是有其 ...

  8. G1 垃圾收集器深入剖析(图文超详解)

    G1(Garbage First)垃圾收集器是目前垃圾回收技术最前沿的成果之一. G1 同 CMS 垃圾回收器一样,关注最小时延的垃圾回收器,适合大尺寸堆内存的垃圾收集.但是,G1 最大的特点是引入分 ...

  9. 深入理解 Java G1 垃圾收集器--转

    原文地址:http://blog.jobbole.com/109170/?utm_source=hao.jobbole.com&utm_medium=relatedArticle 本文首先简单 ...

随机推荐

  1. window.URL.createObjectURL

    window.URL.createObjectURL https://html5.xgqfrms.xyz/Canvas/safety-canvas.html var video = document. ...

  2. stackoverflow & xgqfrms

    stackoverflow & xgqfrms stackoverflow https://stackoverflow.com/users/5934465/xgqfrms https://st ...

  3. qt 向窗口发送消息,键盘输入事件

    #include <windows.h> #include <QtDebug> #include <locale> #include <tchar.h> ...

  4. 画一个PBN大角度飞越转弯保护区

      今天出太阳了,尽管街上的行人依旧很少,但心情开始不那么沉闷了.朋友圈里除了关注疫情的最新变化之外,很多人已经开始选择读书或是和家人一起渡过这个最漫长的春节假期.陕西广电网络春节期间所有点播节目一律 ...

  5. 前端监控SDK开发分享

    目录 前言 收集哪些数据 性能 错误 辅助信息 小结 客户端SDK(探针)相关原理和API Web 微信小程序 编写测试用例 单元测试 流程测试 提供Web环境的方式 Mock Web API的方式 ...

  6. Python3网络爬虫-- 使用代理,轮换使用各种IP访问

    # proxy_list 代理列表 run_times = 100000 for i in range(run_times): for item in proxy_list: proxies = { ...

  7. JDK源码阅读-DirectByteBuffer

    本文转载自JDK源码阅读-DirectByteBuffer 导语 在文章JDK源码阅读-ByteBuffer中,我们学习了ByteBuffer的设计.但是他是一个抽象类,真正的实现分为两类:HeapB ...

  8. 学习笔记——JVM性能调优之 jmap

    jmap jmap(JVM Memory Map)命令可生成head dump文件,还可查询finalize执行队列.Java堆和永久代的详细信息. 通过配置启动参数:-XX:+HeapDumpOnO ...

  9. 微信小程序:自定义组件

    为什么要学习自定义组件? 1.用上我自己的单词abc,我希望在页面中展示椭圆形的图片, 2.打开手机淘宝,假如现在要做一个企业级项目,里面有很多页面,首页存在导航模块,点击天猫,进入第二个页面,而第二 ...

  10. JS中try catch的用法

    在js中也可以使用try/catch语法,把可能发生异常的代码使用try包裹起来,然后在catch中对异常进行处理,处理后就不会影响后面代码的执行. const a = null try { cons ...