数数FastJson那些年犯下的'血案'...
作者:landon30
来源:https://club.perfma.com/article/1656271
现象
QA同学反映登录不上服务器
排查问题1–日志级别
查看log,发现玩家登录的时候抛出了一个java.lang.OutOfMemoryError
大概代码是向Redis序列化一个PlayerMirror镜像数据,但是在JSON.toJSONString的时候出现了错误.比较清晰,即序列化的时候expandCapacity,内存不足。
又看了一下日志,有好几个OutOfMemoryError,都是类似于用fastjson序列化PlayerMirror报的错误
又仔细看了一下server目录,发现了几个.hprof,说明确实发生了堆内存溢出,因为启动参数增加了’-XX:+HeapDumpOnOutOfMemoryError’
at java.lang.OutOfMemoryError.<init>()V (OutOfMemoryError.java:48)at com.alibaba.fastjson.serializer.SerializeW
riter.expandCapacity(I)V (SerializeWriter.java:249)
-rw------- 1 xx xx 2043416350 Nov 24 11:37 java_pid8068.hprof
-rw------- 1 xx xx 2028797313 Nov 24 11:17 java_pid4671.hprof
-rw------- 1 xx xx 1477222612 Nov 23 23:25 java_pid31563.hprof
排查问题2–JVM命令级别
使用了jvm命令初步排查一下问题 jstat -gcutil pid jstat -gc pid jmap -histo pid jmap -heap pid
jstat看到老年代基本已经满了
jmap看到排名前两位的分别是Object[]和char[]
num #instances #bytes class name
----------------------------------------------1: 146219 741203672 [Ljava.lang.Object;2: 2842356 639498168 [C
排查问题3–专业工具级别
因为了hprof,所以只需要用专业的内存分析工具mat即可 mat#Open Heap Dump,载入后直接出来一个Getting Started Wizard#Leak Suspects Report,即内存泄露的报告,选择finish 两个怀疑的问题:
其中有一个JSONArray的实例就占用了大约700M内存
另外一个是线程的local Variables占用了500M内存
点开问题1详情,发现这个JSONArray是配置类PersonalityStrengthenConfig#cost字段,仔细看一下这个JSONArray#list#elementData的数组长度是可怕的183842095。
点开问题2详情,第一张图可以看到,fastjson内部的SerializeWriter中中buf#char[]长度竟然是可怕的262012306,而第二种图的堆栈信息可以看到是在序列化PersonalityStrengthenConfig抛出的内存溢出。
结合两个问题,比较能容易的想到答案,PersonalityStrengthenConfig
中的cost字段(JSONArray)占用了大量的内存,而玩家下线或者上线的时候要序列化一部分数据到redis,其中就包括这个PersonalityStrengthenConfig
,所以也要序列化这个超级大的cost,而序列化要申请空间,所以就内存溢出了。
分析问题1–观察数据
为什么数据配置类PersonalityStrengthenConfig会被序列化呢,因为玩家下线的时候需要序列化一个玩家镜像数据到redis Player->Hero->HeroPersonality->getConfig(PersonalityStrengthenConfig) HeroPersonality
有一个get方法,而做序列化的这个同学忘记加了SerializerFeature.IgnoreNonFieldGetter
这个参数,所以导致getConfig中的这个config对象被序列化进去了,修改完毕代码后,所有的问题都没有了。
需要确认一下:PersonalityStrengthenConfig#cost这个JsonArray
为什么占这么大空间,能看一下里面都是什么?
在mat中怀疑的第一个怀疑报告中点击PersonalityStrengthenConfig@0x8140c468
对象,左侧Inspector页面有一个Attributes,找到cost右键->List Objects->with outgoing references 从下图可以看到,这个JSONAray内部出了第一个元素是一个正常的JSONObject外,其他的全部为null,当然你可以从第二个怀疑报告中将SerializeWriter中的buf#char[]数据拷贝出来->单击->Copy->Save Value to File.当然这个文件几百M(且只有一行),非常大,普通的文本编辑器根本看不出来(我在linux上使用了tail,然后不断的ctrl+c 最终看到了数据的开头),而这个数据也是当序列化到了config#cost字段时,只有一个正常的数据,其他后面全部为null,所以数据问题确认完毕:cost字段里面除了一个正常的JSONObject外,剩余的全部是null。
分析问题2–尝试重现
最初的解决方法很简单 尝试通过代码方式能否复现
即new一个HeroPersonality,其内部有一个getConfig,使用没有加IgnoreNonFieldGetter的方式序列化,看是否会造成大内存的占用 很遗憾,未能复现
HeroPersonality hp = new HeroPersonality();hp.setPersonalityLevel(1);String str = JSON.toJSONString(hp);
然后尝试还原数值表最近的几个版本,看看是否有问题,这个就是怀疑策划配置表有问题 导致这个cost字段在某些特殊情况下会如可能在加载的时候就变的很大,不过很遗憾未能复现。
分析源代码 确认是否可能启动加载配置表后这个cost字段就很大
debug DataConfigService 发现的第一个问题是这个类混用了json-lib和fastjson(这个框架已更新,我们项目一直未更新),这里怀疑是否是json-lib有bug,发现反序列化的过程是JSONLexer#扫描如大括号,逗号,方括号。先找到了配置表的的第一个JSONObject,然后加到cost数组(注意此时JSONArray#list的底层数组长度已经被expand到了长度10) 然后遇到RBRACKET,就结束扫描了。
看到这里就有一个想法 是否是有可能遇到了特殊字符,如fastjson中的循环引用 进而猜测到是否是策划配置的时候配置了公式?而且我也各种尝试在json的cost字段加各种特殊字符,很遗憾,经过验证 未能复现。
尝试仔细看了一下堆快照,将PersonalityStrengthenConfig的10个对象内部数据都看了一下,和svn的策划表对比了一下,确认了是某个版本的数据。而这个版本的数据在本地测试是没有任何问题的,排除策划配置数据问题。
分析问题3–山重水复疑无路
在我写本地测试代码重现的时候,我写了一个反序列化HeroPersonality的例子,先用HeroPersonality序列化为一堆字符串,然后尝试在这堆字符串加入一些额外信息,然后再反序列化,不经意的发现当进行一次HeroPersonality的反序列化后 再将原来的HeroPersonality再次序列化输出时惊奇的发现序列化后的cost字段多了一个null,然后我就将反序列化代码放在循环里 然后再次输出 发现cost字段被加了很多null。
伪测试代码
PersonalityStrengthenConfig config = DataConfigService.
getSettingById(PersonalityStrengthenConfig.class, 1);System.out.println("dcs.config1:" + config.cost);
System.out.println("dcs.config1:" + config.attr_num);for (int i = 0; i < 10; i++) {String str1 = "{...}";JSON.parseObject(str1, HeroPersonality.class);
}System.out.println("dcs.config2:" + config.cost);
System.out.println("dcs.config2:" + config.attr_num);
结果输出:
这个结果让我惊喜,让我非常的肯定,cost中的大量null就是这样产生的,而且我最早就怀疑HeroPersonality中有一些非序列化的get方法有一些问题;而同样的attr_num也是JSONArray类型,就没有任何问题.问题初步锁定在HeroPersonality中的一个get方法.
分析问题4–源代码跟踪
需要源代码debug,为什么在不断的调用反序列化的时候,cost被加入了大量的null,下面这个是HeroPersonality
的两个get方法,可以看到其中的getNextTrainCost
调用了getConfig字段cost字段,下面从源代码debug的角度看一下为什么会每次反序列化都多了很多null。首先getNextTrainCost
这个getter中的nextTrainCost
被当成了一个field,因为其返回值是一个JSONArray,其本身是可以作为setter用到的。其反序列化,用json中"nextTrainCost"相关反序列化 该字符串是[{".config.cost[0]"} 即使用了fastjson的循环引用,这个反序列化出来为[null] (因为本身config压根就不属于field,只是一个get方法而已) 然后调用setter(本身就是一个setter),得到cost,然后将这个[null] add到cost上 然后每反序列化一次都向cost中加入一个[null],进而使cost越来越大(JSONArray#底层数组还会自动expand)
public JSONArray getNextTrainCost() {return ((PersonalityStrengthenConfig) getConfig()).cost;
}@Override
public DataConfigItem getConfig() {return DataConfigService.getSettingById(PersonalityStrengthenConfig.class,personalityLevel);
}
反序列化nextTrainCost
// FieldDeserializer#setValue 其中method就是getNextTrainCost() 即获取方法的返回值然后加了一个null
Collection collection = (Collection) method.invoke(object);
if (collection != null) {collection.addAll((Collection) value);
}
分析问题5–问题初步总结
第一个问题出在了HeroPersonality
中的getNextTrainCost
方法引用了getConfig
中的cost,导致在反序列化的时候每次会将json#nextTrainCost中反序列化出来的JSONArray#add到cost上(相当于调用了setter方法) 注意即使json#nextTrainCost
不是循环引用(fastjson可关闭),即值就是引用的config#cost值,则每次反序列化一样也会将反序列化出来的JSONArray#add到cost,只不过这次不会是null而已,第二个问题是最开始提到的我们正常序列化的时候就要带上IgnoreNonFieldGetter
这个参数,不要将非field的get方法给序列化上去,加上参数后,序列化后的json就没有nextTrainCost
了,那么也就不会有反序列化的问题了,因为压根就扫描不到,当然HeroPersonality
这个getNextTrainCost
也比较奇葩,用了引用的方式,其实完全没有必要,可以考虑干掉。
解决问题1–为什么cost会那么大
刚才我们已经基本肯定是因为错误模式下的反序列化会导致cost字段会越来越大,那么也不至于上亿次吧?这个我大概查了一下代码,很大几率是好友推荐模块和相关模块。相关代码需要较频繁的对于离线镜像反序列化或者存在类似心跳业务处理
解决办法
很简单,就是一定要记住fastjson序列化的时候要加上IgnoreNonFieldGetter
就可以了。
扩展
mat还有很多强大的使用功能:
Hisogram: list Number of instances per class
Dominator: TreeList the biggest objects and what they keep alive
Top Consumers: Print the most expensive objects grouped by class and by package
List Objects…
此外还可以查看线程、Class Loader Explorer等
PS:如果觉得我的分享不错,欢迎大家随手点赞、在看。
大家一起在评论区聊聊呗~
关注微信公众号:互联网架构师,在后台回复:2T,可以获取我整理的教程,都是干货。
猜你喜欢
1、GitHub 标星 3.2w!史上最全技术人员面试手册!FackBoo发起和总结
2、如何才能成为优秀的架构师?
3、从零开始搭建创业公司后台技术栈
4、程序员一般可以从什么平台接私活?
5、37岁程序员被裁,120天没找到工作,无奈去小公司,结果懵了...
6、滴滴业务中台构建实践,首次曝光
7、不认命,从10年流水线工人,到谷歌上班的程序媛,一位湖南妹子的励志故事
8、15张图看懂瞎忙和高效的区别
9、2T架构师学习资料干货分享
数数FastJson那些年犯下的'血案'...相关推荐
- 比尔盖茨痛数离婚:我确实出轨犯下大错,爱泼斯坦的事早该听她的
视学算法报道 编辑:David 拉燕 [新智元导读]与前妻分手一年.离婚九个月后,比尔盖茨再次回应:承认出轨犯下错误,应该为此承担责任,后悔未听前妻建议,不该和爱泼斯坦走得太近. 转眼间,比尔 ...
- 专业分享:“以数治税”金税四期背景下,企业税务风控与管理的思考
分享嘉宾:上海国家会计学院庞教授 百望云直播特邀上海国家会计学院庞教授带来了< "以数治税"金税四期背景下,企业税务风控与管理的思考>的主题分享. 庞教授主要为大家介绍 ...
- python画海绵宝宝_《1,2,3到动物园》数数书,适合幼儿园小班宝宝亲子共读,从游戏中了解数字的概念...
大家好,我是神桐妈妈,最近开始陆续给几个幼儿园做了有关绘本方面的师资培训,然后又有新的幼儿园要有了嵌入式幼儿园绘本馆,有了绘本,有了书香氛围,又有孩子们开始接触绘本,每天拿着一本绘本带回家,和爸爸或者 ...
- 【HDU3530】 [Sdoi2014]数数 (AC自动机+数位DP)
3530: [Sdoi2014]数数 Time Limit: 10 Sec Memory Limit: 512 MB Submit: 682 Solved: 364 Description 我们称 ...
- 幼儿园小班上计算机课 作业内容是手口一致,幼儿园1-10数字手口一致,对物数数教案...
活动名称 10以内的手口一致数数(正数.接着数) 教育目标 1.通过操作活动让孩子练习手口一致数数; 2.在操作活动中发展孩子的动手能力.口语表达能力. 教具组成 数学插板人手一套,教师演示板 活动过 ...
- 服务 700+ 厂商,接入 4000+游戏,数数科技 C+ 轮再融 1 亿元
8 月,游戏大数据分析服务商数数科技宣布正式完成 1 亿元 C+ 轮融资. 本轮融资的投资方为 GGV 纪源资本,资金将用于人才队伍建设.产品研发.国际化进程等方面.据了解,这是数数科技一年半内的第3 ...
- DBA解题集:学会数数
你会数数吗?很多人对这个问题大概是嗤之以鼻吧,因为小时候爸爸妈妈就教我们数天上的星星,地上的羊.因此大部分人第一反应我想应该是:"你在逗我吗?"也正是因为这种如此小儿科的简单到我们 ...
- 中国石油大学(北京)第三届“骏码杯”程序设计竞赛(同步赛)——C 小菲爱数数
题目链接:C-小菲爱数数_中国石油大学(北京)第三届"骏码杯"程序设计竞赛(同步赛) (nowcoder.com) 思路好像和题解不一样,但是a了. 主要思路:首先肯定是要先把素数 ...
- 菱形数阵c语言,二年级奥数数阵习题及参考答案.doc-资源下载在线文库www.lddoc.cn...
二年级 奥数 数阵习题及参考答案.doc 2016春季数学集训二队每周习题3参考答案星期一1.将自然数1,2,3,按下表的规律排列.问55应该出现在哪个字母所在的一列如果1.2.3.4所在的那行称作第 ...
- 华为手机8大超实用功能!省心省力省钱!数数你用过几个
由于种种客观因素,今年好多小伙伴都换了华为手机, 可换了之后,却有好多功能不会用,或者说还从没发现, 那实在是太暴殄天物啦! 本文就用动图来直观演示--那些不为人知的8大超实用功能吧! 涵盖学习工作生 ...
最新文章
- 是什么轮胎_为什么现在的车轮胎轮毂尺寸越来越大
- ERP与EWM集成配置-ERP端组织架构(二)
- treeview递归绑定的两种方法
- linux中怎么安装ded包_快速提示:如何在Linux中安装.deb和.tar文件 - push博客
- python数组切片教程_手把手numpy教程【二】——数组与切片
- 腾讯 PB 级大数据计算如何做到秒级?
- 汇编语言笔记(一):基础
- logstash-input-redis插件使用详解
- 常对象和常函数的关系 const
- python 绘制柱状图
- update关联其他表批量更新数据-跨数据库-跨服务器Update时关联表条件更新
- Redis 网络编程
- empty判断0会出的问题
- linux命令we,Linux 命令执行过程
- python 文本替换 速度_python 实现批量替换文本中的某部分内容
- 2020定额水平测算——“15定额工程”一键转换“20定额工程”
- layui多图片上传并限制上传的图片数量
- C语言实现格林威治时间转北京时间+根据日期计算星期几
- cbrt c语音_如何在C语言中实现功能重载?
- 【WPS】未安装VBA支持库,无法运行文档中的宏。如需要启用宏功能,请点击这里了解详情。