一、问题背景与现象
某天半夜,某线上服务出现内存告警,某苦逼程序猿半夜起床,通过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,当你了解了这些,希望你不会因为这个问题而半夜起床了。
领取专属 10元无门槛券
私享最新 技术干货