1. 问题的发现
    听云监控显示JVM堆内存老年代经过多次FullGC仍然无法回收,gc次数过多且STW过长,会影响线上业务,暂时联系运维老师重启了服务器(-Xmx4G -Xms4G)
  2. 一次线上JVM内存泄露的排查解决_json

    一次线上JVM内存泄露的排查解决_json_02

  3. 问题排查
    通过导出的dump文件分析了解到占用内存最大的对象是alibaba.fastjson的ParseConfig对象及其内部的IdentityHashMap对象,当前现象与测试环境dump结果一致
  4. 一次线上JVM内存泄露的排查解决_ide_03

  5. ParseConfig及IdentityHashMap对象的作用项目中在使用fastjson进行反序列化的时候会调用parseObject(java.lang.String, java.lang.reflect.Type, com.alibaba.fastjson.parser.Feature...)方法
private Object getFromCache(Method realMethod, String str, ProceedingJoinPoint pjp, Object[] args, Cache cached,
String key) throws Throwable {
Object proceed;
try {
Type type = realMethod.getGenericReturnType();
proceed = JSON.parseObject(str, type);
} catch (Exception e) {
XLoggerUtil.sysErrLog("JSON字符串转换为对象失败 json = {}", e, str);
return proceed;
}

最终会调用到ParserConfig的getDeserializer()方法来获取反序列化器

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

public ObjectDeserializer getDeserializer(Type type) {
ObjectDeserializer deserializer = get(type);
...
deserializer = createJavaBeanDeserializer(clazz, type);
...
putDeserializer(type, deserializer);
}

com.alibaba.fastjson.parser.ParserConfig#putDeserializer

public void putDeserializer(Type type, ObjectDeserializer deserializer) {
...
if (mixin != null) {
...
} else {
this.deserializers.put(type, deserializer);
}
}

com.alibaba.fastjson.util.IdentityHashMap#put

public boolean put(K key, V value) {
final int hash = System.identityHashCode(key);
final int bucket = hash & indexMask;

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

Entry<K, V> entry = new Entry<K, V>(key, value, hash, buckets[bucket]);
buckets[bucket] = entry; // 并发是处理时会可能导致缓存丢失,但不影响正确性

return false;
}

com.alibaba.fastjson.util.IdentityHashMap.Entry

protected static final class Entry<K, V> {

public final int hashCode;
public final K key;
public V value;

public final Entry<K, V> next;

public Entry(K key, V value, int hash, Entry<K, V> next){
this.key = key;
this.value = value;
this.next = next;
this.hashCode = hash;
}
}

可以看到,IdentityHashMap作为Type与反序列化器的kv存储缓存对象

  1. 问题的复现
    项目中用到fastjson的位置只有RedisCacheAspect中将redis中缓存的字符串转换为对象。直接debug跟进到ObjectDeserializer deserializer = get(type)方法的时候发现即使是同一个方法中两次获取同一个缓存,每次取到的type并非同一个对象(aop代理每次获取到的对象不一样,而key的比较必须是两个内存地址相同的对象,即同一个对象,所以永远不会命中key),这就会导致每次都获取不到缓存的反序列化器,所以会一直创建Entry并追加到链表上面,导致每个bucket上的entry数量越来越多
  2. 解决方案引入内部缓存,缓存方法全路径名称对应的Type,不需要重复获取
/**
* 方法全路径对应的返回值类型Type
*/
private static ConcurrentHashMap<String, Type> methodFullNameWithType = new ConcurrentHashMap<>(128);

private Object getFromCache(Method realMethod, String methodFullName, String str, ProceedingJoinPoint pjp, Object[] args, Cache cached,
String key) throws Throwable {
Object proceed;
try {
Type type = getType(methodFullName, realMethod);
if (type == null) {
XLoggerUtil.log("未获取到方法全路径对应Type ==> methodFullName = {}", methodFullName);
return GeneralResult.warnMessage("缓存处理出错");
}
proceed = JSON.parseObject(str, type);
} catch (Exception e) {
XLoggerUtil.sysErrLog("JSON字符串转换为对象失败 json = {}", e, str);
return proceed;
}

private Type getType(String methodFullName, Method realMethod) {
if (StringUtils.isBlank(methodFullName)) {
return null;
}
try {
Parameter[] parameters = realMethod.getParameters();
if (parameters != null && parameters.length > 0) {
StringBuilder params = new StringBuilder(methodFullName);
for (Parameter parameter : parameters) {
params.append(".").append(parameter.getName());
}
methodFullName = params.toString();
}
Type type = methodFullNameWithType.get(methodFullName);
if (type == null) {
type = realMethod.getGenericReturnType();
methodFullNameWithType.put(methodFullName, type);
XLoggerUtil.debug("设置方法全路径对应Type ==> methodFullName = {}", methodFullName);
}
return type;
} catch (Exception e) {
XLoggerUtil.sysErrLog("获取方法全路径对应Type出错 ==> methodFullName = {}", e, methodFullName);
}
return null;
}
  1. 项目中普遍使用的是Jackson来进行对象序列化和反序列化,为什么这里采用fastjson?受限于同一个对象,我可能既作为入参,又作为出参,而且出入参中同一属性的格式不相同,如:
@Data
public class DeliverItem {
/**
* 审核日期
*/
@JsonDeserialize(using = InstantDeserializers.DateStringDeserializer.class)
@JsonSerialize(using = InstantSerializers.ShangHaiDateTimeStringSerializer.class)
private Instant auditTime;

...
}

上面对象序列化的时候时间会格式化为‘yyyy-MM-dd HH:mm:ss’格式,反序列化的时候时间格式为‘yyyy-MM-dd’,丢失了详细时间数据,所以最稳妥的办法就是采用一个毫不相关的序列化机制;

但是这并不是长久之计,只是由于目前项目未严格规范导致的这个问题,而严格规范起来又会引入很多对象DTO转换!