GC耗时高,原因竟是服务流量小?

最近,我们系统配置了GC耗时的监控,但配置上之后,系统会偶尔出现GC耗时大于1s的报警,排查花了一些力气,故在这里分享下。,我们系统分多个环境部署,出现GC长耗时的是俄罗斯环境,其它环境没有这个问题,这里比较奇怪的是,俄罗斯环境是流量最低的一个环境,而且大多数GC长耗时发生在深夜。,发现报警后,我立马查看了GC日志,如下: ,图片,日志中出现了to-space exhausted,经过一番了解,出现这个是由于g1在做gc时,都是先复制存活对象,再回收原region,当没有空闲空间复制存活对象时,就会出现to-space exhausted,而这种GC场景代价是非常大的。,同时,在这个gc发生之前,还发现了一些如下的日志。,图片,这里可以看到,系统在分配约30M+的大对象,难道是有代码频繁分配大对象导致的gc问题。,jdk在分配大对象时,会调用G1CollectedHeap::humongous_obj_allocate方法,而使用async-profiler可以很容易知道哪里调用了这个方法,如下:,将humongous.jfr文件用jmc打开,如下:,图片,根据调用栈我发现,这是我们的一个定时任务,它会定时调用一个接口获取配置信息并缓存它,而这个接口返回的数据量达14M之多。,难道是这个接口导致的gc问题?但这个定时任务调用也不频繁呀,5分钟才调用一次,不至于让gc忙不过来吧!另一个疑问是,这个定时任务在其它环境也会运行,而且其它环境的业务流量大得多,为什么其它环境没有问题?,至此,我也不确定是这个定时任务导致的问题,还是系统自身特点导致偶尔的高gc耗时。,由于我们有固定的上线日,于是我打算先优化产生大对象的代码,然后在上线前的期间试着优化一下jvm gc参数。,我们使用的是httpclient调用接口,调用接口时,代码会先将接口返回数据读取成String,然后再使用jackson把String转成map对象,简化如下:,要优化它也很简单,使用jackson的readValue有一个传入InputStream的重载方法,用它来读取json数据,而不是将其加载成一个大的String对象再读,如下:,注:这里面map从逻辑上来说是一个大对象,但对jvm来说,它只是很多个小对象然后用指针连接起来而已,大对象一般由大数组造成,大String之所以是大对象,是因为它里面有一个很大的char[]数组。,优化完代码后,我开始研究优化jvm gc参数了,我们使用的是jdk8,垃圾收集器是g1,为了理解g1的调优参数,又简单学习了下g1的关键概念。,g1是分region收集的,但region也分年轻代与老年代。,g1的gc分young gc与mixed gc,young gc用于收集年轻代region,mixed gc会收集年轻代与老年代region。,在mixed gc回收之前,需要先经历一个并发周期(Concurrent Cycles),用来标记各region的对象存活情况,让mixed gc可以判断出需要回收哪些region。,并发周期分为如下4个子阶段:a. 初始标记(initial marking)b. 并发标记(concurrent marking)c. 重新标记(remarking)d. 清理(clean up)需要注意的是,初始标记(initial marking)这一步是借助young gc完成的。,在g1中,young gc几乎没有什么可调参数,而mixed gc有一些,常见如下:,出现to-space exhausted会不会是mixed gc太慢了呢?于是我调整了如下参数:,于是我按照上面调整了jvm参数,可是第二天我发现还是有GC长耗时,次数也没有减少,看来问题根因和我调整的参数没有关系。,就这样,到了上线日,于是我上线了前面优化大对象的代码,一天后,我发现偶尔的GC长耗时竟然没有了!,问题就这样解决了!!!,然而我心里还是有一个大大的问号,其它环境同样有这个定时任务,一样的运行频率,jvm配置也全是一样的,为啥其它环境没有问题呢?其它环境业务流量还大一些!,为此,我搜索了大量关于g1大对象的文章,我发现大对象的分配与回收过程有点特殊,如下:,我忽然想到,莫非是俄罗斯环境流量太低,触发不了young gc,且并发周期又因为什么原因没执行,但定时任务又慢慢生成大对象将old region占满,导致了to-space exhausted?,于是,我打算写段代码试验一下,慢慢的只生成大对象,看g1会不会回收,如下:,图片,这个一个命令行交互程序,使用如下jvm参数启动它:,使用了1600M的堆,16M的region size,所以总共有100个region,jdk版本是1.8.0_222。通过在这个交互程序中输入gc 9437184 20 0,可以生成20个9M的大对象。,当我输入3次gc 9437184 20 0后,如下:我从gc日志中发现了一次由initial marking触发的young gc,说明g1启动了并发周期,之所以发生young gc,是因为initial marking是借助ygc执行的。,图片,紧接着,我发现了并发标记、重新标记与清理阶段的日志。,图片,然后我在jstat中发现老年代使用率降低了,因为young gc会回收大对象,所以老年代使用率降低也是正常的。,图片,当我又执行了2次gc 9437184 20 0后,使得堆占用率再次大于45%,我发现gc日志中出现如下内容:,图片,看字面意思是,由于mixed gc正在执行,没有再次启动并发周期。,可是,我在这种状态下等了好久,也没有看到mixed gc的日志出来,不是说mixed gc正在执行嚒,它一定是有什么问题!,于是,我又执行了好几次gc 9437184 20 0,我在gc日志中发现了to-space exhausted!,图片,图片,注:添加-XX:G1LogLevel=finest参数,才能输出Humongous Reclaimed这一项。,但是,大对象分区占了98个,堆占用率肯定超过了45%,为何一直没有再次启动并发周期呢?,感觉这可能是jdk的bug,于是我分别下了最新的jdk8u与jdk11u验证,发现问题在最新的jdk8u中依然存在,而jdk11u中则不存在,这应该就是JDK的Bug!于是我通过二分搜索法多次编译了不同版本的JDK,最终确定问题fix在jdk9_b93与jdk9_b94之间。,图片,并在jdk的bug库中,搜索到了相同描述的bug反馈,如下:,图片,https://bugs.openjdk.org/browse/JDK-8140689,Bug改动代码如下:,图片,大致瞄了下代码,可能理解得不完全正确,改动逻辑如下:,所以在使用JDK8时,像那种系统流量很小,新生代较大,又有定时任务不断产生大对象的场景,堆几乎必然会被慢慢占满,要解决这个问题,可参考如下处理:,考虑到我们负责的其它系统中,时不时就有一波大响应体的请求,也没法快速修改代码,于是我统一将-XX:G1MaxNewSizePercent减小到30%,经过观察,修改后GC频率有所增加,但暂停时间有所下降,这是符合期望的。,当我在jdk的bug库中搜索问题时,发现不少和G1大对象相关的优化,早期JDK(如JDK8)的G1实现可能在大对象回收上不太完善,所以写代码时要注意尽量少创建大对象,以回避这些隐性问题。,注:这之后又遇到了Update RS (ms)耗时高,竟也和大对象有关,添加-XX:-ReduceInitialCardMarks后解决,看来大对象是万恶之源啊

文章版权声明

 1 原创文章作者:cmcc,如若转载,请注明出处: https://www.52hwl.com/17902.html

 2 温馨提示:软件侵权请联系469472785#qq.com(三天内删除相关链接)资源失效请留言反馈

 3 下载提示:如遇蓝奏云无法访问,请修改lanzous(把s修改成x)

 免责声明:本站为个人博客,所有软件信息均来自网络 修改版软件,加群广告提示为修改者自留,非本站信息,注意鉴别

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2023年3月5日 上午12:00
下一篇 2023年3月7日 下午10:34