背景:

某业务系统中,同一天产生多次excel导出请求,excel数据需要通过查表获取,由于数据量过大,导致了OutOfMemoryError

事先在服务启动脚本中已设置OOM异常触发堆快照参数及GC详情打印参数:-XX:+PrintGCTimeStamps -XX:+PrintHeapAtGC -XX:+HeapDumpOnOutOfMemoryError,已知为该服务环境分配的jvm内存为6G,所以在事故发生后,我们可以得到gc.log和heapDump.hprof文件来进行分析


堆快照分析:

通过MAT(eclipse memory analyser)分析堆快照

异常线程描述

线程http-bio-8443-exec-2的Retained Heap,即持有资源约为3.5g左右,加上jdbc结果集对象、数据库连接池对象等,占据6g左右空间,而JVM最大空间为6g,所以OOM也是情理之中

占用空间Top3对象

到这里我们得到的线索是:2号线程及数据查询相关对象均持有非常多的资源,那么这个资源具体对应什么内容,我们查看mat持有树便可发现

线程持有的大批量对象

线程中存在着海量的CmcCustomer对象,每个对象占用1到2K空间,mat还为我们提供了OOM时的一些日志信息,我们可以查找一下什么方法创建了这么多对象

导致OOM的方法:exportExcel方法

CmcCustomer对象为cmc下的一个pojo,那么我们可以初步推断:exportExcel方法需要返回一个excel,excel所需的数据需要在数据库中查询业务所需的CmcCustomer表数据,但负责此请求的2号线程持有的该对象量过大,导致在该方法执行过程中需要分配更多空间时,没有了足够空间可以分配,导致了OutOfMemoryError

至此,我们通过堆快照分析,得到了"exportExcel方法引起OOM"的初步结论,那么这个方法就是OOM的罪魁祸首吗?现场还有一条gc.log的线索,我们可以通过GC日志来分析OOM的形成原因。


GC日志分析:

在实时打印的gc日志中,我们先分析一段事故发生前的gc信息

{Heap before GC invocations=6425 (full 29):

par new generation  total 699072K, used 358594K [0x0000000660000000, 0x00000006a0000000, 0x00000006a0000000)

eden space 349568K, 100% used [0x0000000660000000, 0x0000000675560000, 0x0000000675560000)

from space 349504K,  2% used [0x0000000675560000, 0x0000000675e30aa0, 0x000000068aab0000)

to  space 349504K,  0% used [0x000000068aab0000, 0x000000068aab0000, 0x00000006a0000000)

137826.671: [GC137826.671: [ParNew: 358594K->9042K(699072K), 0.0174300 secs] 3006268K->2656717K(5941952K), 0.0176250 secs] [Times: user=0.05 sys=0.01, real=0.02 secs]

Heap after GC invocations=6426 (full 29):

par new generation  total 699072K, used 9042K [0x0000000660000000, 0x00000006a0000000, 0x00000006a0000000)

eden space 349568K,  0% used [0x0000000660000000, 0x0000000660000000, 0x0000000675560000)

from space 349504K,  2% used [0x000000068aab0000, 0x000000068b384950, 0x00000006a0000000)

to  space 349504K,  0% used [0x0000000675560000, 0x0000000675560000, 0x000000068aab0000)

concurrent mark-sweep generation total 5242880K, used 2647675K [0x00000006a0000000, 0x00000007e0000000, 0x00000007e0000000)

concurrent-mark-sweep perm gen total 524288K, used 75286K [0x00000007e0000000, 0x0000000800000000, 0x0000000800000000)

}

这是一次gc,下一次gc与其间隔500s,均是新生代并行回收,新生代分配了1G空间,eden区达到350M左右,survivor区空间均充足,回收后新生代仅剩余9M左右空间,效果明显,老年代分配了5G空间,使用空间保持在2.6G左右,永久代空间更加充裕,500M空间仅使用70M左右。

接下来我们定位到excel的导出方法

2017-08-02 18:22:40,141 INFO  [jeesite.modules.cmc.web.CmcCustomerController] - 导出指定时间段内的数据到Excel

2017-08-02 18:22:40,142 DEBUG [modules.cmc.dao.CmcCustomerDao.getExcelCmcList] - ==>  Preparing: SELECT 篇幅关系此处省略查询字段 WHERE a.del_flag = ? AND a.create_date > concat(?," ","00:00:00") AND a.create_date < concat(?," ","23:59:59") ORDER BY a.pid DESC

2017-08-02 18:22:40,142 DEBUG [modules.cmc.dao.CmcCustomerDao.getExcelCmcList] - ==> Parameters: 0(String), 2017-04-17(String), 2017-07-31(String)

=>18:22分,查询了2017-04-17至2017-07-31的CmcCustomer数据,开始后,gc日志如下

{Heap before GC invocations=1454 (full 192):

par new generation  total 699072K, used 699071K [0x0000000660000000, 0x00000006a0000000, 0x00000006a0000000)

eden space 349568K, 100% used [0x0000000660000000, 0x0000000675560000, 0x0000000675560000)

from space 349504K,  99% used [0x0000000675560000, 0x000000068aaaffe8, 0x000000068aab0000)

to  space 349504K,  0% used [0x000000068aab0000, 0x000000068aab0000, 0x00000006a0000000)

202585.700: [Full GC202585.700: [CMS202589.173: [CMS-concurrent-mark: 3.507/3.509 secs] [Times: user=3.56 sys=0.00, real=3.51 secs]

(concurrent mode failure): 5242879K->5242879K(5242880K), 14.3420460 secs] 5941951K->5936395K(5941952K), [CMS Perm : 74280K->74274K(524288K)], 14.3421470 secs] [Times: user=14.35 sys=0.00, real=14.34 secs]

Heap after GC invocations=1455 (full 193):

par new generation  total 699072K, used 693515K [0x0000000660000000, 0x00000006a0000000, 0x00000006a0000000)

eden space 349568K,  99% used [0x0000000660000000, 0x000000067555ffb8, 0x0000000675560000)

from space 349504K,  98% used [0x0000000675560000, 0x000000068a542fe8, 0x000000068aab0000)

to  space 349504K,  0% used [0x000000068aab0000, 0x000000068aab0000, 0x00000006a0000000)

concurrent mark-sweep generation total 5242880K, used 5242879K [0x00000006a0000000, 0x00000007e0000000, 0x00000007e0000000)

concurrent-mark-sweep perm gen total 524288K, used 74274K [0x00000007e0000000, 0x0000000800000000, 0x0000000800000000)

}

202600.077: [Full GC202600.077: [CMS: 5242879K->5242879K(5242880K), 8.9369500 secs] 5941951K->5936519K(5941952K), [CMS Perm : 74277K->74277K(524288K)], 8.9370540 secs] [Times: user=8.95 sys=0.00, real=8.93 secs]

我们可以发现:

1.两次gc间隔时间仅为5秒钟

2.gc发生时,eden区、survivor区均爆满,而且回收后仅回收了1%

3.老年代空间未见减少,停留在5242879K

4.cms收集器发生了concurrent mode failure-并发模式失败(发生的原因一般是CMS正在进行,但是由于老年代空间内存不足,需要尽快回收老年代里死的java对象,这个时候需要触发full gc,停止所有的java线程,同时终止CMS)。

篇幅原因在此不列出后续所有gc日志,本次gc是第192次,但是在OutOfMemory发生时,gc执行到了第597次,并且均为"cms回收->被阻断concurrent mode failure->full gc"的循环,而且老年代始终未见减少,直到最后一次请求分配空间,并且没有空间可分配,JVM也无法再回收,在这种情况下产生了内存溢出。

到这里,结论慢慢清晰,exportExcel方法产生太多的对象,占满了新生代、老年代空间,JVM也无法回收更多对象。但是有一点不知道有没有引起大家的好奇——老年代是什么时候变的这么大了?在之前正常回收的日志中,老年代使用空间只有2.6g左右,即使峰值也不到4g,就被JVM回收掉了,而现在使用空间已经到了阈值。下面这个图模拟了老年代已使用空间随gc次数的变动

两种回收情况下老年代已使用空间

导出excel动作在事故发生日发生多次,但时间范围为当天的查询请求中,数据量不小,但是JVM还是可以支撑,只是新生代gc频繁,效果如上图(左)所示;但发生OOM的那次请求中,时间起止范围在100天左右,数据量非常之大,新生代不断产生海量对象,survivor区瞬间达到最大值,eden区跳过survivor区进入老年代的对象非常多,即使JVM仍在gc,但是回收的效率低于对象增长的速度,那么就会产生上图(右)的现象,CMS收集器无法进行,full gc强制进行,系统停顿时间暴增,直至新生代、老年代占用空间均达到峰值,JVM再无可分配空间,游戏结束。


总结:

通过堆快照、GC日志的分析,我们得到了最终的结论:问题确实出在excel导出方法,这个方法在内存中一次性加载的数据量太过庞大,虽然不是立即发生GC,但是新对象的不断堆积最终还是压垮JVM发生了内存溢出,这是一种典型的内存溢出问题。

一、查询开始后,对象的创建过程,

解决:

1.分配更大的JVM内存空间,但不推荐,目前系统流量不大,扩大空间只是缓兵之计,假如后期推广开始,100个人同时导出excel,总不能扩大100倍的内存,这会造成资源的浪费

2.优化业务流程,每日凌晨定时生成当日excel数据,用户若有需求,可以异步推送到其用户空间,但这种方式等于修改了产品层面内容,代价比较大

3.将导出excel改为写入csv,这样就通过流输出解决了创建超大excel的问题,用户可以通过excel打开csv数据;而由于是流输出,我们也可以利用游标的思想,进行流式读取,如利用MyBatisCursorItemReader:

static void testCursor1() throws UnexpectedInputException, ParseException, Exception {

try {

Mapparam = new HashMap();

AccsDeviceInfoDAOExample accsDeviceInfoDAOExample = new

AccsDeviceInfoDAOExample();

accsDeviceInfoDAOExample.createCriteria().

andAppKeyEqualTo("12345").andAppVersionEqualTo("5.7.2.4.5").

andPackageNameEqualTo("com.test.zlx");

param.put("oredCriteria", accsDeviceInfoDAOExample.getOredCriteria()); // 设置参数

myMyBatisCursorItemReader.setParameterValues(param); // 创建游标

myMyBatisCursorItemReader.open(new ExecutionContext()); //使用游标迭代获取每个记录

Long count = 0L;

AccsDeviceInfoDAO accsDeviceInfoDAO;

while ((accsDeviceInfoDAO = myMyBatisCursorItemReader.read()) != null) {

System.out.println(JSON.toJSONString(accsDeviceInfoDAO));

++count;

System.out.println(count);

}

} catch (Exception e) {

System.out.println("error:" + e.getLocalizedMessage());

} finally {            // do some

myMyBatisCursorItemReader.close();

}

}

MyBatisCursorItemReader注入:

<bean id="myMyBatisCursorItemReader"          class="org.mybatis.spring.batch.MyBatisCursorItemReader">

<property name="sqlSessionFactory" ref="sqlSessionFactory" />

<property name="queryId" value="com.taobao.accs.mass.petadata.dal.sqlmap.AccsDeviceInfoDAOMapper.selectByExampleForPetaData" />

</bean>

mapper.xml中再手动为sql加入fetchSize=-2147483648来配置JDBC的流式抓取,对这个配置感兴趣的同学可以参考此文章, 这种方式返回了操作ResultSet的游标让用户自己迭代获取数据,我们也可以使用Mybatis的ResultHandler,内部直接操作ResultSet逐条获取数据并调用回调handler的handleResult方法进行处理

static void testCursor2() {

SqlSession session = sqlSessionFactory.openSession();    Mapparam = new HashMap();

AccsDeviceInfoDAOExample accsDeviceInfoDAOExample = new AccsDeviceInfoDAOExample();

accsDeviceInfoDAOExample.createCriteria().andAppKeyEqualTo("12345").

andAppVersionEqualTo("1.2.3.4").andPackageNameEqualTo("com.hello.test");

param.put("oredCriteria", accsDeviceInfoDAOExample.getOredCriteria());

session.select("com.taobao.accs.mass.petadata.dal.sqlmap.

AccsDeviceInfoDAOMapper.selectByExampleForPetaData", param, new

ResultHandler() {

@Override

public void handleResult(ResultContext resultContext) {

AccsDeviceInfoDAO accsDeviceInfoDAO = (AccsDeviceInfoDAO)                                           resultContext.getResultObject();

System.out.println(resultContext.getResultCount());

System.out.println(JSON.toJSONString(accsDeviceInfoDAO));

}

}

);

}

本次事故复盘结束,解决方案不尽完善,希望对遇到类似问题的同学有所帮助。

内存溢出真实案例分析相关推荐

  1. Java内存溢出故障案例及Linux内存机制探究

    文章目录 Java内存溢出故障案例及Linux内存机制探究 OOM Killer触发机制分析 如何避免系统触发OOM Killer 这部分内容属于demo案例分享,解决线上运维问题,思路是最重要的 J ...

  2. java模拟内存溢出并分析_模拟内存溢出通过MAT分析

    构建一个简单的Springboot应用,模拟出OOM场景,再导出heap dump文件,通过Mat分析. 搭建简易Springboot,模拟OOM场景 搭建一个简易的springboot工程,在con ...

  3. linux防火墙转发udp,一次穿透 iptables 防火墙的 UDP ***报文真实案例分析

    一次穿透 iptables 防火墙的 UDP ***报文真实案例分析,揭示了一些在使用 iptables 时鲜为人知的安全隐患,希望能给大家带来帮助! 这类***确实少见,也是一次好的排错过程,所以转 ...

  4. 又一起.NET程序挂死, 用 Windbg 抽丝剥茧式的真实案例分析

    一:背景 1. 讲故事 前天有位粉丝朋友在后台留言让我帮忙看看他的 Winform程序 UI无响应 + 410线程 到底是啥情况,如下图: 说实话,能看到这些真实案例我是特别喜欢的?????????? ...

  5. OutOfMemoryError/OOM/内存溢出异常实例分析--堆内存溢出

    Java堆内存溢出 只要不断创建对象,并且保证GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象, 那么在对象数量到达最大堆的容量限制后就会产生内存溢出异常,代码如下: import ...

  6. 46栈内存溢出、内存区域(程序计数器、Java 虚拟机栈、本地方法栈、Java 堆、方法区、直接内存、内存溢出)与内存溢出(对象实例化分析)

    46.什么情况下会发生栈内存溢出 46.1.Java 内存区域与内存溢出 46.1.1.内存区域 46.1.1.1.程序计数器 46.1.1.2.Java 虚拟机栈 46.1.1.3.本地方法栈 46 ...

  7. java堆外内存溢出_JVM 案例 - 堆外内存导致的溢出错误

    案例 一个网站为了实现客户端实时从服务端接收数据,使用了 CometD 1.1.1 作为服务端推送框架,服务器是 Jetty7.1.4,CPU i5,内存 4G,操作系统 32位Windows. 服务 ...

  8. java线程内存溢出_Java常见问题分析(内存溢出、内存泄露、线程阻塞等)

    Java垃圾回收机制(GC) 1.1 GC机制作用 1.2 堆内存3代分布(年轻代.老年代.持久代) 1.3 GC分类 1.4 GC过程 Java应用内存问题分析 2.1 Java内存划分 2.2 J ...

  9. Java内存溢出问题排查分析

    目录 前言 一.MAT(Memory Analyzer Tool) 二.软件初识 三.捕获dump文件 1.主动方式 2.被动方式 四.分析dump文件 总结 前言 项目运行过程中,我们可能会遇到Ja ...

  10. 内存管理实战案例分析3:为何分配不出一个页面?

    微信公众号:奔跑吧linux社区 本文节选自<奔跑吧Linux内核>第二版卷1第6.3.3章 1.问题描述 下面是有问题的OOM Killer内核日志,其中空闲页面为86048KB,最低警 ...

最新文章

  1. C 格式化的输入输出(printf scanf)
  2. 火星直播?一种可以给中国火星探测工程当云监工的方法
  3. TF之RNN:TF的RNN中的常用的两种定义scope的方式get_variable和Variable
  4. 大型分布式网站架构设计与实践 第一章《面向服务的体系架构(SOA)》
  5. python数独代码_python 实现计算数独
  6. 惊了!同事竟然在代码里“下毒”
  7. 途观l怎么使用_官宣!中型SUV质量最新排名出炉:汉兰达失前三,大众途观L上榜!...
  8. JDI考虑让日本国内部分工厂停工 因苹果公司需求低迷
  9. 简洁的留言代码_这几段代码,测测你是 Python 菜鸟还是老司机
  10. linux lvm 删除pv磁盘,如何安全的删除Linux LVM中的PV物理卷(硬盘或分区)
  11. 计算机二级msoffice高级应用考试,全国计算机二级MSOffice高级应用考试大纲
  12. oracle有关游标的知识
  13. [转载] python内置函数 compile()
  14. 太强了!这款轻量级的数据库中间件完美解决了SpringBoot中分库分表问题?
  15. 真的发现自己已不再年轻
  16. 7.Oracle数据库SQL开发之 算术运算
  17. #1024程序员节# cc2530 按键唤醒功耗模式PM3例程
  18. Lightroom 2020年2月版新增功能
  19. python控制苹果手机触摸屏失灵怎么办_苹果手机触摸屏失灵怎么办
  20. visual studio 2022 离线安装包 下载

热门文章

  1. 漫谈Java IO之 NIO那些事儿
  2. 利用 Commons-Fileupload 实现文件上传分析(转)
  3. 基本数据类型-列表_元组_字典
  4. 3.2 GUN as汇编(本文内容大部分引用原文,非原创)
  5. Python第3次作业
  6. Spring 注解配置(2)——@Autowired
  7. NOIP2008pj luoguP1058 立体图 模拟
  8. js 控制 head 元素 隐藏与显示
  9. 求出字符“2013-9-8”与“2013-10-1”相差的天数
  10. spring mvc 学习 转载