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

news2025/1/22 21:46:34

简介

最近,我们系统配置了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可以很容易知道哪里调用了这个方法,如下:

# 开启收集
./profiler.sh start --all-user -e G1CollectedHeap::humongous_obj_allocate -f ./humongous.jfr jps

# 停止收集
./profiler.sh stop -f ./humongous.jfr jps
复制代码

将humongous.jfr文件用jmc打开,如下:


根据调用栈我发现,这是我们的一个定时任务,它会定时调用一个接口获取配置信息并缓存它,而这个接口返回的数据量达14M之多。

难道是这个接口导致的gc问题?但这个定时任务调用也不频繁呀,5分钟才调用一次,不至于让gc忙不过来吧!
另一个疑问是,这个定时任务在其它环境也会运行,而且其它环境的业务流量大得多,为什么其它环境没有问题?

至此,我也不确定是这个定时任务导致的问题,还是系统自身特点导致偶尔的高gc耗时。

由于我们有固定的上线日,于是我打算先优化产生大对象的代码,然后在上线前的期间试着优化一下jvm gc参数。

优化产生大对象的代码

我们使用的是httpclient调用接口,调用接口时,代码会先将接口返回数据读取成String,然后再使用jackson把String转成map对象,简化如下:

rsp = httpClient.execute(request);
String result = IOUtils.toString(rsp.getEntity().getContent());
Map resultMap = JSONUtil.getMapper().readValue(result, Map.class);
复制代码

要优化它也很简单,使用jackson的readValue有一个传入InputStream的重载方法,用它来读取json数据,而不是将其加载成一个大的String对象再读,如下:

rsp = httpClient.execute(request);
InputStream is = rsp.getEntity().getContent();
Map resultMap = JSONUtil.getMapper().readValue(is, Map.class);
复制代码

注:这里面map从逻辑上来说是一个大对象,但对jvm来说,它只是很多个小对象然后用指针连接起来而已,大对象一般由大数组造成,大String之所以是大对象,是因为它里面有一个很大的char[]数组。

优化GC参数

优化完代码后,我开始研究优化jvm gc参数了,我们使用的是jdk8,垃圾收集器是g1,为了理解g1的调优参数,又简单学习了下g1的关键概念。

  1. g1是分region收集的,但region也分年轻代与老年代。
  2. g1的gc分young gcmixed gc,young gc用于收集年轻代region,mixed gc会收集年轻代与老年代region。
  3. mixed gc回收之前,需要先经历一个并发周期(Concurrent Cycles),用来标记各region的对象存活情况,让mixed gc可以判断出需要回收哪些region。
  4. 并发周期分为如下4个子阶段:
    a. 初始标记(initial marking)
    b. 并发标记(concurrent marking)
    c. 重新标记(remarking)
    d. 清理(clean up)
    需要注意的是,初始标记(initial marking)这一步是借助young gc完成的。

在g1中,young gc几乎没有什么可调参数,而mixed gc有一些,常见如下:

参数作用
-XX:InitiatingHeapOccupancyPercent开始mixed gc并发周期的堆占用阈值,当大于此比例,启动并发周期,默认45%
-XX:ConcGCThreads在并发标记时,使用多少个并发线程。
-XX:G1HeapRegionSize每个region大小,当分配对象尺寸大于region一半时,才认为这是一个大对象。
-XX:G1MixedGCLiveThresholdPercentregion存活比例,默认85%,当并发标记后发现region中存活对象比例小于这个值,mixed gc才会回收这个region,毕竟一个region如果都是存活的对象,那还有什么回收的必要呢。
-XX:G1HeapWastePercent允许浪费的堆比例,默认5%,当可回收的内存比例大于此值时,mixed gc才会去执行回收,毕竟没什么可回收的对象时,还有什么回收的必要呢。
-XX:G1MixedGCCountTargetmixed gc执行的次数,一旦并发标记周期确认了回收哪些region后,mixed gc就进行回收,但mixed gc会分少量多次来回收这些region,默认8次。
-XX:G1OldCSetRegionThresholdPercent每次mixed gc回收old region的比例,默认10%,如果G1MixedGCCountTarget是8的话,mixed gc整体能回收80%。
-XX:G1ReservePercent堆保留空间比例,默认10%,这部分空间g1会保留下来,用来在gc时复制存活对象。

出现to-space exhausted会不会是mixed gc太慢了呢?于是我调整了如下参数:

  1. 让并发标记周期启动更早,运行得更快,将-XX:InitiatingHeapOccupancyPercent从默认值45%调整到35%,-XX:ConcGCThreads从1调整为3。
  2. -XX:G1MixedGCLiveThresholdPercent-XX:G1HeapWastePercent确定回收哪些region,有多少比例垃圾才执行回收,我觉得它们的值本来就蛮激进,就没有调整。
  3. -XX:G1MixedGCCountTarget-XX:G1OldCSetRegionThresholdPercent控制mixed gc执行多少次,每次回收多少region,我将-XX:G1OldCSetRegionThresholdPercent从10%调整到了15%,让它一次多回收些old region。
  4. 增加保留空间,避免复制存活对象失败,将-XX:G1ReservePercent从10%调整到20%。
  5. 尽量避免产生大对象,将-XX:G1HeapRegionSize增加到16m。

于是我按照上面调整了jvm参数,可是第二天我发现还是有GC长耗时,次数也没有减少,看来问题根因和我调整的参数没有关系。

问题根因

就这样,到了上线日,于是我上线了前面优化大对象的代码,一天后,我发现偶尔的GC长耗时竟然没有了!

问题就这样解决了!!!

然而我心里还是有一个大大的问号,其它环境同样有这个定时任务,一样的运行频率,jvm配置也全是一样的,为啥其它环境没有问题呢?其它环境业务流量还大一些!

为此,我搜索了大量关于g1大对象的文章,我发现大对象的分配与回收过程有点特殊,如下:

  1. 大于region size一半的对象是大对象,会直接分配在old region,且gc时大对象不会被复制或移动,而是直接回收。
  2. 大对象回收发生在2个地方,一个是并发周期的clean up子阶段,另一个是young gc(这个特性在jdk8u60才加入)。

我忽然想到,莫非是俄罗斯环境流量太低,触发不了young gc,且并发周期又因为什么原因没执行,但定时任务又慢慢生成大对象将old region占满,导致了to-space exhausted?

于是,我打算写段代码试验一下,慢慢的只生成大对象,看g1会不会回收,如下:


这个一个命令行交互程序,使用如下jvm参数启动它:

# 1600m的堆,16m的region size
# rlwrap作用是使得这个命令行程序更好用
rlwrap java -server -Xms1600m -Xmx1600m -Xss1m -XX:MetaspaceSize=512m -XX:MaxMetaspaceSize=1g -Xloggc:/home/work/logs/applogs/gc-%t.log -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/home/work/logs/applogs/ -XX:+PrintClassHistogram -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintHeapAtGC -XX:+PrintGCDateStamps -XX:+PrintGCApplicationStoppedTime -XX:+PrintAdaptiveSizePolicy -XX:+UseG1GC -XX:G1LogLevel=finest -XX:G1HeapRegionSize=16m -XX:MaxGCPauseMillis=200 -jar command-cli.jar
复制代码

使用了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!
 

  1. 从上面do not start mixed GCs, reason: candidate old regions not available的日志中看到,mixed gc日志之所以长时间没出来,是因为没有可回收的region导致mixed gc没有执行,因为我们只创建了大对象,但mixed gc不回收大对象分区,所以确实是没有可回收的region的。
  2. Humongous Reclaimed: 98可以发现,这次young gc,回收了98个大对象分区,而我们总共只有100个分区,说明在inital marking之后创建的大对象,确实一直都没有回收。

注:添加-XX:G1LogLevel=finest参数,才能输出Humongous Reclaimed这一项。

但是,大对象分区占了98个,堆占用率肯定超过了45%,为何一直没有再次启动并发周期呢?

感觉这可能是jdk的bug,于是我分别下了最新的jdk8u与jdk11u验证,发现问题在最新的jdk8u中依然存在,而jdk11u中则不存在,这应该就是JDK的Bug!
于是我通过二分搜索法多次编译了不同版本的JDK,最终确定问题fix在jdk9_b93与jdk9_b94之间。


并在jdk的bug库中,搜索到了相同描述的bug反馈,如下:

bugs.openjdk.org/browse/JDK-…

Bug改动代码如下:


大致瞄了下代码,可能理解得不完全正确,改动逻辑如下:

  1. G1再次启动并发周期之前,至少需要执行过一次mixed gcyoung gc,类似于Concurrent Cycles -> mixed gc|young gc -> Concurrent Cycles -> mixed gc|young gc
  2. 我们的场景是,在并发周期结束之后,由于没有需要回收的分区,导致mixed gc实际没有执行,可是我们只分配了大对象,且大对象又只分配在old region,所以young gc也不可能被触发,而由于上面的条件,Concurrent Cycles也不会被触发,因此最终大对象将堆占满触发了to-space exhausted。
  3. 修复逻辑是,当并发周期结束后,没有需要回收的分区时,should_continue_with_reclaim=false,进而使得允许不执行纯young gc而直接启动并发周期,类似于Concurrent Cycles -> Concurrent Cycles

所以在使用JDK8时,像那种系统流量很小,新生代较大,又有定时任务不断产生大对象的场景,堆几乎必然会被慢慢占满,要解决这个问题,可参考如下处理:

  1. 优化代码,避免分配大对象。
  2. 代码无法优化时,可考虑升级jdk11。
  3. 无法升级jdk11时,可考虑减小-XX:G1MaxNewSizePercent让新生代小一点,让young gc能执行得更频繁些,同时老年代更大,能缓冲更多大对象分配。

考虑到我们负责的其它系统中,时不时就有一波大响应体的请求,也没法快速修改代码,于是我统一将-XX:G1MaxNewSizePercent减小到30%,经过观察,修改后GC频率有所增加,但暂停时间有所下降,这是符合期望的。

总结

当我在jdk的bug库中搜索问题时,发现不少和G1大对象相关的优化,早期JDK(如JDK8)的G1实现可能在大对象回收上不太完善,所以写代码时要注意尽量少创建大对象,以回避这些隐性问题。

注:这之后又遇到了Update RS (ms)耗时高,竟也和大对象有关,添加-XX:-ReduceInitialCardMarks后解决,看来大对象是万恶之源啊😂

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/151534.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

高校舆情监控系统建设(TOOM)如何做好教育行业舆情监控方案?

高校作为高密度学生聚集地,舆情管理上,需要保持高度的警惕性。高校中大学生是活跃在互联网上的重要群体,他们作为文化水平较高、思维较活跃的特殊群体,其网络中的言论合集往往会引发社会关注。高校舆情监控系统建设(TOOM)如何做好…

Sapped of vitality 生机已被耗尽 | 经济学人社论高质量双语精翻

选自TE20221217,leaders The global economy:Sapped of vitality 世界经济:生机已被耗尽 Why are the rich world’s politicians giving up on economic growth? 为什么发达国家的政客们不再追求经济增长目标? The prospect of …

智能音箱app开发-广州app开发定制

科技高速发展,智能产品遍地都是。日常生活都是智能化时代,智能音箱app也开始被开发出来。为用户提供便捷的服务,优化体验。 智能音箱app开发特点 一:搭建快速 线上渠道在各个行业中不可或缺的,因为线上平台不管是流量…

nginx学习笔记3(小d课堂)

nginx访问日志的作用: 我们先去查看一下我们的nginx.conf文件: BAT大厂应用运维平台案例统计 awk 默认以空格进行分隔。 {print $1} 只取第一个属性 sort -n排序 sort -rn倒序 uniq -c去重 head -n 100取前100个。 自定义日志统计接口性能 我们修改完…

上半年要写的博客文章28

上半年要写的博客文章21 这里写自定义目录标题欢迎使用Markdown编辑器新的改变功能快捷键合理的创建标题,有助于目录的生成如何改变文本的样式插入链接与图片如何插入一段漂亮的代码片生成一个适合你的列表创建一个表格设定内容居中、居左、居右SmartyPants创建一个…

基于ESP32的蓝牙刷屏器自动点击器的制作

ESP32模块的选型: 这里是利用蓝牙连接手机来做点击器或刷屏器,ESP8266只有WIFI而ESP32有WIFI和蓝牙,所以选择ESP32模块。 ESP32模块可以选择ESP32-NodeMCU: 或ESP32-MiniKit: 这里使用的是ESP32 MINI KIT,Arduino环境下烧录选择如…

Nginx搭建Web服务器

环境: CentOS 7.2.1511 一、搭建静态web服务器 访问基本的静态页面(基于IP访问) 1.使用源码包编译安装nginx 启动ngin 直接在服务器测试访问: 访问nginx Web页面实现用户认证 修改nginx配置文件 2.生成密码文件&#xff…

64. 方法的值的传递及调用类内部的属性、方法

64. 方法的值的传递及调用类内部的属性、方法 文章目录64. 方法的值的传递及调用类内部的属性、方法1. 知识回顾2. 值的传递3. 没有值4. 需要向方法传递值5. 调用类内部的属性和方法6. 综合代码7. 类在爬虫中的应用7.1 创建类7.2 创建对象7.3 调用方法7.4 第2次调用方法7.5 第2…

MSE ZooKeeper 数据导入导出功能上线

作者:草谷 背景 MSE 提供了托管版的 ZooKeeper,拥有比自建开源 ZooKeeper 稳定性更高的SLA,同时管控面提供了丰富的服务自治功能。赶在2022年的岁末,MSE ZooKeeper 上线了一个非常实用的功能-数据导入导出功能,彻底解…

Mybatis自动生成增删改查代码

GitHub项目地址 Gitee项目地址 使用 mybatis generator 自动生成代码&#xff0c;实现数据库的增删改查。 1 配置Mybatis插件 在pom文件添加依赖&#xff1a; <plugins> <plugin><groupId>org.mybatis.generator</groupId><artifactId>myba…

宝塔面板Nginx开启Brotli压缩,提升网站加载速度

前言Google 认为互联网用户的时间是宝贵的&#xff0c;他们的时间不应该消耗在漫长的网页加载中&#xff0c;因此在 2015 年 9 月 Google 推出了无损压缩算法 Brotli。Brotli 通过变种的 LZ77 算法、Huffman 编码以及二阶文本建模等方式进行数据压缩&#xff0c;与 Gzip相比效率…

图解cross attention

英文参考链接&#xff1a; https://vaclavkosar.com/ml/cross-attention-in-transformer-architecture 交叉注意力与自我注意力 除了输入&#xff0c;cross-attention 计算与self-attention相同。交叉注意力不对称地组合了两个相同维度的独立嵌入序列&#xff0c;相比之下&a…

移动互联网社交江湖已定,抖音为何仍不放过微信?

进入2023年&#xff0c;抖音集团依然放不下“社交梦”。2022年12月30日&#xff0c;抖音官网上线了一款名为“抖音聊天”的桌面端聊天软件&#xff0c;提供Windows和Mac两个版本&#xff0c;进入软件需使用抖音App扫码登录。这并不是抖音集团首次推出社交产品。2019年以来&…

Day854.两阶段终止模式 -Java 并发编程实战

两阶段终止模式 Hi&#xff0c;我是阿昌&#xff0c;今天学习记录的是关于两阶段终止模式的内容。 从纯技术的角度看&#xff0c;都是启动多线程去执行一个异步任务。既启动&#xff0c;那又该如何终止呢&#xff1f; 那如何优雅地终止线程&#xff0c;正所谓有始有终。在Ja…

Kafka 概念

Kafka 属于分布式的消息引擎系统&#xff0c;主要功能 &#xff1a;提供一套完备的消息发布与订阅解决方案 生产者和消费者都是客户端&#xff08;Clients&#xff09;&#xff1a; 生产者&#xff08;Producer&#xff09;&#xff1a;向主题发布消息的客户端应用程序消费者…

前端基础(九)_this基本使用、this指向判断、改变this指向的方法

前言&#xff1a; 看这个例子&#xff1a; 例&#xff1a;给所有的div标签都添加点击事件&#xff0c;并打印当前节点的内容 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><meta http-equiv"X-UA-Compa…

HW14基于规则化的终身学习Regularization-based Lifelong Learning

文章目录一、介绍-终身学习目标数据集任务示例代码-准备数据模型体系结构样本代码-训练和评估Training Pipeline:MAS - Memory Aware SynapseSISCP - Sliced Cramer Preservation二、实验1、baseline2、EWC3、MAS4、SI5、RWalk6、SCP一、介绍-终身学习 目标 一个模型可以打败…

Naive UI 组件使用体验之-级联选择 Cascader

使用场景 地址区域选择 简单使用 安装依赖 npm install naive-ui -D 按需引入之-手动引入 import {NCascader,CascaderOption } from naive-ui 使用 <n-cascader:value"regionValue":options"regionOptions":multiple"false"placehol…

一篇搞懂Python中的随机数

在 python 中生成随机样本的所有你需要的示例列表 长按关注《Python学研大本营》&#xff0c;加入读者群&#xff0c;分享更多精彩 扫码关注《Python学研大本营》&#xff0c;加入读者群&#xff0c;分享更多精彩 在这篇博客中&#xff0c;我将演示如何根据不同的需求在python…

Docker三剑客——Docekr Swarm

目录 一、概述 二、Swarm 的核心概念 1. Node&#xff08;节点&#xff09; 2. Service&#xff08;服务&#xff09; 3. Task&#xff08;任务&#xff09; 三、Swarm 服务的运行部分 1. 工作节点 2. 服务、任务和容器 3. 任务和调度 4. 副本服务和全局服务 四、Swa…