Java反序列化json内存溢出_fastJson与一起堆内存溢出'血案'

现象

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

排查问题1–日志级别

查看log,发现玩家登录的时候抛出了一个java.lang.OutOfMemoryError

大概代码是向Redis序列化一个PlayerMirror镜像数据,但是在JSON.toJSONString的时候出现了错误.比较清晰,即序列化的时候expandCapacity,内存不足。

又看了一下日志,有好几个OutOfMemoryError,都是类似于用fastjson序列化PlayerMirror报的错误

又仔细看了一下server目录,发现了几个.hprof,说明确实发生了堆内存溢出,因为启动参数增加了’-XX:+HeapDumpOnOutOfMemoryError’

at java.lang.OutOfMemoryError.()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内存

f238e6b3dca3335c5dc46349efb58aad.png

点开问题1详情,发现这个JSONArray是配置类PersonalityStrengthenConfig#cost字段,仔细看一下这个JSONArray#list#elementData的数组长度是可怕的183842095。

405362cf5c5435cba39a1ddc7bd7cfcf.png

点开问题2详情,第一张图可以看到,fastjson内部的SerializeWriter中中buf#char[]长度竟然是可怕的262012306,而第二种图的堆栈信息可以看到是在序列化PersonalityStrengthenConfig抛出的内存溢出。

744485ebbfbcc4d14fc563a9b0e42903.png

780b24218edc67c36312310541c6568b.png

结合两个问题,比较能容易的想到答案,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。

dfa7cf6ab9e189a6428f7ed46d70319c.png

分析问题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);复制

结果输出:

27f628a240b4faf77cc0dee4940874ed.png

这个结果让我惊喜,让我非常的肯定,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等

114124530ab4a41c9f463851bdf4c8a7.png

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

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

相关文章

c语言中预处理器是什么

点击上方蓝字关注我&#xff0c;了解更多咨询1、C语言有预处理器&#xff0c;Java中没有这个概念&#xff0c;其实只是文本替换工具。2、C的预处理器&#xff0c;即CPP&#xff0c;将在实际编译器中完成处理&#xff0c;所有预处理命令将从#开始。实例#include <stdio.h>…

愚弄dnn_不要被泛型和向后兼容性所愚弄。 使用泛型类型

愚弄dnn最近&#xff0c;我与jOOQ的早期客户Ergon的 Sebastian Gruber进行了非常有趣的讨论&#xff0c;我们与之保持了密切联系。 与Sebastian交谈使我们的工程团队得出了一个结论&#xff0c;即我们应该完全重写jOOQ API。 现在&#xff0c;我们已经有很多用于各种用途的泛型…

php asp.net 代码量少,.NET_asp.net 反射减少代码书写量, 复制代码 代码如下:public b - phpStudy...

asp.net 反射减少代码书写量public bool Add(Liuyan refmodel){string sql "insert into liuyan(name,phone,zhiwei,gongsi,addr,country,dianyou,content,adddate)values(name,phone,zhiwei,gongsi,addr,country,dianyou,content,adddate)";OleDbParameter[] param…

c语言strcat_s函数如何使用

点击上方蓝字关注我&#xff0c;了解更多咨询1、strcat_s函数将strSource指向的字符串添加到其它字符串结尾。因此需要确保strDestination有足够的内存空间来容纳strSource和strDestination两个字符串&#xff0c;否则会导致溢出错误。2、strDestination末端的\0将被覆盖。strS…

502无法解析服务器标头_编写下载服务器。 第二部分:标头:Last-Modified,ETag和If-None-Match...

502无法解析服务器标头客户端缓存是万维网的基础之一。 服务器应通知客户端资源的有效性&#xff0c;客户端应尽可能快地对其进行缓存。 如我们所见&#xff0c;如果不缓存Web&#xff0c;它将非常缓慢。 只需在任何网站上Ctrl F5并将其与普通F5进行比较-后者就会更快&#xf…

c语言strcat_s函数的原理

点击上方蓝字关注我&#xff0c;了解更多咨询1、dst 内存空间大小目标字符串长度原始字符串场地‘\0’。2、使用sizeof函数获取内存空间大小&#xff0c;strlen函数获取字符串长度。即获取内存空间大小和查字符串长度。实例#include "stdafx.h" #include<stdlib.h…

java开发用怎么软件开发_Java 9中的5个功能将改变您开发软件的方式(还有2个不会)...

java开发用怎么软件开发有望在Java 9中发布的最令人兴奋的功能是什么&#xff1f; 近期不要对Java 9相对沉默而分心。JDK提交者正在努力准备下一个版本&#xff0c;预计在2015年12月才完成功能的下一个版本。此后&#xff0c;它将通过严格的测试和错误修复了将其准备于一般可用…

php redis并发读写,PHP使用Redis实现防止大并发下二次写入的方法

本文实例讲述了PHP使用Redis实现防止大并发下二次写入的方法。分享给大家供大家参考&#xff0c;具体如下&#xff1a;PHP调用redis进行读写操作&#xff0c;大并发下会出现&#xff1a;读取key1&#xff0c;没有内容则写入内容&#xff0c;但是大并发下会出现同时多个php进程写…

理解C语言指针概念只需几分钟

点击上方蓝字关注我&#xff0c;了解更多咨询当我们声明一个变量或常量时&#xff0c;计算机系统会为这个变量或常量分配存储单元&#xff0c;变量的数据存储到被分配的存储单元内&#xff0c;对变量的赋值和取值操作都是针对存储单元的操作。C编译器是如何通过变量找到与其对应…

php做一个网页的源代码,用HTML5做一个个人网站此文仅展示个人主页界面。内附源代码下载地址...

下载说明&#xff1a; 1.再好的作品都不如将来要做的作品。在每一次的设计当中都能有所收获&#xff0c;才是设计师在web开发中最得益的。 2.本站所有作品均是杨青个人设计。如果发现模板有错&#xff0c;请尽情谅解。 3.如果遇到什么问htmlcss编写的个人主页&#xff0c;适合初…

jax-rs jax-ws_JAX-RS 2.x与Spring MVC:返回对象列表的XML表示

jax-rs jax-wsJSON是所有REST * API的王者&#xff0c;但是您仍然可能需要公开多种表示形式&#xff0c;包括XML。 使用JAX-RS和Spring MVC都非常简单。 实际上&#xff0c;唯一要做的就是用JAXB注释对从API调用返回的POJO进行注释。 但是在我看来&#xff0c;序列化对象列表时…

C语言中经典的程序设计结构:顺序、条件、循环

点击上方蓝字关注我&#xff0c;了解更多咨询无论使用何种编程语言&#xff0c;都含有程序设计的三大经典结构。即&#xff1a;顺序结构、条件结构和循环结构&#xff0c;C语言也是如此。综述顺序结构&#xff0c;就是一条大路走到底&#xff0c;没有岔路口&#xff0c;一步步从…

php protected const,关于const:PHP类常量 – 公共,私有还是受保护?

假设常量属性是自动公开的&#xff0c;对吗&#xff1f;有没有办法让它们成为私有的或受保护的&#xff1f;事先谢谢。隐藏它们的原因是什么&#xff1f;即使它们是公开的——它们是只读的。常量应该是公共的&#xff0c;因为它们描述的是关于类的不可变事实&#xff0c;而不是…

7-8垃圾箱分布_您认为有关垃圾收集的7件事-完全错了

7-8垃圾箱分布关于Java Garbage Collection的最大误解是什么&#xff1f;它的真实情况如何&#xff1f; 小时候&#xff0c;我的父母曾经告诉我&#xff0c;如果我学习不好&#xff0c;我将成为垃圾收集者。 他们几乎不知道&#xff0c;垃圾回收实际上是很棒的。 也许这就是为…

c语言中abort函数的使用

点击上方蓝字关注我&#xff0c;了解更多咨询1、abort函数的作用是异常终止一个进程&#xff0c;意味着abort后面的代码将不再执行。2、当调用abort函数时&#xff0c;会导致程序异常终止&#xff0c;而不会进行一些常规的清除工作。实例#include <stdio.h> #include <…

php 异常值检测,PHP中的错误处理、异常处理机制分析

例&#xff1a;$a fopen(test.txt,r);//这里并没有对文件进行判断就打开了&#xff0c;如果文件不存在就会报错?>那么正确的写法应该如下&#xff1a;if(file_exists(test.txt)){$ffopen(test.txt,r);//使用完后关闭fclose($f);}?>一、PHP错误处理的三种方式A、简单的…

c语言中如何防止数组下标越界

点击上方蓝字关注我&#xff0c;了解更多咨询1、若数组长度和下标访问值出现错误&#xff0c;则会导致数组下标越界。数组下标从0开始&#xff0c;访问值为-1。2、在使用循环遍历数组元素时&#xff0c;注意防范off-by-one的错误。对于作为函数参数传入的数组下标&#xff0c;要…

java oauth2.0_教程:如何实现Java OAuth 2.0以使用GitHub和Google登录

java oauth2.0将Google和GitHub OAuth登录添加到Java应用程序的指南 我们添加到Takipi的最新功能之一是3rd party登录。 如果您像我一样懒惰&#xff0c;那么我想您也希望跳过填写表单和输入新密码的操作 。 只要有权限&#xff0c;许多人都希望使用第三方登录&#xff0c;只要…

c语言中数组访问越界如何理解

点击上方蓝字关注我&#xff0c;了解更多咨询1、可以通过数组下标直接访问数组中的元素。2、如果一个数组被定义为n个元素&#xff0c;那么访问n个元素是合法的。如果访问n个元素以外&#xff0c;则是非法的&#xff0c;称为访问越界。实例int a[5] {0}; //等价 int a[5] {0,…

php全部公开课,PHP公开课|这篇PHP的each()函数教学数,只为了帮你的PHP会学的更好...

【摘要】PHP作为一种超文本预处理器&#xff0c;已经成为了我们常用的网站编程语言&#xff0c;并且结合了C语言&#xff0c;Java等我们常见的编程语言&#xff0c;所以&#xff0c;有很多web开发领域的新人都看中了他的使用广泛性&#xff0c;有很多人都想了解php的内容&#…