暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

记一次fastjson导致的线上OOM事件

编程阁楼 2020-10-13
1987


01



事件起因


一个安定、祥和、舒适的周末,突然线上传来报警,快速保留现场过后,使用万能的重启大法临时解决了问题。一个难得的周末就这么被无情的淹没了,剩下的时间只能越快分析dump出来的堆栈信息。


02


迹象表现


使用IBM Heap Analyzer导入快照文件发现以下情况:

从如上可以看出,83.5%的内存都是这个对象com.alibaba.fastjson.util.IdentityHashMap,很明显,这又是万恶的fastjson的坑了。
来看故障现场的jstack信息,发现有以下信息:
“http-nio-8080-exec-197” #7490 daemon prio=5 os_prio=0 tid=0x00007fdd5806b000 nid=0xed1 waiting for monitor entry [0x00007fdd1b7d5000]
java.lang.Thread.State: BLOCKED (on object monitor)
at org.apache.catalina.webresources.CachedResource.validateResources(CachedResource.java:125)
- waiting to lock <0x000000008015c660> (a org.apache.catalina.webresources.CachedResource)
at org.apache.catalina.webresources.Cache.getResources(Cache.java:129)
at org.apache.catalina.webresources.StandardRoot.getResources(StandardRoot.java:315)
at org.apache.catalina.webresources.StandardRoot.getClassLoaderResources(StandardRoot.java:231)
at org.apache.catalina.loader.WebappClassLoaderBase.findResources(WebappClassLoaderBase.java:995)
at java.lang.ClassLoader.getResources(ClassLoader.java:1142)
at com.alibaba.fastjson.util.ServiceLoader.load(ServiceLoader.java:33)
at com.alibaba.fastjson.parser.ParserConfig.getDeserializer(ParserConfig.java:459)
at com.alibaba.fastjson.parser.ParserConfig.getDeserializer(ParserConfig.java:354)
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)

(熟悉java线程知识的同学都知道,这个线程状态为BLOCKED,说明线程在等待获取一个排它锁。)

通过上面的堆栈信息,暂且抱怀疑的态度,怀疑跟这个ParserConfig#getDeserializer方法关系。


03


根因分析

3.1. 定位出问题的代码

因为项目中使用到fastjson的地方小编比较清楚,主要是对远程接口返回的数据进行反序列化。小编直接异步到相关行看代码:
Type parameterizedTypeClass = ParameterizedTypeImpl.make(cl, types, null);
if (!result.contains(CAN_NOT_SERIALIZABLE_0) && !result.contains(CAN_NOT_SERIALIZABLE_1) && !result.contains(CAN_NOT_SERIALIZABLE_2)) {
MyParserConfig config = new MyParserConfig();
// 禁用ASM影响性能
config.setAsmEnable(false);
object = JSON.parseObject(result, parameterizedTypeClass, config);
}


// 泛型内套泛型以上parseObject可能返回null值
if (object == null) {
// 此处未提供对多种序列化协议的支持
object = PojoUtils.realize(JSONObject.parseObject(result, Map.class), cl, parameterizedTypeClass);
}
这里主要使用了fastjson的JSON.parseObject(result, parameterizedTypeClass, config),难道这个parseObject存在惊天大bug?
带着疑问,小编开始追踪源码。
--> com.alibaba.fastjson.JSON#parseObject(java.lang.String, java.lang.reflect.Type, com.alibaba.fastjson.parser.ParserConfig, com.alibaba.fastjson.parser.Feature...)


--> com.alibaba.fastjson.JSON#parseObject(java.lang.String, java.lang.reflect.Type, com.alibaba.fastjson.parser.ParserConfig, com.alibaba.fastjson.parser.deserializer.ParseProcess, int, com.alibaba.fastjson.parser.Feature...)


--> com.alibaba.fastjson.parser.DefaultJSONParser#parseObject(java.lang.reflect.Type, java.lang.Object)


--> com.alibaba.fastjson.parser.ParserConfig#getDeserializer(java.lang.reflect.Type)


--> com.alibaba.fastjson.parser.ParserConfig#get


--> com.alibaba.fastjson.util.IdentityHashMap#get


--> com.alibaba.fastjson.parser.ParserConfig#getDeserializer(java.lang.Class<?>, java.lang.reflect.Type)
以上代码基本上已经可以和内存快照上的IdentityHashMap和jstack中的ParserConfig#getDeserializer对上号了。



3.2. 深入剖析代码问题

继续看com.alibaba.fastjson.util.IdentityHashMap#get这个方法的实现:

public final V get(K key) {
final int hash = System.identityHashCode(key); // #1
final int bucket = hash & indexMask;


for (Entry<K, V> entry = buckets[bucket]; entry != null; entry = entry.next) {
if (key == entry.key) {
return (V) entry.value;
}
}


return null;
}
注意以上代码#1处使用的System.identityHashCode获取对象的原始hashCode值,即便重写了hashCode方法,这里获取到的也是原始值。看以下代码很容易理解hashCode和identityHashCode的区别了,这里就不做赘述。
 public static void main(String[] args) {
System.out.println(System.identityHashCode(new String()));
System.out.println(System.identityHashCode(new String()));

System.out.println(new String("zhangsan").hashCode());
System.out.println(new String("zhangsan").hashCode());
}


以上代码输出:
621009875
1265094477
-1432604556
-1432604556
再来看com.alibaba.fastjson.parser.ParserConfig#getDeserializer这个方法的代码:
public ObjectDeserializer getDeserializer(Class<?> clazz, Type type) {
ObjectDeserializer deserializer = get(type);
if (deserializer != null) {
return deserializer;
}


if (type == null) {
type = clazz;
}


deserializer = get(type);
if (deserializer != null) {
return deserializer;
}


{
JSONType annotation = TypeUtils.getAnnotation(clazz,JSONType.class);
if (annotation != null) {
Class<?> mappingTo = annotation.mappingTo();
if (mappingTo != Void.class) {
return getDeserializer(mappingTo, mappingTo);
}
}
}


if (type instanceof WildcardType || type instanceof TypeVariable || type instanceof ParameterizedType) {
deserializer = get(clazz);
}


if (deserializer != null) {
return deserializer;
}


for (Module module : modules) {
deserializer = module.createDeserializer(this, clazz);
if (deserializer != null) {
putDeserializer(type, deserializer);
return deserializer;
}
}


...此处省略1w行代码...


putDeserializer(type, deserializer);


return deserializer;
}
这部分代码比较长,中间部分小编省略了,有兴趣的读者可以自行查看fastjson源码。注意看这个com.alibaba.fastjson.parser.ParserConfig#putDeserializer方法:
public void putDeserializer(Type type, ObjectDeserializer deserializer) {
Type mixin = JSON.getMixInAnnotations(type);
if (mixin != null) {
IdentityHashMap<Type, ObjectDeserializer> mixInClasses = this.mixInDeserializers.get(type);
if (mixInClasses == null) {
//多线程下可能会重复创建,但不影响正确性
mixInClasses = new IdentityHashMap<Type, ObjectDeserializer>(4);
this.mixInDeserializers.put(type, mixInClasses);
}
mixInClasses.put(mixin, deserializer);
} else {
this.deserializers.put(type, deserializer);
}
}
debug追踪代码发现每次调用的都是这行:


而这个private final IdentityHashMap<Type, ObjectDeserializer> deserializers         = new IdentityHashMap<Type, ObjectDeserializer>();局部变量中的内容持续在增加中...



04


根因总结


综上分析,我们来总结下,出现该OOM现象的本质原因其实就是每次new一个ParameterizedTypeImpl,在调用JSON.parseObject时,判断deserializers中是否已经存在ParameterizedTypeImpl使用的是identityHashCode,而identityHashCode每次都不相同,从而每次new出来都要缓存在deserializers这个IdentityHashMap中,久而久之内存就被撑爆了。


05


解决方案


实际上,因为ParameterizedTypeImpl已经重写了hashCode方法,如果这里使用的hashCode来判断是否存在就没有问题了。第一想到的解决方案就是将com.alibaba.fastjson.util.IdentityHashMap#get方法修改了,但是一个好的开发习惯是基于扩展的方式解决问题,而非修改源码,所以该方案被否定。

那么有没有办法使得IdentityHashMap缓存尽量少的内容呢?答案是肯定的,我们可以使用二次缓存的理念来解决该问题。即在外部再增加一层缓存,该层缓存使用ParameterizedTypeImpl的hashCode进行判断对象是否已缓存。所以小编最终的解决方案是如下代码所示:
ConcurrentMap<Type, Type> classTypeCache
= new ConcurrentHashMap<Type, Type>(16, 0.75f, 1);

Type parameterizedTypeClass = ParameterizedTypeImpl.make(cl, types, null);
Type cachedType = classTypeCache.get(parameterizedTypeClass);
if (cachedType == null) {
classTypeCache.putIfAbsent(parameterizedTypeClass, parameterizedTypeClass);
cachedType = classTypeCache.get(parameterizedTypeClass);
}
if (!result.contains(CAN_NOT_SERIALIZABLE_0) && !result.contains(CAN_NOT_SERIALIZABLE_1) && !result.contains(CAN_NOT_SERIALIZABLE_2)) {
MyParserConfig config = new MyParserConfig();
// 禁用ASM影响性能
// config.setAsmEnable(false);
object = JSON.parseObject(result, cachedType, config);
}


// 泛型内套泛型以上parseObject返回null值,通过以下方式弥补
if (object == null) {
// 此处未提供对多种序列化协议的支持
object = PojoUtils.realize(JSONObject.parseObject(result, Map.class), cl, cachedType);
}
重点是引入了classTypeCache缓存,经过压测验证,内存已不再会出现OOM了。

读者如果想测试这个问题可以参考以下代码:
package test;


import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.util.ParameterizedTypeImpl;


import java.lang.reflect.Type;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicLong;




public class FastjsonLeakDemo {


// static ConcurrentMap<Type, Type> classTypeCache
// = new ConcurrentHashMap<Type, Type>(16, 0.75f, 1);


public static void main(String[] args) {
System.out.println(System.identityHashCode(new String()));
System.out.println(System.identityHashCode(new String()));


System.out.println(new String("zhangsan").hashCode());
System.out.println(new String("zhangsan").hashCode());


/*
-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");
}
}));


UserInfo someInfo = new UserInfo();
someInfo.setName("Tom");
Response<UserInfo> result = new Response<>();
result.setData(someInfo);
result.setRetCode(0);
result.setMessage("Success");


String json = JSON.toJSONString(result);


// 模拟请求
while (true) {
ParameterizedTypeImpl type = new ParameterizedTypeImpl(new Type[]{UserInfo.class}, null, Response.class);
// Type cachedType = classTypeCache.get(type);
// if (cachedType == null) {
// classTypeCache.putIfAbsent(type, type);
// cachedType = classTypeCache.get(type);
// }
Response<UserInfo> tmpResult = (Response<UserInfo>) JSON.parseObject(json, type);
Objects.requireNonNull(tmpResult);


counter.incrementAndGet();
}
}


public static class Response<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 UserInfo {
private String name;


public String getName() {
return name;
}


public void setName(String name) {
this.name = name;
}
    }


}

文章转载自编程阁楼,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论