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

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

一、现象分析

上篇博客说到,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操作,具体代码如下所示。

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的操作,这样就造成了一部分数据无法被删除,随着时间的推移,内存中的数据会越来越多。

三、故障解决

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

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里面可能已经为空。因此,采用两个可重入锁,防止出现异常。

相关文章
|
10天前
|
Java Maven Windows
使用Java创建集成JACOB的HTTP服务
本文介绍了如何在Java中创建一个集成JACOB的HTTP服务,使Java应用能够调用Windows的COM组件。文章详细讲解了环境配置、动态加载JACOB DLL、创建HTTP服务器、实现IP白名单及处理HTTP请求的具体步骤,帮助读者实现Java应用与Windows系统的交互。作者拥有23年编程经验,文章来源于稀土掘金。著作权归作者所有,商业转载需授权。
使用Java创建集成JACOB的HTTP服务
|
14天前
|
算法 安全 Java
Java内存管理:深入理解垃圾收集器
在Java的世界里,内存管理是一块基石,它支撑着应用程序的稳定运行。本文将带你走进Java的垃圾收集器(GC),探索它是如何默默守护着我们的内存安全。我们将从垃圾收集的基本概念出发,逐步深入到不同垃圾收集器的工作机制,并通过实例分析它们在实际应用中的表现。文章不仅旨在提升你对Java内存管理的认识,更希望你能通过这些知识优化你的代码,让程序运行更加高效。
34 3
|
10天前
|
Java 数据库连接 数据库
Java服务提供接口(SPI)的设计与应用剖析
Java SPI提供了一种优雅的服务扩展和动态加载机制,使得Java应用程序可以轻松地扩展功能和替换组件。通过合理的设计与应用,SPI可以大大增强Java应用的灵活性和可扩展性。
43 18
|
4天前
|
监控 算法 Java
Java中的内存管理:理解垃圾回收机制的深度剖析
在Java编程语言中,内存管理是一个核心概念。本文将深入探讨Java的垃圾回收(GC)机制,解析其工作原理、重要性以及优化方法。通过本文,您不仅会了解到基础的GC知识,还将掌握如何在实际开发中高效利用这一机制。
|
5天前
|
存储 监控 算法
Java中的内存管理与垃圾回收机制解析
本文深入探讨了Java编程语言中的内存管理策略和垃圾回收机制。首先介绍了Java内存模型的基本概念,包括堆、栈以及方法区的划分和各自的功能。进一步详细阐述了垃圾回收的基本原理、常见算法(如标记-清除、复制、标记-整理等),以及如何通过JVM参数调优垃圾回收器的性能。此外,还讨论了Java 9引入的接口变化对垃圾回收的影响,以及如何通过Shenandoah等现代垃圾回收器提升应用性能。最后,提供了一些编写高效Java代码的实践建议,帮助开发者更好地理解和管理Java应用的内存使用。
|
12天前
|
安全 Java API
【性能与安全的双重飞跃】JDK 22外部函数与内存API:JNI的继任者,引领Java新潮流!
【9月更文挑战第7天】JDK 22外部函数与内存API的发布,标志着Java在性能与安全性方面实现了双重飞跃。作为JNI的继任者,这一新特性不仅简化了Java与本地代码的交互过程,还提升了程序的性能和安全性。我们有理由相信,在外部函数与内存API的引领下,Java将开启一个全新的编程时代,为开发者们带来更加高效、更加安全的编程体验。让我们共同期待Java在未来的辉煌成就!
41 11
|
13天前
|
安全 Java API
【本地与Java无缝对接】JDK 22外部函数和内存API:JNI终结者,性能与安全双提升!
【9月更文挑战第6天】JDK 22的外部函数和内存API无疑是Java编程语言发展史上的一个重要里程碑。它不仅解决了JNI的诸多局限和挑战,还为Java与本地代码的互操作提供了更加高效、安全和简洁的解决方案。随着FFM API的逐渐成熟和完善,我们有理由相信,Java将在更多领域展现出其强大的生命力和竞争力。让我们共同期待Java编程新纪元的到来!
37 11
|
10天前
|
监控 Java 大数据
【Java内存管理新突破】JDK 22:细粒度内存管理API,精准控制每一块内存!
【9月更文挑战第9天】虽然目前JDK 22的确切内容尚未公布,但我们可以根据Java语言的发展趋势和社区的需求,预测细粒度内存管理API可能成为未来Java内存管理领域的新突破。这套API将为开发者提供前所未有的内存控制能力,助力Java应用在更多领域发挥更大作用。我们期待JDK 22的发布,期待Java语言在内存管理领域的持续创新和发展。
|
7天前
|
存储 缓存 算法
Java中的内存管理:理解垃圾回收机制
本文将深入探讨Java中的内存管理,特别是垃圾回收机制。我们将从基本的内存分配开始,逐步解析垃圾回收的原理和过程,以及它对Java应用程序性能的影响。通过实例演示,我们会展示如何在Java中有效地管理和优化内存使用。最后,我们将讨论一些常见的内存泄漏问题及其解决方案。
|
16天前
|
Java C++
Java内存区域于内存溢出异常
这篇文章详细解释了Java虚拟机的内存区域划分、各区域的作用以及可能遇到的内存溢出异常情况。
29 0

热门文章

最新文章