分析一個線上內存告警的問題時,發現了形成內存告警的緣由是使用fastjson不當致使的。 分析dump發現com.alibaba.fastjson.util.IdentityHashMap$Entry對象比較多。html
查找相關文檔
- 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中進行緩存。
// ... ... ParameterizedTypeImpl type = new ParameterizedTYpeImpl(new Type[]{ SomeInfo.class }, null, CommonVO.class); CommonVO<SomeInfo> result = (CommonVO<SomeInfo>)JSON.parseObject(jsonString, type);
因此,隨着不斷的請求發起,內存泄漏產生了。(上面提到的fastjson自身的bug修復就是針對不一樣的類型又採用了ConcurrentHashMap基於Class進行了一次緩存)
問題修復
方法一: 因爲這裏主要只是由於泛型才用了ParameterizedTypeImp,而且只有這一處,因此能夠簡單粗暴把這個定義爲局部變量的type改成private static final的全局變量就能夠避免內存泄漏了java
private static final ParameterizedTypeImpl SOME_INFO_TYPE = ...
方法二: 使用com.alibaba.fastjson.TypeReference。git
JSON.parseObject(json, new TypeReference<CommonVO<T>>(SomeInfo.class) {});
https://github.com/alibaba/fastjson/wiki/TypeReferencegithub
問題模擬重現
代碼:json
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