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操作,具体代码如下所示。
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里面可能已经为空。因此,采用两个可重入锁,防止出现异常。
Java服务假死后续之内存溢出的更多相关文章
- 记一次生产事故的排查与优化——Java服务假死
		
一.现象 在服务器上通过curl命令调用一个Java服务的查询接口,半天没有任何响应.关于该服务的基本功能如下: 1.该服务是一个后台刷新指示器的服务,即该服务会将用户需要的指示器数据提前计算好,放入 ...
 - Tomcat9.0.13 Bug引发的java.io.IOException:(打开的文件过多 Too many open files)导致服务假死
		
问题背景: 笔者所在的项目组最近把生产环境Tomcat迁移到Linux,算是顺利运行了一段时间,最近一个低概率密度的(too many open files)问题导致服务假死并停止响应客户端客户端请求 ...
 - 分析java进程假死状况
		
摘自: http://www.myexception.cn/internet/2044496.html 分析java进程假死情况 1 引言 1.1 编写目的 为了方便大家以后发现进程假死的时候能够正常 ...
 - Java虚拟机系列(三)---内存溢出情况及解决方法
		
因为Java虚拟机内存有堆内存.方法区.虚拟机栈.本地方法栈和程序计数器五部分组成,其中程序计数器是唯一一块不会发生内存溢出异常的内存区,所以只有四类内存区可能发生内存溢出异常,其中虚拟机栈和本地方法 ...
 - 【Java】几种典型的内存溢出案例,都在这儿了!
		
写在前面 作为程序员,多多少少都会遇到一些内存溢出的场景,如果你还没遇到,说明你工作的年限可能比较短,或者你根本就是个假程序员!哈哈,开个玩笑.今天,我们就以Java代码的方式来列举几个典型的内存溢出 ...
 - 记一次阿里云oss文件上传服务假死
		
引言 记得以前刚开始学习web项目的时候,经常涉及到需要上传图片啥的,那时候都是把图片上传到当前项目文件夹下面,每次项目一重启图片就丢了.虽然可以通过修改/tomcat/conf/server.xml ...
 - java 导出 excel 最佳实践,java 大文件 excel 避免OOM(内存溢出) excel 工具框架
		
产品需求 产品经理需要导出一个页面的所有的信息到 EXCEL 文件. 需求分析 对于 excel 导出,是一个很常见的需求. 最常见的解决方案就是使用 poi 直接同步导出一个 excel 文件. 客 ...
 - windows环境下解决web服务假死的问题
		
最近在windows系统在部署web服务器,发现很不稳定.web服务有容易假死,改过配置换过各种web软件,如apache.nginx都不管用. 所以干脆做个简易的定时检测Web服务状态的软件.一旦w ...
 - 解决因为终端打印造成的java程序假死
		
问题状态: java 程序 日志采用 log4j 运行时由另一个管理进程拉起,程序在后台运行. 现象: 程序后台运行时,运行一段时间后假死 分析原因: 尝试打印输出,定位假死的具体位置,发现出现假死的 ...
 
随机推荐
- 技术管理进阶——一线Leader怎么做?经理的速成宝典
			
原创不易,求分享.求一键三连 本期培训材料关注公众号后回复:经理培训,获得 前段时间有个同学问我有没有一线Leader的速成培训课程,很好的问题,首先我们需要定义一下什么是小Leader: 所谓小Le ...
 - 基于.NetCore开发博客项目 StarBlog - (3) 模型设计
			
系列文章 基于.NetCore开发博客项目 StarBlog - (1) 为什么需要自己写一个博客? 基于.NetCore开发博客项目 StarBlog - (2) 环境准备和创建项目 基于.NetC ...
 - 一个实战让你搞懂Dockerfile
			
摘要 在认识Dockerfile的基础功能之后,即一个用基础镜像来构建新镜像的文本文件,就需要在实际工作中使用其灵活便利的操作来提升我们的工作效率了,这里演示在Tomcat里运行一个程序的过程,以此来 ...
 - 【原创】浅谈指针(十二)关于static(上)
			
0.前言 这个系列基本上是一月一更到两月一更 今天写一篇关于static的,内含大量干货,做好准备 1.基础知识的回顾 1.1.内存的种类 一般来说,我们之前已经讲过的变量(或者说是内存)可以大体分为 ...
 - Dockerfile创建自有镜像
			
文件名必须名为Dockerfile,用touch命令新建Dockerfile文件(执行touch Dockerfile),Dockerfile内容: from ubuntu --基础镜像名字 main ...
 - python学习-Day36
			
目录 进程 进程间通信 队列(multiprocess.Queue) 概念 Queue的实例q具有以下方法: 消息队列 生产者消费者模型 实现生产者消费者模型三要素: 线程理论 线程概念 线程存在的意 ...
 - 在博客文章中使用mermaid 定义流程图,序列图,甘特图
			
概述 Mermaid(美人鱼)是一套markdown语法规范,用来在markdown文档中定义图形,包括流程图.序列图.甘特图等等. 它的官方网站是 https://mermaid-js.github ...
 - 【mq】从零开始实现 mq-08-配置优化 fluent
			
前景回顾 [mq]从零开始实现 mq-01-生产者.消费者启动 [mq]从零开始实现 mq-02-如何实现生产者调用消费者? [mq]从零开始实现 mq-03-引入 broker 中间人 [mq]从零 ...
 - CTF简介
			
最近在学习渗透测试,后来发现CTF很有趣,发现对学习有所帮助,于是找了几个网站,下面推荐几个我觉得不错的网站 https://www.ctfhub.com/#/index https://adworl ...
 - 理解 Angular 服务
			
理解 Angular 服务 本文写于 2021 年 3 月 29 日 理解 Angular 服务 什么是服务 服务写法 原理简述 提供服务 1. 在服务中注册 2. 在 module 中注册 3. 在 ...