fastjson|fastJson 与一起堆内存溢出‘血案

现象
QA同学反映登录不上服务器
排查问题1–日志级别
查看log,发现玩家登录的时候抛出了一个java.lang.OutOfMemoryError
大概代码是向Redis序列化一个PlayerMirror镜像数据,但是在JSON.toJSONString的时候出现了错误.比较清晰 , 即序列化的时候expandCapacity,内存不足 。
又看了一下日志 , 有好几个OutOfMemoryError,都是类似于用fastjson序列化PlayerMirror报的错误
又仔细看了一下server目录 , 发现了几个.hprof,说明确实发生了堆内存溢出 , 因为启动参数增加了’-XX:+HeapDumpOnOutOfMemoryError’
排查问题2–JVM命令级别
使用了jvm命令初步排查一下问题 jstat -gcutil pid
jstat -gc pid
jmap -histo pid
jmap -heap pid
jstat看到老年代基本已经满了
jmap看到排名前两位的分别是Object[]和char[]
排查问题3–专业工具级别
因为了hprof,所以只需要用专业的内存分析工具mat即可 mat#Open Heap Dump,载入后直接出来一个Getting Started Wizard#Leak Suspects Report,即内存泄露的报告,选择finish 两个怀疑的问题:
其中有一个JSONArray的实例就占用了大约700M内存
另外一个是线程的local Variables占用了500M内存
fastjson|fastJson 与一起堆内存溢出‘血案
本文图片
点开问题1详情 , 发现这个JSONArray是配置类PersonalityStrengthenConfig#cost字段 , 仔细看一下这个JSONArray#list#elementData的数组长度是可怕的183842095 。
fastjson|fastJson 与一起堆内存溢出‘血案
本文图片
fastjson|fastJson 与一起堆内存溢出‘血案
本文图片
结合两个问题 , 比较能容易的想到答案 , 中的cost字段(JSONArray)占用了大量的内存 , 而玩家下线或者上线的时候要序列化一部分数据到redis,其中就包括这个 , 所以也要序列化这个超级大的cost , 而序列化要申请空间 , 所以就内存溢出了 。
分析问题1–观察数据
为什么数据配置类PersonalityStrengthenConfig会被序列化呢 , 因为玩家下线的时候需要序列化一个玩家镜像数据到有一个get方法,而做序列化的这个同学忘记加了这个参数 , 所以导致getConfig中的这个config对象被序列化进去了 , 修改完毕代码后 , 所有的问题都没有了 。
需要确认一下:为什么占这么大空间 , 能看一下里面都是什么?
在mat中怀疑的第一个怀疑报告中点击对象,左侧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 。
fastjson|fastJson 与一起堆内存溢出‘血案
本文图片
分析问题2–尝试重现
最初的解决方法很简单 尝试通过代码方式能否复现
即new一个HeroPersonality,其内部有一个getConfig,使用没有加IgnoreNonFieldGetter的方式序列化,看是否会造成大内存的占用