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

3 篇文章 0 订阅
  1. 问题的发现
    听云监控显示JVM堆内存老年代经过多次FullGC仍然无法回收,gc次数过多且STW过长,会影响线上业务,暂时联系运维老师重启了服务器(-Xmx4G -Xms4G)
  2. 问题排查
    通过导出的dump文件分析了解到占用内存最大的对象是alibaba.fastjson的ParseConfig对象及其内部的IdentityHashMap对象,当前现象与测试环境dump结果一致
  3. 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存储缓存对象

     

  4. 问题的复现
    项目中用到fastjson的位置只有RedisCacheAspect中将redis中缓存的字符串转换为对象。

    直接debug跟进到ObjectDeserializer deserializer = get(type)方法的时候发现即使是同一个方法中两次获取同一个缓存,每次取到的type并非同一个对象(aop代理每次获取到的对象不一样,而key的比较必须是两个内存地址相同的对象,即同一个对象,所以永远不会命中key),这就会导致每次都获取不到缓存的反序列化器,所以会一直创建Entry并追加到链表上面,导致每个bucket上的entry数量越来越多

  5. 解决方案

    引入内部缓存,缓存方法全路径名称对应的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;
    }

     

  6. 项目中普遍使用的是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转换!

 

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值