fastjson与一起堆内存溢出引发的'血案'

问题现象

QA同学反映登录不上服务器

排查问题1--日志

查看log,发现玩家登录的时候抛出了一个java.lang.OutOfMemoryError,大概出错代码是序列化一个PlayerMirror镜像数据保存到Redis的时候,但是在JSON.toJSONString的时候出现了错误。即序列化的时候需要扩容expandCapacity,但是内存不足。

又看了一下日志,有好几个OutOfMemoryError,都是类似于用fastjson序列化PlayerMirror报的错误,还看了一下server目录,发现了几个.hprof,即OOM时自动生成的dump文件,说明确实发生了堆内存溢出,因为启动参数增加了'-XX:+HeapDumpOnOutOfMemoryError':,错误日志如下:

atjava.lang.OutOfMemoryError.()V(OutOfMemoryError.java:48)

atcom.alibaba.fastjson.serializer.SerializeWriter.expandCapacity(I)V(SerializeWriter.java:249)

dump文件如下:

-rw-------1xxxx2043416350Nov24 11:37java_pid8068.hprof

-rw-------1xxxx2028797313Nov24 11:17java_pid4671.hprof

-rw-------1xxxx1477222612Nov23 23:25java_pid31563.hprof

排查问题2--JVM命令

使用了jvm命令(jstat和jmap)初步排查一下问题:

jstat看到老年代基本已经满了

jmap看到排名前两位的分别是Object[]和char[]

num     #instances         #bytesclassname

----------------------------------------------

1:146219741203672[Ljava.lang.Object;

2:2842356639498168[C

这种的结果,就能说明是有一个Object[]占了很大的空间,导致OOM,正常的jmap -histo结果前两位应该是char[]和byte[],而且占用空间远大于其他对象。

排查问题3--MAT

因为有hprof,所以只需要用专业的内存分析工具mat即可,mat->Open Heap Dump,打开dump文件后直接出来一个Getting Started Wizard->Leak Suspects Report,即内存泄露的报告,选择finish:

两个怀疑的问题:

其中有一个JSONArray的实例就占用了大约700M内存

另外一个是线程的local Variables占用了500M内存

MAT截图如下,两个怀疑点:

点开问题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有一个getConfig方法,而做序列化时这个同学忘记加SerializerFeature.IgnoreNonFieldGetter这个参数,所以导致这个不需要保存到Redis的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(且只有一行),非常大,普通的文本编辑器根本看不出来(UE可以,Notepad++基本上不行),而这个数据也是当序列化到了config#cost字段时,只有一个正常的数据,其他后面全部为null。

所以数据问题确认完毕:cost字段里面除了一个正常的JSONObject外,剩余的全部是null:

分析问题2--山重水复疑无路

尝试在本地重现这个问题,比想象要难一些。在我反复写本地测试代码重现的时候,出现了一道曙光(坚持就是胜利),我写了一个反序列化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(inti =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方法。

分析问题3--源代码跟踪

需要源代码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):

publicJSONArraygetNextTrainCost(){

return((PersonalityStrengthenConfig) getConfig()).cost;

}

@Override

publicDataConfigItemgetConfig(){

returnDataConfigService.getSettingById(PersonalityStrengthenConfig.class,

personalityLevel);

}

反序列化nextTrainCost:

// FieldDeserializer#setValue 其中method就是getNextTrainCost() 即获取方法的返回值然后加了一个null

Collection collection = (Collection) method.invoke(object);

if(collection !=null) {

collection.addAll((Collection) value);

}

分析问题4--问题初步总结

第一个问题出在了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也比较奇葩,用了引用的方式,其实完全没有必要,可以考虑干掉。

为什么cost会那么大?

刚才我们已经基本肯定是因为错误模式下的反序列化会导致cost字段会越来越大,那么也不至于上亿次吧?这个我大概查了一下代码,很大几率是好友推荐模块和相关模块,相关代码需要较频繁的对于离线镜像反序列化,或者存在类似心跳业务处理。解决办法很简单,就是一定要记住fastjson序列化的时候要加上IgnoreNonFieldGetter就可以了。

关注公众号领资料

搜索公众号【Java耕耘者】,回复【Java】,即可获取大量优质电子书和一份Java高级架构资料、Spring源码分析、Dubbo、Redis、Netty、zookeeper、Spring cloud、分布式等视频资料

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,589评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,615评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,933评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,976评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,999评论 6 393
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,775评论 1 307
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,474评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,359评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,854评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,007评论 3 338
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,146评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,826评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,484评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,029评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,153评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,420评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,107评论 2 356

推荐阅读更多精彩内容