OMG!又一个频繁FullGC的案例

转载自  OMG!又一个频繁FullGC的案例

将用户已安装APP数据从MySQL中迁移到MongoDB中。MySQL中存储方式比较简单,每个用户每个已安装的APP一行记录,且数据模型对应AppFromMySQL。迁移到MongoDB中,我们想更好的利用MongoDB的优势,所以其对应的数据模型为UserAppMongo,如果用JSON表示则如下所示:

{"id": "201811040001","userId": "12","appMongoList": [{"appName": "支付宝","packageName": "com.alipay","iconUrl": "http://s3.domain.com/12/12/com.alipay.jpg"},{"appName": "淘宝","packageName": "com.alibaba.taobao","iconUrl": "http://s3.domain.com/12/12/com.alibaba.taobao.jpg"}]
}

问题重现

按照惯例,为了方便重现问题,将代码浓缩一下:

class AppMongo {private String appName;private String packageName;private int versionCode;private Date installTime;private String iconUrl;private String downloadUrl;private String remark;private Long size;private String developer;
}
// 需要保存到MongoDB中的用户已安装app信息,这样保存的好处就是MongoDB中installed_apps这张表的user_id能设置唯一键约束,查询性能相比RDBMS中数据平铺要高不少
class UserAppMongo {private String id;private Long userId;private List<AppMongo> appMongoList;
}
// 关系型数据库中用户已安装app
class AppFromMySQL {private int id;private Long userId;private String packageName;private int versionCode;private Date installTime;private String appName;private String iconUrl;private String downloadUrl;private String remark;private Long size;private String developer;
}public class FullGCSample {public static void main(String[] args) throws Exception{for (int pageNo = 0; pageNo < 10000; pageNo++) {List<Long> userList = getUserIdByPage(pageNo);List<UserAppMongo> userAppMongoList = new ArrayList<>(userList.size());for (Long userId:userList){List<AppFromMySQL> appFromMySQLList = getUserInstalledAppList(userId);UserAppMongo userAppMongo = new UserAppMongo();userAppMongo.setId(System.nanoTime()+"");//测试代码任意模拟一个伪唯一IDuserAppMongo.setUserId(userId);userAppMongo.setAppMongoList(appFromMySQL2AppMongo(appFromMySQLList));userAppMongoList.add(userAppMongo);}// save List<UserAppMongo> to mongodbsave2MongoDB(userAppMongoList);}}private static void save2MongoDB(List<UserAppMongo> userAppMongoList) throws Exception {// 模拟保存一次数据到mongodb中要5msThread.sleep(5);}private static List<AppMongo> appFromMySQL2AppMongo(List<AppFromMySQL> list){List<AppMongo> appMongoList = new ArrayList<>();for (AppFromMySQL app:list){AppMongo appMongo = new AppMongo();//TODO bean copyappMongoList.add(appMongo);}return appMongoList;}private static List<AppFromMySQL> getUserInstalledAppList(Long useId){List<AppFromMySQL> appFromMySQLList = new ArrayList<>();// 假设用户手机上安装的app数量在50~200之间int size = 50 + new Random().nextInt(150);for (int i = 0; i < size; i++) {AppFromMySQL appFromMySQL = new AppFromMySQL(i, (long)i, "com.afei.android"+i, i, new Date(), "appName"+i);appFromMySQL.setIconUrl(String.valueOf(i));appFromMySQL.setDownloadUrl(String.valueOf(i));appFromMySQL.setRemark(String.valueOf(i));appFromMySQL.setSize((long)i);appFromMySQL.setDeveloper(String.valueOf(i));appFromMySQLList.add(appFromMySQL);}return appFromMySQLList;}private static List<Long> getUserIdByPage(int pageNo){List<Long> userList = new ArrayList<>();// 取数据时每一页1000个用户for (int i = 0; i < 2000; i++) {userList.add((long)i);}return userList;}
}

配套的JVM参数如下(由于是迁移程序,没必要配置CMS甚至G1,默认的PS垃圾回收即可):

-Xmx400m -Xms400m -Xmn150m -verbose:gc -XX:+PrintGCDetails

运行后jstat -gcutil 57408 2s的结果如下:

  S0     S1     E      O      M     CCS    YGC     YGCT    FGC    FGCT     GCT29.81  82.88 100.00  39.35  61.05  61.52     40   16.274     7    6.756   23.03091.43  21.01 100.00  39.26  61.05  61.52     45   17.791     8    7.327   25.1180.00  90.53   0.00  88.47  61.05  61.52     47   18.694     9    7.327   26.02123.00   0.00 100.00  19.10  61.05  61.52     52   19.655    10    9.227   28.88293.29   0.00   0.00  90.25  61.05  61.52     56   21.326    11    9.227   30.55394.21   0.00   0.00  82.39  61.05  61.52     60   22.435    12   10.253   32.68893.23  93.23 100.00  71.09  61.05  61.52     64   23.223    12   11.027   34.250

这里有两个比较严重的问题:

  1. Old区涨的过快;

  2. FGC太频繁;

事实上第二个问题就是第一个问题引起的。

分析问题

这个案例比较特殊,虽然FGC频繁,但是每次FGC后,Old都能降下去。这种情况下,我们不好通过jmap -dump得到dump文件,或者通过jmap -histo得到Java对象柱状图,因为极大可能是Old区的使用率很低的时候生成的结果,这种结果没多大参考价值:

[afei@node1 ~]# jstat -gcutil 121165 100S0     S1     E      O      M     CCS    YGC     YGCT    FGC    FGCT     GCT   0.00   0.00  40.00  15.71  58.25  51.76    287    7.891    63    2.921   10.81296.58   0.00  18.00  34.05  58.25  51.76    289    7.937    63    2.921   10.85896.84   0.00   0.00  70.73  58.25  51.76    291    8.001    63    2.921   10.9230.00   0.00   0.00  27.31  58.25  51.76    291    8.033    64    2.978   11.0100.00  99.47   0.00  45.80  58.25  51.76    293    8.077    64    2.978   11.0550.00  96.84   0.00  83.17  58.25  51.76    295    8.144    65    2.978   11.12196.91   0.00   0.00  21.68  58.25  51.76    296    8.157    65    3.026   11.183

那么我们有其他办法在Old区使用率很大,甚至发生FGC前生成dump文件吗?当然有,这里介绍两个参数:-XX:+HeapDumpAfterFullGC-XX:+HeapDumpBeforeFullGC。看命名就知道,这两个参数是在FGC前后生成dump文件。需要注意的是,一定是发生FGC,而不是CMS GC或者G1这种并发GC。加上-XX:+HeapDumpBeforeFullGC这个参数后,再次运行,我们看到如下这样的GC日志,即在FGC之前生成dump文件:

[GC (Allocation Failure) [PSYoungGen: 94016K->42816K(102400K)] 236438K->227942K(358400K), 0.0661795 secs] [Times: user=0.62 sys=0.88, real=0.07 secs] 
[GC (Allocation Failure) [PSYoungGen: 94016K->42752K(102400K)] 279142K->270606K(358400K), 0.0711319 secs] [Times: user=0.60 sys=1.01, real=0.07 secs] 
[Heap Dump (before full gc): Dumping heap to java_pid121598.hprof ...
Heap dump file created [366886452 bytes in 1.878 secs]
, 1.8782650 secs][Full GC (Ergonomics) [PSYoungGen: 42752K->0K(102400K)] [ParOldGen: 227854K->41341K(256000K)] 270606K->41341K(358400K), [Metaspace: 2828K->2828K(1056768K)], 0.1720676 secs] [Times: user=3.72 sys=0.07, real=0.17 secs] 

对dump文件进行分析,结果如下,两个比较靠前的对象是UserAppMongo和AppMongo:

 

headp dump

而通过TOP1的对象UserAppMongo的"List Objects"->"with outgoing references",得到如下图所示,由图可知,UserAppMongo这个对象属性里包含了List<AppMongo>对象(appMongoList),其本质是Object数组,每个AppMongo对象又是由appName,packageName,installTime等属性组成,所以Histogram视图中排名前几位的UserAppMongo,Object[],ArrayList,AppMongo事实上都是UserAppMongo这一个对象:

 

outgoing references

迁移程序比较简单,核心代码就那么几行,通过问题对象UserAppMongo,review代码的过程中,我们很快就怀疑到了下面这段代码:

List<Long> userList = getUserIdByPage(pageNo);
List<UserAppMongo> userAppMongoList = new ArrayList<>(userList.size());
for (Long userId:userList){List<AppFromMySQL> appFromMySQLList = getUserInstalledAppList(userId);UserAppMongo userAppMongo = new UserAppMongo();userAppMongo.setId(System.nanoTime()+"");userAppMongo.setUserId(userId);userAppMongo.setAppMongoList(appFromMySQL2AppMongo(appFromMySQLList));userAppMongoList.add(userAppMongo);
}
// save List<UserAppMongo> to mongodb
save2MongoDB(userAppMongoList);

这段代码的逻辑是:

  1. 得到一批用户ID;
  2. 然后遍历这些用户ID,取得每个用户已安装APP集合转换成MongoDB需要的数据模型;
  3. 批量保存到MongoDB中;

我们仔细分析一下这段代码就会发现,遍历每一页的过程中,总计有pageSize*n*2个对象直到保存到MongoDB后,遍历下一页时这些对象才会得到释放,其中pageSize是每一页的用户数量(方法getUserIdByPage中),n是用户平均安装APP的数量,之所以乘以2是因为有一半是MySQL数据模型对象,另一半是MongoDB数据模型对象。假设每一页1000个用户,用户平均安装的APP数量为100个。那么处理每一页时总计有20w个对象一直常驻,且无法被GC掉。

如何解决

了解了问题的本质后,就比较好解决了,而且有很多种方法可以解决。

  • 方法1-增大Young区

方法1就是增大Young区大小,准确的说是增大Eden区大小,大到能容忍20w个对象。那如果迁移程序将pageSize改为2000,那么就需要增大Eden区直到能容下40w个对象。

  • 方法2-优化代码

方法1优化办法的JVM参数还得跟pageSize参数值耦合,有点约束。我们能否优化成无论pageSize多大。每次内存中最大常驻对象数量是一定的呢?当然可以,请看下面这段优化后的代码:

List<Long> userList = getUserIdByPage(pageNo);
List<UserAppMongo> userAppMongoList = new ArrayList<>(userList.size());for (Long userId:userList){List<AppFromMySQL> appFromMySQLList = getUserInstalledAppList(userId);UserAppMongo userAppMongo = new UserAppMongo();userAppMongo.setId(System.nanoTime()+"");userAppMongo.setUserId(userId);userAppMongo.setAppMongoList(appFromMySQL2AppMongo(appFromMySQLList));userAppMongoList.add(userAppMongo);// 核心优化代码if (userAppMongoList.size()>=threshold){save2MongoDB(userAppMongoList);userAppMongoList.clear();}
}
// save List<UserAppMongo> to mongodb
save2MongoDB(userAppMongoList);

说明:

核心优化代码的threshold的值,取一个合理的值即可。这样的话,无论getUserIdByPage()时pageSize多大,整个堆中不可GC的驻留对象只会多几个userId而已。

假设threshold设置为500,那么在遍历到下一页之前整个堆中不可GC的驻留对象个数为:500*100*2=10000,其中100是平均每个用户安装APP的数量。

这样优化以后,无论getUserIdByPage()中批量取用户时pageSize为1000,还是5000,还是20000。JVM参数都不需要调整,且非常稳定。jstat -gcutil 56436 2s结果如下所示,运行一段时间都没有FGC,并且Old涨幅基本可以接受:

  S0     S1     E      O      M     CCS    YGC     YGCT    FGC    FGCT     GCT35.87   0.00  54.00   3.64  61.16  61.52     52    3.894     0    0.000    3.8940.00  50.37  48.00   3.89  61.16  61.52     67    4.392     0    0.000    4.39212.41   0.00  46.00   4.14  61.16  61.52     80    4.990     0    0.000    4.9901.66  14.04 100.00   4.38  61.16  61.52     89    5.636     0    0.000    5.6360.00  27.05  24.00   4.63  61.16  61.52    103    6.146     0    0.000    6.146

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

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

相关文章

ASP.NET Core - 关于标签帮助器值得了解的五点

如果您开发过ASP.NET Core Web应用程序&#xff0c;您应该已经熟悉了标签帮助器。ASP.NET Core应用程序依赖标签帮助器来呈现表单和表单字段是很常见的。所以&#xff0c;一个视图通常包含许多标签帮助器以及标准的HTML标记。您可以通过多种方式使用标签帮助器来提高开发的效率…

存储过程示例整理

--列出服务器上所有的数据库 exec sp_databases--改数据库的名字 exec sp_renamedb QQDB, QQ--查看表users中的列 exec sp_columns users《此组件已作为此服务器安全配置的一部分而被关闭》的解决办法use master exec sp_configure show advanced options,1 --显示高级配置信息…

如何使用MAT进行JVM内存泄露分析

转载自 如何使用MAT进行JVM内存泄露分析 在《Java Agent的隔离实现以及卸载时一些坑》中&#xff0c;卸载Agent之后&#xff0c;使用 jmap-histo:live pid命令验证执行FGC&#xff0c;相关Class是否会被回收&#xff0c;结果遇到了一些问题&#xff0c;最终通过MAT内存分析才…

Ribbon 客户端负载均衡

文章目录零、懒汉式改为饿汉式一、基于配置文件二、基于Bean配置三、自定义规则1 权重优先调用2 集群优先调用3 元数据优先调用零、懒汉式改为饿汉式 【consumer-springboot-80子模块】 Ribbon默认使用懒汉式加载服务列表&#xff0c;更改为懒汉式 application.yml ribbon: …

C#使用Xamarin开发可移植移动应用(3.Xamarin.Views控件)附源码

.NET core2.0 发布了,刺激,大致看了一下,很不错,打算后期学习.(不出意外,应该也会写个小系列). 虽然官方推荐用共享类库创建新的类库..然而我这个Demo还是使用的可移植.. 嗯..解释一下 为什么暂时没用共享类库.. 有些小BUG 可能是为了迎合其他类型的项目..所以在共享类库里创…

“老师,我不要苹果味的,我要葡萄味的”!

大家好&#xff0c;我是雄雄&#xff0c;欢迎关注公众号【雄雄的小课堂】。题库四班目前题库正在维护中&#xff0c;工作量最大的莫过于题库里面的题量了&#xff0c;所以目前的解决方法是让动员大家一起出题&#xff0c;但是人多较杂&#xff0c;各种各样的题都有&#xff08;…

一次频繁Full GC的排查过程,根源居然是它...

转载自 一次频繁Full GC的排查过程&#xff0c;根源居然是它... 业务部门的一个同事遇到个奇怪的 Full GC 问题&#xff0c;有个服务迁移到新的应用后&#xff0c;一直频繁 Full GC。新应用机器的配置是 4c 8g&#xff0c;老应用是 4c 4g&#xff0c;老应用 GC 都很正常&…

jzoj1158-荒岛野人【扩欧,gcd,同余方程】

正题 大意 有n个野人&#xff0c;每个野人有一个初始山洞CiCi&#xff0c;每次向前移动距离PiPi&#xff0c;寿命LiLi&#xff0c;如果野人走到了最后一个山洞那么继续就好回到第一个山洞&#xff0c;求至少多少个山洞才可以让野人们不会发生冲突。 解题思路 我们可以枚举答…

.NET Core 2.0 的dll实时更新、https、依赖包变更问题及解决

今天所有开发环境已经迁移到mac OS下的Visual Studio Code 命令行编译发布&#xff0c;而运行服务器是CentOS7&#xff0c;和windows没什么关联了。 只要你Relese编译并在本地有一个与服务器相同的运行环境中运行成功了&#xff0c;迁移到真实服务器不会有什么难度。 下面是迁…

“老师,我写着写着就 强制交卷了……”

大家好&#xff0c;我是雄雄&#xff0c;欢迎关注公众号【雄雄的小课堂】。01暴露出的问题

张老师讲Python~

大家好&#xff0c;我是雄雄&#xff0c;欢迎关注微信公众号【雄雄的小课堂】。最近我的个人站上线啦&#xff0c;欢迎大家访问http://穆雄雄.com&#xff1b;或者点击文末的“阅读原文”。昨天下午靳老师分享了关于网站部署的内容&#xff0c;今天下午请张炜林上去分享了下他准…

Entity Framework Core 2.0 新特性

一.模型级查询过滤器&#xff08;Model-level query filters&#xff09; ef core2.0包含了一个新特性&#xff0c;我们叫他模型级查询过滤器&#xff08;Model-level query filters&#xff09;。此特性允许使用Linq查询表达式直接定义在实体类型的元数据模型上。这样的过滤器…

激动的时刻,终于成啦~

大家好&#xff0c;我是雄雄&#xff0c;欢迎关注公众号&#xff1a;【雄雄的小课堂】。今天&#xff0c;最令我激动的一件事莫过于倾注一周精力的“在线测试”终于可以投入使用了&#xff0c;周二发过一篇文章&#xff0c;是关于在线测试的问题总结&#xff0c;也就是在周二&a…

C#使用Xamarin开发可移植移动应用(4.进阶篇MVVM双向绑定和命令绑定)附源码

今天的学习内容? 今天我们讲讲Xamarin中的MVVM双向绑定,嗯..需要有一定的MVVM基础.,具体什么是MVVM - -,请百度,我就不多讲了 效果如下: 正文 1.简单的入门Demo 这个时间的功能很简单,就是一个时间的动态显示. 我们首先创建一个基础的页面如下: <?xml version"…

由「Metaspace容量不足触发CMS GC」从而引发的思考

转载自 由「Metaspace容量不足触发CMS GC」从而引发的思考 某天早上&#xff0c;毛老师在群里问「cat 上怎么看 gc」。 好好的一个群 看到有 GC 的问题&#xff0c;立马做出小鸡搓手状。 之后毛老师发来一张图。 老年代内存占用情况 图片展示了老年代内存占用情况。 第一个…

是现在的钱不值钱还是药太贵!

大家好&#xff0c;我是雄雄&#xff0c;欢迎关注微信公众号【雄雄的小课堂】。莫名其妙的就感觉身体不舒服&#xff0c;然后越来越严重&#xff0c;打小以来还是第一次遇见这样的&#xff0c;你说是感冒吧&#xff0c;它也不流鼻涕&#xff0c;喉咙也不痛&#xff0c;鼻子也通…

一次堆外内存泄露的排查过程

转载自 一次堆外内存泄露的排查过程 最近在做一个基于 websocket 的长连中间件&#xff0c;服务端使用实现了 socket.io 协议&#xff08;基于websocket协议&#xff0c;提供长轮询降级能力&#xff09; 的 netty-socketio 框架&#xff0c;该框架为 netty 实现&#xff0c;鉴…

.NET Core 2.0 特性介绍和使用指南

前言 这一篇会比较长&#xff0c;介绍了.NET Core 2.0新特性、工具支持及系统生态&#xff0c;现状及未来计划&#xff0c;可以作为一门技术的概述来读&#xff0c;也可以作为学习路径、提纲来用。 对于.NET Core 2.0的发布介绍&#xff0c;围绕2.0的架构体系&#xff0c;我想…

Lombok MyBatisX

Lombok的使用 [1] 什么是LomBok lombok是一个可以通过简单的注解的形式来帮助我们简化消除一些必须有但显得很臃肿的 Java 代码的工具&#xff0c;简单来说&#xff0c;比如我们新建了一个类&#xff0c;然后在其中写了几个属性&#xff0c;然后通常情况下我们需要手动去建立g…

一次堆外OOM问题的排查过程

转载自 一次堆外OOM问题的排查过程 背景 线上服务有一台机器访问不通&#xff08;一个管理平台),在公司的服务治理平台上查看服务的状况是正常的&#xff0c;说明进程还在。进程并没有完全crash掉。去线上查看机器日志&#xff0c;发现了大量的OOM异常: 017-03-15 00:00:0…