- 问题的发现
听云监控显示JVM堆内存老年代经过多次FullGC仍然无法回收,gc次数过多且STW过长,会影响线上业务,暂时联系运维老师重启了服务器(-Xmx4G -Xms4G) - 问题排查
通过导出的dump文件分析了解到占用内存最大的对象是alibaba.fastjson的ParseConfig对象及其内部的IdentityHashMap对象,当前现象与测试环境dump结果一致 - 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存储缓存对象
- 问题的复现
项目中用到fastjson的位置只有RedisCacheAspect中将redis中缓存的字符串转换为对象。直接debug跟进到ObjectDeserializer deserializer = get(type)方法的时候发现即使是同一个方法中两次获取同一个缓存,每次取到的type并非同一个对象(aop代理每次获取到的对象不一样,而key的比较必须是两个内存地址相同的对象,即同一个对象,所以永远不会命中key),这就会导致每次都获取不到缓存的反序列化器,所以会一直创建Entry并追加到链表上面,导致每个bucket上的entry数量越来越多
- 解决方案
引入内部缓存,缓存方法全路径名称对应的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; }
- 项目中普遍使用的是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转换!