FastJson竟然会导致内存泄露?你遇到过吗?

FastJson是一款性能优异的java序列化和反序列框架,广泛应用于日常开发工作中,也许正是因为作者在设计这款框架时,比较注重性能方面的考量,在框架安全性,空间占用等方面做了一些牺牲。

很不幸小编前两天就遇到了一个使用FastJson导致内容泄漏的问题。下面是小编从发现内存泄漏问题,到问题排查,再到问题修复的整个过程的记录,当各位读者遇到类似问题后,能有所启发。

某天线上告警,系统jvm堆内存使用率过高,后台登录机器发现jvm 比较gc频繁,而且gc后,对内存占用并没有明显减少,初步怀疑出现了"内存泄漏"。

于是,使用jmap命令查看了一下当前堆内存的信息:

jamp -histo <pid> | head -n 20

结果如下图

在这里插入图片描述

经过多次GC后,上图中的主要对象的数量不降反增,说明系统的确存在内存泄漏问题,而且是FastJson内部的对象。

到这里,第一个问题出现了:上面的FieldInfo,DefaultFieldDeserializer,JavaBeanDeserializer 这些类是干啥的呢?

fastJson反序列化的流程

要了解 FieldInfo,DefaultFieldDeserializer,JavaBeanDeserializer 类的作用,我们需要先了解一下FastJson进行反序列化的流程。

FastJson在将json字符串反序列化成java对象的过程中,会为这个java类生成一个反序列化器,也就是 JavaBeanDeserializer,

同时FastJson还会给这个java类的每个字段都会被封装成一个FeildInfo对象,并且为每个字段也会生成一个 针对字段的反序列化器 DefaultFieldDeserializer

当对一个java类 进行反序列化时,先用字段反序列化器,反序列化出每个字段,然后再用 JavaBeanDeserializer 反序列化出对象。

上面涉及到的FastJson内部类的关系可以参考下图:

在这里插入图片描述

简单来说,就是在反序列化时,一个java类型会对应一个 JavaBeanDeserializer 和 多个 DefaultFieldDeserializer,而且fastJson内部会使用一个类似Map的KV结构 IdentityHashMap 将 类型和 JavaBeanDeserializer进行缓存,防止每次使用时都需要创建,来提升性能。

为什么会产生内存泄露

这一切都看似很合理,但是为什么会出现上图中产生的内存泄漏呢?

在上图中 堆中存在大量的 JavaBeanDeserializer 对象,按照上文讲到的 JavaBeanDeserializer 和类型的对应关系,那么业务系统中 也就会有对应数量的 java类,但是业务系统中其实并没有这么多java类型。

到这里,第二个问题出现了:IdentityHashMap的缓存没有生效吗?

这个时候,我使用了 jmap dump命令将此时内存空间进行了 dump。

jmap -dump:live,format=b,file=dump.bin <pid>

然后使用mat工具分析了一个这么多 JavaBeanDeserializer 对应的java类型都是什么?
在这里插入图片描述

经过分析发现了问题,大量的 JavaBeanDeserializer 对应的 java class 都是一样的。

到这里我们基本上可以得出结论:由于中存在某种问题,产生了java类型一样,但是保存到 IdentityHashMapkey不同,导致 IdentityHashMap 认为他们是不同的数据型,导致大量 相同的 JavaBeanDeserializer 保存到了 IdentityHashMap 中,进而也会存在更多的 DefaultFieldDeserializer ,因为这些 JavaBeanDeserializer 一直被 IdentityHashMap 引用,所以无法被gc回收,也就产生了 内存泄漏。

什么情况下产生内存泄露

知道了问题产生的原因,那接下来就要看下业务系统到底存在什么bug,才会产生很多类型相同但对 IdentityHashMap 而言 Key 却不同的问题呢?

在排查业务系统的bug之前,还要确认下 IdentityHashMap 判断两个Key相同的标准是什么?具体可以看一下代码

public boolean put(K key, V value) {final int hash = System.identityHashCode(key);final int bucket = hash & indexMask;for (Entry<K, V> entry = buckets[bucket]; entry != null; entry = entry.next) {if (key == entry.key) { // 使用 == 进行相同判断entry.value = value;return true;}}Entry<K, V> entry = new Entry<K, V>(key, value, hash, buckets[bucket]);buckets[bucket] = entry;  return false;
}

看以上代码可以发现 IdentityHashMap 判断两个key是否相同的依据是”==“,条件十分苛刻。

接下来就要看一下,业务系统中在进行反序列化时,使用的具体类型是什么,是不是么有满足 上面判断相同的条件呢?

下面是业务系统的伪代码:

    private static void des2(String json) {ParameterizedTypeImpl type = new ParameterizedTypeImpl(new Type[]{SomeInfo.class}, null, CommonVO.class);CommonVO<SomeInfo> tmpResult =  JSON.parseObject(json, type);System.out.println(tmpResult);}

果然有问题,每次进行序列化时,都创建了一个新的数据类型,这也就导致了IdentityHashMap缓存失效,罪魁祸首在这里。

如何解决

知道了问题,修改起来就很简单了:将数据类型作为一个对象属性,或者一个类属性,防止每次调用desc2时,都创建一个新的类型,修改后内存泄漏问题,也就迎刃而解了。

不过除了以上解决方案,FastJson也提供了一个解决方案,使用 TypeRefrence,及时每次序列化化都创建一个 对应的对象也不会产生内存泄漏的问题,为什么 TypeRefrence 不会出现问题呢?
源码之下无秘密,以下TypeRefreen的代码

protected TypeReference(){Type superClass = getClass().getGenericSuperclass();Type type = ((ParameterizedType) superClass).getActualTypeArguments()[0];Type cachedType = classTypeCache.get(type);if (cachedType == null) {classTypeCache.putIfAbsent(type, type);cachedType = classTypeCache.get(type);}this.type = cachedType;}

这里使用了一个 ConcurrentMap 对类型进行缓存,我们知道 ConcurrentMap 判断key相同的依据是 key的hashcode是否一样, 而这里的 ParameterizedType对hashcode和equal方法进行进行了重写:

@Overridepublic boolean equals(Object o) {if (this == o) return true;if (o == null || getClass() != o.getClass()) return false;ParameterizedTypeImpl that = (ParameterizedTypeImpl) o;// Probably incorrect - comparing Object[] arrays with Arrays.equalsif (!Arrays.equals(actualTypeArguments, that.actualTypeArguments)) return false;if (ownerType != null ? !ownerType.equals(that.ownerType) : that.ownerType != null) return false;return rawType != null ? rawType.equals(that.rawType) : that.rawType == null;}@Overridepublic int hashCode() {int result = actualTypeArguments != null ? Arrays.hashCode(actualTypeArguments) : 0;result = 31 * result + (ownerType != null ? ownerType.hashCode() : 0);result = 31 * result + (rawType != null ? rawType.hashCode() : 0);return result;}

使用TypeRefrence改写后的代码如下:

    private static void des3(String json) {Type type = new TypeReference<ResultDTO<OEVideoCreateResultDTO>>() {}.getType();CommonVO<SomeInfo> tmpResult =  JSON.parseObject(json, type);System.out.println(tmpResult);}

只要数据类型相同,那么key就是相同的,这也就保证了,只要数据类型相同,即使每次都是用新的 TypeRefrence 也不会产生上面的内存泄漏问题。

最后小编还尝试了GSON和 Jackson这两种工具,使用上面问题代码进行了反序列化压测,结果并没有出现内存泄露的问题,你知道为什么吗?

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

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

相关文章

RHCSA --- Linux存储管理

存储管理 Boot&#xff1a;可引导操作系统的分区&#xff08;必须是主分区&#xff09; 分区 ll /dev/nvme0n* 表示的是 nvme接口的磁盘 0n1 1 0n2 2 0n3 3 brw-rw----. 1 root disk 259, 0 Nov 15 19:31 /dev/nvme0n1 磁盘1 brw-rw----. 1 ro…

《洛谷深入浅出基础篇》P5266 学籍管理——map的应用

上链接&#xff1a;P5266 【深基17.例6】学籍管理 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)https://www.luogu.com.cn/problem/P5266#submit 题干&#xff1a; 题目描述 您要设计一个学籍管理系统&#xff0c;最开始学籍数据是空的&#xff0c;然后该系统能够支持下面的…

存储过程与触发器

一、存储过程 1.1 概念 把需要重复执行的内容放在存储过程中&#xff0c;实现代码的复用。 create procedure 创建存储过程的关键字 my_proc1:存储过程的名字。 执行下例代码就是创建了一个存储过程 执行存储过程&#xff0c;就是把上图的插入语句重复执行&#xff0c;现…

go学习之简单项目

项目 文章目录 项目1.项目开发流程图2.家庭收支记账软件项目2&#xff09;项目代码实现3&#xff09;具体功能实现 3.客户信息管理系统1&#xff09;项目需求说明2&#xff09;界面设计3&#xff09;项目框架图4&#xff09;流程5&#xff09;完成显示客户列表的功能6&#xff…

光敏传感器模块(YH-LDR)

目录 1. YH-LDR模块说明 1.1 简介 1.2 YH-LDR 模块的引脚说明 1.3 LDR 传感器工作原理与输出特性 2. 使用单片机系统控制 YH-LDR 模块 2.1 通用控制说明 1. YH-LDR模块说明 1.1 简介 YH-LDR 是野火设计的光强传感器&#xff0c;使用一个光敏电阻作为采集源&#x…

Kafka-4.1-工作原理综述

1 Kafka工作原理详解 1.1 工作流程 Kafka集群将 Record 流存储在称为 Topic 的类中&#xff0c;每个记录由⼀个键、⼀个值和⼀个时间戳组成。 Kafka 中消息是以 Topic 进⾏分类的&#xff0c;⽣产者⽣产消息&#xff0c;消费者消费消息&#xff0c;⾯向的都是同⼀个Topic。Topi…

【JavaEE初阶】计算机是如何工作的

一、计算机发展史 计算的需求在⼈类的历史中是广泛存在的&#xff0c;发展大体经历了从⼀般计算⼯具到机械计算机到目前的电子计算机的发展历程。 人类对计算的需求&#xff0c;驱动我们不断的发明、改善计算机。目前这个时代是“电子计算机”的时代&#xff0c;发展的潮流是…

快速支持客户知识库的核心优势是什么?

快速支持客户知识库是一个集中存储和组织企业知识的平台&#xff0c;包含了丰富的信息和解决方案&#xff0c;以帮助客户快速解决问题&#xff0c;帮助企业提高客户支持效率和满意度。那么&#xff0c;快速支持客户知识库的核心优势是什么呢&#xff1f; | 1、提高客户自助支持…

基于Springboot的地方美食分享网站(有报告)。Javaee项目,springboot项目。

演示视频&#xff1a; 基于Springboot的地方美食分享网站(有报告)。Javaee项目&#xff0c;springboot项目。 前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到网站。 项目介绍&#xff1a; 采用…

贝锐蒲公英助力智慧楼宇,实现自控系统远程运维、数据实时监测

在智慧楼宇系统中&#xff0c;存在着多套不同的系统&#xff0c;比如&#xff1a;智能照明控制、智能空调控制、智能安防监控等。在实际应用中&#xff0c;除了需要打通楼内各个系统实现智能联动&#xff0c;如何实现各地多楼宇的数据实时互通构建智慧楼宇生态系统也是需要解决…

JAVA小游戏 “拼图”

第一步是创建项目 项目名自拟 第二部创建个包名 来规范class 然后是创建类 创建一个代码类 和一个运行类 代码如下&#xff1a; package heima;import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.KeyEvent; import jav…

Spring IOC - Bean的生命周期之依赖注入

在Spring启动流程中&#xff0c;创建的factoryBean是DefaultListableBeanFactory&#xff0c;其类图如下所示&#xff1a; 可以看到其直接父类是AbstractAutoireCapableBeanFactory&#xff0c;他主要负责完成Bean的自动装配和创建工作。 具体来说&#xff0c;AbstractAutowire…