分析一個線上內存告警的問題時,發現了造成內存告警的原因是使用fastjson不當導致的。
分析dump發現com.alibaba.fastjson.util.IdentityHashMap$Entry對象比較多。
查找相關文檔
- fastjson IdentityHashMap 內存泄漏排查 (這篇文檔分析描述的情況與我們遇到的問題的原因一樣,是使用com.alibaba.fastjson.util.ParameterizedTypeImpl不當導致的)
- fastjon官方在很早的版本就修復過類似的問題,https://github.com/alibaba/fastjson/issues/849 ,相關代碼:https://github.com/alibaba/fastjson/commit/ef50a5b756a6cab1ab753f4a661bdfb0ccbd6b7e ,他們修復的這個bug是針對com.alibaba.fastjson.TypeReference,這個類實際也是基於com.alibaba.fastjson.util.ParameterizedTypeImpl的。
問題產生的原因分析
- com.alibaba.fastjson.ParserConfig定義一個字段用於緩存不同類的反序列化器,使用的是IdentityHashMap(IdentityHashMap使用的是==比較key的值,不同於HashMap使用equals比較),緩存是以Type為key:
private final IdentityHashMap<Type, ObjectDeserializer> deserializers = new IdentityHashMap<Type, ObjectDeserializer>();
- 而我們的業務代碼是在調用一個接口后將結果反序列化,然后每次都去創建一個ParameterizedTypeImpl實例,而fastjson針對每次創建的PamrameterizedTypeImpl都會作為一個key加入到deserizers中進行緩存。
所以,隨着不斷的請求發起,內存泄漏產生了。(上面提到的fastjson自身的bug修復就是針對不同的類型又采用了ConcurrentHashMap基於Class進行了一次緩存)// ... ... ParameterizedTypeImpl type = new ParameterizedTYpeImpl(new Type[]{ SomeInfo.class }, null, CommonVO.class); CommonVO<SomeInfo> result = (CommonVO<SomeInfo>)JSON.parseObject(jsonString, type);
問題修復
方法一:
由於這里主要只是因為泛型才用了ParameterizedTypeImp,並且只有這一處,所以可以簡單粗暴把這個定義為局部變量的type改為private static final的全局變量就可以避免內存泄漏了
private static final ParameterizedTypeImpl SOME_INFO_TYPE = ...
方法二:
使用com.alibaba.fastjson.TypeReference。
JSON.parseObject(json, new TypeReference<CommonVO<T>>(SomeInfo.class) {});
https://github.com/alibaba/fastjson/wiki/TypeReference
問題模擬重現
代碼:
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.util.ParameterizedTypeImpl;
import java.lang.reflect.Type;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicLong;
public class LeakDemo {
public static void main(String[] args) {
/*
-Xms30m
-Xmx30m
-XX:+PrintGCDateStamps
-XX:+PrintGCDetails
-XX:+PrintHeapAtGC
-XX:+PrintGCApplicationStoppedTime
-Xloggc:/tmp/gc_%p_%t_.log
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/tmp/
*/
final long start = System.currentTimeMillis();
final AtomicLong counter = new AtomicLong(0);
Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
@Override
public void run() {
System.out.println("count: " + counter.get());
System.out.println("took " + (System.currentTimeMillis() - start) + " ms");
}
}));
SomeInfo someInfo = new SomeInfo();
someInfo.setName("Tom");
CommonVO<SomeInfo> result = new CommonVO<>();
result.setData(someInfo);
result.setRetCode(0);
result.setMessage("Success");
String json = JSON.toJSONString(result);
// 模擬業務中不斷的接口請求處理
while (true) {
ParameterizedTypeImpl type = new ParameterizedTypeImpl(new Type[]{SomeInfo.class}, null, CommonVO.class);
CommonVO<SomeInfo> tmpResult = (CommonVO<SomeInfo>) JSON.parseObject(json, type);
Objects.requireNonNull(tmpResult);
counter.incrementAndGet();
}
}
public static class CommonVO<T> {
private int retCode;
private String message;
private T data;
public int getRetCode() {
return retCode;
}
public void setRetCode(int retCode) {
this.retCode = retCode;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
}
public static class SomeInfo {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
}
執行結果:
java.lang.OutOfMemoryError: GC overhead limit exceeded
Dumping heap to /tmp/java_pid13092.hprof ...
Heap dump file created [48333772 bytes in 0.402 secs]
Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
at java.util.zip.ZipCoder.getBytes(ZipCoder.java:80)
at java.util.zip.ZipFile.getEntry(ZipFile.java:306)
at java.util.jar.JarFile.getEntry(JarFile.java:227)
at java.util.jar.JarFile.getJarEntry(JarFile.java:210)
at sun.misc.URLClassPath$JarLoader.getResource(URLClassPath.java:840)
at sun.misc.URLClassPath$JarLoader.findResource(URLClassPath.java:818)
at sun.misc.URLClassPath$1.next(URLClassPath.java:226)
at sun.misc.URLClassPath$1.hasMoreElements(URLClassPath.java:236)
at java.net.URLClassLoader$3$1.run(URLClassLoader.java:583)
at java.net.URLClassLoader$3$1.run(URLClassLoader.java:581)
at java.security.AccessController.doPrivileged(Native Method)
at java.net.URLClassLoader$3.next(URLClassLoader.java:580)
at java.net.URLClassLoader$3.hasMoreElements(URLClassLoader.java:605)
at sun.misc.CompoundEnumeration.next(CompoundEnumeration.java:45)
at sun.misc.CompoundEnumeration.hasMoreElements(CompoundEnumeration.java:54)
at com.alibaba.fastjson.util.ServiceLoader.load(ServiceLoader.java:34)
at com.alibaba.fastjson.parser.ParserConfig.getDeserializer(ParserConfig.java:468)
at com.alibaba.fastjson.parser.ParserConfig.getDeserializer(ParserConfig.java:363)
at com.alibaba.fastjson.parser.DefaultJSONParser.parseObject(DefaultJSONParser.java:639)
at com.alibaba.fastjson.JSON.parseObject(JSON.java:350)
at com.alibaba.fastjson.JSON.parseObject(JSON.java:318)
at com.alibaba.fastjson.JSON.parseObject(JSON.java:281)
at LeakDemo.main(LeakDemo.java:45)
count: 17300
took 6332 ms