首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

一个Fastjson反序列化内存泄漏问题的分析与修复

一、问题背景与现象

某天半夜,某线上服务出现内存告警,某苦逼程序猿半夜起床,通过Heap Dump分析发现com.alibaba.fastjson.util.IdentityHashMap$Entry对象堆积,最终确认是由于Fastjson反序列化操作中不当使用ParameterizedTypeImpl导致的缓存泄漏。

该问题与Fastjson早期版本中TypeReference的设计缺陷类似([GitHub Issue#849]()),尽管官方已修复了TypeReference的问题,但类似逻辑的代码仍可能因误用ParameterizedTypeImpl引发内存泄漏。

二、技术细节分析

2.1 IdentityHashMap的设计风险

Fastjson的ParserConfig类通过IdentityHashMap<Type,ObjectDeserializer> deserializers缓存反序列化器,其核心机制如下:

• 键比较方式:

`IdentityHashMap`使用`==`(对象地址)而非`equals`比较键值。这意味着即使两个`Type`对象逻辑等价(如相同泛型类型),只要它们是不同实例,就会被视为不同的键。

• 缓存膨胀场景:

若每次反序列化时动态创建`Type`实例(如`ParameterizedTypeImpl`),每次新实例都会作为新键存入缓存,导致缓存条目无限增长。

2.2 ParameterizedTypeImpl的使用方式

ParameterizedTypeImpl是Fastjson内部用于表示泛型类型的类,例如CommonVO<SomeInfo>。

典型使用方式如下示例:

```java

// 动态创建泛型类型实例

ParameterizedTypeImpl type = new ParameterizedTypeImpl(

new Type[]{SomeInfo.class}, null, CommonVO.class

);

CommonVO<SomeInfo> result = JSON.parseObject(jsonString, type);

```

每次调用new ParameterizedTypeImpl会生成一个新的`Type`实例,即使其逻辑类型相同,也会被`IdentityHashMap`视为新键,从而持续占用内存。

2.3 Fastjson缓存机制源码解析

Fastjson的反序列化流程中,ParserConfig.getDeserializer(Type type)会优先从`deserializers`缓存中获取反序列化器。

若未命中,则创建新反序列化器并缓存:

```java

// Fastjson源码片段(简化)

public ObjectDeserializer getDeserializer(Type type) {

ObjectDeserializer deserializer = deserializers.get(type);

if (deserializer == null) {

deserializer = createDeserializer(type);

deserializers.put(type, deserializer); // 动态类型实例导致缓存膨胀

}

return deserializer;

}

```

三、问题复现代码

3.1 内存泄漏场景模拟

以下这个示例我们可以通过代码通过循环调用反序列化操作

模拟缓存泄漏:

```java

import com.alibaba.fastjson.JSON;

import com.alibaba.fastjson.util.ParameterizedTypeImpl;

import java.lang.reflect.Type;

import java.util.ArrayList;

import java.util.List;

public class FastjsonMemoryLeakDemo {

public static void main(String[] args) throws InterruptedException {

String json = "{\"data\":\"test\"}";

List<Type> typeCache = new ArrayList<>();

// 模拟持续请求,动态创建ParameterizedTypeImpl

for (int i = 0; i < 1000000; i++) {

// 每次循环生成新的ParameterizedTypeImpl实例

ParameterizedTypeImpl type = new ParameterizedTypeImpl(

new Type[]{String.class}, null, CommonVO.class

);

typeCache.add(type); // 强制保留引用,避免GC(实际场景中缓存由Fastjson持有)

JSON.parseObject(json, type);

Thread.sleep(10);

}

}

static class CommonVO<T> {

private T data;

// getter/setter省略

}

}

```

3.2 内存监控与验证

1. 运行参数:

添加JVM参数以观察内存变化:

```

-Xmx512m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=./fastjson-dump.hprof

```

2. 监控工具:

使用VisualVM或JProfiler监控堆内存,可观察到`IdentityHashMap$Entry`对象数量持续增长,最终触发OOM。

3. 运行日志:

随着程序运行,内存使用量持续增长,最终触发`OutOfMemoryError`时,打印的日志如下:

```

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space

at java.util.ArrayList.iterator(ArrayList.java:861)

at com.alibaba.fastjson.util.TypeUtils.compute(Class.java:2560)

at com.alibaba.fastjson.util.TypeUtils.<clinit>(Class.java:2551)

at com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer.<init>(JavaBeanDeserializer.java:91)

at com.alibaba.json.b.l.<init>(SourceFile:91)

at com.alibaba.json.b.f.a(SourceFile:278)

at com.alibaba.json.b.a.a(SourceFile:175)

at com.alibaba.json.b.a.a(SourceFile:150)

at com.alibaba.json.b.a.a(SourceFile:311)

at com.alibaba.fastjson.JSON.parseObject(JSON.java:372)

at com.FastjsonMemoryLeakDemo.main(FastjsonMemoryLeakDemo.java:24)

```

4. Heap Dump分析结果:

使用Mat工具分析生成的`fastjson-dump.hprof`文件,发现以下问题:

• 内存泄漏的根本原因:`com.alibaba.fastjson.util.IdentityHashMap$Entry`对象大量堆积。

• 泄漏的类型和数量:`com.alibaba.fastjson.util.IdentityHashMap$Entry`占用的内存高达80%以上。

四、修复方案与原理

4.1 方案一:

静态化ParameterizedTypeImpl将泛型类型定义为静态常量,避免重复创建:

代码举例:

```java

private static final ParameterizedTypeImpl CACHED_TYPE =

new ParameterizedTypeImpl(new Type[]{SomeInfo.class}, null, CommonVO.class);

public CommonVO<SomeInfo> parse(String json) {

return JSON.parseObject(json, CACHED_TYPE);

}

```

原理:静态变量保证`Type`实例唯一,`IdentityHashMap`中仅缓存一次。

4.2 方案二:

使用TypeReference利用Fastjson提供的`TypeReference`封装泛型类型:

代码举例:

```java

public CommonVO<SomeInfo> parse(String json) {

return JSON.parseObject(json, new TypeReference<CommonVO<SomeInfo>>(SomeInfo.class) {});

}

```

原理:

`TypeReference`内部通过`getSuperclassTypeParameter`方法缓存泛型类型(基于`Class`),避免重复创建`Type`实例。其核心源码如下:

核心源码示例:

```java

protected TypeReference(Type... actualTypeArguments) {

Type type = ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0];

this.type = TypeUtils.canonicalize(type); // 标准化处理,合并相同类型

}

```Fastjson源码修复:

```java

private static final ConcurrentHashMap<Type, Type> classTypeCache = new ConcurrentHashMap<>();

public static Type canonicalize(Type type) {

// 使用ConcurrentHashMap缓存标准化后的Type实例

Type canonized = classTypeCache.get(type);

if (canonized == null) {

classTypeCache.putIfAbsent(type, type);

canonized = classTypeCache.get(type);

}

return canonized;

}

```

• 关键点:通过`ConcurrentHashMap`以`equals`而非`==`作为键比较方式,合并逻辑相同的`Type`实例。

五、总结与最佳实践

• 避免动态创建Type实例:对于泛型反序列化,优先使用`TypeReference`或静态化`ParameterizedTypeImpl`。

• 升级Fastjson版本:确保使用已修复缓存的版本(>=1.2.36)。

• 监控缓存增长:定期检查`ParserConfig.deserializers`大小,或通过JMX暴露缓存状态。

So,当你了解了这些,希望你不会因为这个问题而半夜起床了。

  • 发表于:
  • 原文链接https://page.om.qq.com/page/OlB5IuZRJae3tvrHqUUJnSHQ0
  • 腾讯「腾讯云开发者社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

扫码

添加站长 进交流群

领取专属 10元无门槛券

私享最新 技术干货

扫码加入开发者社群
领券