1.背景
由于原先推送二方包的能力不支持扩展,现在由于业务发展需要增加新能力。所以重新开发了消息starter
开发思路是通过抽象工厂去分别创建安卓和IOS的对象。
上线后发现每日固定的时间点会出现full gc的情况,该时间段为推送业务高峰期,所以推断出问题是由于新上的推送starter引起的。
从jvm监控可以观察到,某时间段full gc频率非常高,同时堆内存使用主要集中在eden区。由此可推断出是推送业务中新创建对象太多所导致。
同时最近关于推送,改动的只有推送starter,那么我们基本可以把目标放在stater上面。
2.问题初步分析
尝试导出类加载日志进行分析
打开后未发现排名靠前的相关类,一切都很正常,唯一值得怀疑的是fastjson.asm相关的。继续查找sterter相关类。发现了些比较奇怪的类加载。但也暂时没看出啥问题。
类加载既然看着正常,为什么线上业务高峰期还会频繁full gc?
3.模拟线上业务排查jvm
操作方式是新建一个starter调用的junit Test,循环调用2000次。然后启动VisualVM进行排查
堆的波动看起来比较规律,也没有想象中那种堆内存无限飙高的情况出现。但正常的情况是一个较为平稳的波段,而不是这样断崖式起伏。
此时考虑是否是与三方交互的HttpClient引起的呢? 假如http请求时间较长,那么类在内存中就会一直被引用,就无法被释放。 此时尝试把调用三方的doPost注释掉。
要跑5分多钟的程序这次只用了36秒就跑完了,那么此时是不是可以怀疑是httpClient这里出了问题?
查询相关资料后发现httpClient是可以重用的,不需要重复的去创建,这样每次打开关闭都会有相应的性能损耗。对httpClient进行了一顿优化后,再次进行测试。
发现性能大概优化了50%,但感觉还是不够。堆的图形表现并没有明显的差别。
此时再把目光转回类加载信息当中,也就是fastJson的serializer中。查看了转json的JsonUtil代码
public static <T> String toJsonWithSnakeCase(T objects){ SerializeConfig config = new SerializeConfig(); config.propertyNamingStrategy = PropertyNamingStrategy.SnakeCase; return JSON.toJSONString(objects,config); }
发现这段代码很可疑,怀疑是不是这里的问题呢?
对以上代码进行优化后再次Test进行尝试
private static SerializeConfig config; static { config = new SerializeConfig(); config.propertyNamingStrategy = PropertyNamingStrategy.SnakeCase; ParserConfig.getGlobalInstance().propertyNamingStrategy = PropertyNamingStrategy.SnakeCase; } public static <T> String toJsonWithSnakeCase(T objects){ return JSON.toJSONString(objects,config); }
修改后对应jvm的堆内存情况
执行用时1分59秒,并且堆的加载图表也正常了,问题被成功定位!
4.问题复盘
虽然问题解决了,但还是该想想为什么区区一行转json字符串的代码却造成了线上频繁的full gc。
我们先来看下new SerializeConfig()干了些啥事。
1.初始化容器。
2.创建ASM工厂。
3.初始化序列化器。
简简单单一行代码,内部竟然要加载这么多东西,这jvm能好么~
我们接着往下看最终调用的JSON.toJSONString(objects,config)方法,直接定位到最终执行代码的那个实际方法
我们重点来看write这里。
需要线获取一个ObjectSerializer才能去write,接着进去看。
发现取ObjectSerializer的时候会先从mixInSerializers这个容器中去取,而这个容器所属的类是SerializeConfig,也就是我们每次去新建的这个类,所以就相当于每次都要从ASM的工厂去创建,好吧,汪汪队倒大霉。
而接着往下看就会发现调用到createASMSerializer这个方法,而这个方法里会有这么一个调用,asmFactory.createJavaBeanSerializer,也就是开头我们重复创建的ASMSerializerFactory。