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

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

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

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

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

java 复制代码
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。

java 复制代码
jmap -dump:live,format=b,file=dump.bin <pid>

然后使用mat工具分析了一个这么多 JavaBeanDeserializer 对应的java类型都是什么?

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

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

什么情况下产生内存泄露

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

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

java 复制代码
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是否相同的依据是"==",条件十分苛刻。

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

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

java 复制代码
    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的代码

java 复制代码
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方法进行进行了重写:

java 复制代码
@Override
    public 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.equals
        if (!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;

    }

    @Override
    public 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改写后的代码如下:

java 复制代码
    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这两种工具,使用上面问题代码进行了反序列化压测,结果并没有出现内存泄露的问题,你知道为什么吗?

相关推荐
三84411 小时前
Fastjson 漏洞学习笔记 · 03 · 经典利用链:TemplatesImpl 与 JdbcRowSetImpl
java·web安全·fastjson
其实防守也摸鱼2 天前
Fastjson 反序列化漏洞(JNDI注入)
android·数据库·安全·oracle·自动化·fastjson·反序列化
三8442 天前
Fastjson 漏洞 · 01 · 认识 Fastjson 与序列化基础
json·fastjson·反序列化
XiaoLeisj1 个月前
Android Memory Profiler:堆内存指标、内存抖动与泄漏定位
android·性能优化·内存抖动·内存泄露·memory profiler
SuperherRo5 个月前
服务攻防-Java组件安全&FastJson&高版本JNDI&不出网C3P0&编码绕WAF&写入文件CI链
java·安全·fastjson·waf·不出网·高版本·写入文件
用户962377954486 个月前
代码审计 | FastJSON 1.2.47 不出网利用 —— BCEL 链分析 | setter 与 getter 触发对比
fastjson
庞轩px6 个月前
ThreadLocal 源码分析与内存泄漏问题
java·jvm·线程·threadlocal·内存泄露·key-value
刘大猫.6 个月前
java工具:《字符串转List》
list·json解析·fastjson·反序列化·jsonobject·jsonarray·typereference
雪碧聊技术8 个月前
JSON数据格式
json·fastjson
知无不研8 个月前
c语言动态内存规划
c语言·动态内存管理·内存泄露·基础知识·malloc·realloc·calloc