• Java服务假死后续之内存溢出


    一、现象分析

      上篇博客说到,Java服务假死的原因是使用了Guava缓存,30分钟的有效期导致Full GC无法回收内存。经过优化后,已经不再使用Guava缓存,实时查询数据。从短期效果来看,确实解决了无法回收内存的问题,但是服务运行几天后,发现内存又逐渐被占满,Full GC后只能回收一小部分。

    从上图可以看出,一次Full GC后,老年代基本上没有回收多少内存,占比从99.86%降到99.70%。

    二、原因排查

       到底是什么对象占据这么大的内存,并且无法被JVM垃圾回收呢。在上一篇博客中已经移除了Guava缓存,按理说不应该有无法回收的对象了。那么,很明显这应该是代码问题导致了内存泄露,现在需要知道哪些对象无法被回收,从而定位出代码哪里有BUG。这里采用jmap -histo:live 201349|head -10命令打印出GC后存活的对象。

      从上图可以看出,还是之前存在Guava缓存里面的对象占据着大部分内存,代码修改为实时查询后,每次用完数据都会从Map中剔除,按理不应该有强引用去引用这些对象。光看代码无法排查出哪里导致了内存泄露,只能将GC后的内存文件导出来进行分析。这里采用jmap -dump:format=b,file=/data/heap.hprof命令将内存文件导出来,用JDK自带的visualVM打开。

      这里拿ECBug对象进行分析,从引用关系可以看出,ECBug对象被DataSetCenter引用,DataSetCenter就是实时查询数据进行存储的一个ConcurrentHashMap,但每次用完数据后都会进行remove操作,具体代码如下所示。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    private List<BusinessBean> realTimeQueryBusinessModelData(IDataSetKey accessCacheDataSetKey,Set<IMapper> mappers, Set<IFilter> filters, Set<ISorter> sorters) throws DataNotFoundException, IllegalAccessException, CloneNotSupportedException, InstantiationException {
            List<BusinessBean> resultBeans = null;
            try {
                lock.lock();
                if (!dataSetCenter.containsKey(accessCacheDataSetKey)) {
                    log.info("put DataSetKey into DataSetCenter,dataSetKey is {}",accessCacheDataSetKey);
                    int count = businessModelQuery.count(accessCacheDataSetKey);
                    if (count == 0) throw new DataNotFoundException();
                    Class modelClass = businessModelCenter.getDataModelClass(accessCacheDataSetKey.getModelId());
                    if (modelClass == null) {
                        throw new DataNotFoundException();
                    }
                    dataSetCenter.put(accessCacheDataSetKey, new DataSet(count, modelClass));
                }
                List<BusinessBean> cachedBeans = dataSetCenter.get(accessCacheDataSetKey).getData();
                resultBeans =  getModelDataInternal(accessCacheDataSetKey, businessModelQuery, mappers, filters, sorters, cachedBeans);
            }finally {
                lock.unlock();
                if(!lock.isLocked()){
                    dataSetCenter.remove(accessCacheDataSetKey);
                }
            }
            return resultBeans;
        }

      从代码来看,每次 dataSetCenter.put(accessCacheDataSetKey, new DataSet(count, modelClass))后,都会在finally里面调用dataSetCenter.remove(accessCacheDataSetKey)把key删除掉,这样在GC时会自动回收Value值。但是忽略了一个方法getModelDataInternal,该方法可能会递归调用realTimeQueryBusinessModelData方法,如果存在递归调用的话,那么由于可重入锁lock还没有完成解锁,所以无法进入if(!lock.isLocked())条件语句中进行删除key的操作,这样就造成了一部分数据无法被删除,随着时间的推移,内存中的数据会越来越多。

    三、故障解决

       基于上述的代码分析,改造如下所示。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    private List<BusinessBean> realTimeQueryBusinessModelData(IDataSetKey accessCacheDataSetKey,Set<IMapper> mappers, Set<IFilter> filters, Set<ISorter> sorters) throws DataNotFoundException, IllegalAccessException, CloneNotSupportedException, InstantiationException {
            List<BusinessBean> resultBeans = null;
            try {
                queryLock.lock();
                modelQueryLock.lock();
                if (!dataSetCenter.containsKey(accessCacheDataSetKey)) {
                    log.info("put DataSetKey into DataSetCenter,dataSetKey is {}",accessCacheDataSetKey);
                    int count = businessModelQuery.count(accessCacheDataSetKey);
                    if (count == 0) throw new DataNotFoundException();
                    Class modelClass = businessModelCenter.getDataModelClass(accessCacheDataSetKey.getModelId());
                    if (modelClass == null) {
                        throw new DataNotFoundException();
                    }
                    dataSetCenter.put(accessCacheDataSetKey, new DataSet(count, modelClass));
                }
                List<BusinessBean> cachedBeans = dataSetCenter.get(accessCacheDataSetKey).getData();
                resultBeans =  getModelDataInternal(accessCacheDataSetKey, businessModelQuery, mappers, filters, sorters, cachedBeans);
            }finally {
                modelQueryLock.unlock();
                if(!modelQueryLock.isLocked()){
                    removeDataSetKeys();
                }
                queryLock.unlock();
            }
            return resultBeans;
        }

       这里当modelQueryLock可重入锁完全解锁后,调用removeDataSetKeys方法,该方法会将dataSetCenter里面的key全部删除,这样在GC时就会回收不用的数据对象。这里采用两个可重入锁的目的是,如果只用一个modelQueryLock可重入锁,那么当modelQueryLock完全解锁后,正在执行removeDataSetKeys方法时,其他线程就可以进入该方法区,发现dataSetCenter里面还没有删除完全,从而获取里面的数据,即if (!dataSetCenter.containsKey(accessCacheDataSetKey))为false,从而通过List<BusinessBean> cachedBeans = dataSetCenter.get(accessCacheDataSetKey).getData()直接获取dataSetCenter里面的数据,但是下一刻dataSetCenter里面可能已经为空。因此,采用两个可重入锁,防止出现异常。

     

    作者:kbkb

    本文为作者原创,转载请注明出处:https://www.cnblogs.com/kbkb/p/16442886.html

  • 相关阅读:
    【进化计算】遗传算法求解gr48数据集
    计算机毕业设计选题推荐-教务管理系统-Java/Python项目实战
    kubeadm 搭建 K8s
    offset新探索:双管齐下,加速大数据量查询
    设计模式概述
    EffectiveC++-条款40:明智而审慎地使用多重继承
    Java版分布式微服务云开发架构 Spring Cloud+Spring Boot+Mybatis 电子招标采购系统功能清单
    【QT开发笔记-基础篇】| 第四章 事件QEvent | 4.7 拖放事件
    北斗导航 | ARAIM:Advanced RAIM流程及基本原理(LPV-200)
    【web-渗透测试方法】(15.8)测试逻辑缺陷、共享主机漏洞、Web服务器漏洞、信息泄露
  • 原文地址:https://www.cnblogs.com/kbkb/p/16442886.html