内存问题(三)——生成内存问题案例

本文详细探讨了Java应用中遇到的内存溢出问题,包括元空间溢出、类加载重复导致的内存泄漏、ArrayList递归问题、读取大Excel、SQL全if引起的内存溢出、JVM堆栈溢出以及Docker环境下内存管理。提供了解决策略和预防措施。
摘要由CSDN通过智能技术生成

一、系统非堆内存溢出问题排查

以下例子出自其他人案例,拿过来是为了学习排查问题思路,与解决思路。

1.1 现象描述

运单系统每隔一段时间,内存使用和CPU报警超出阈值85%,通过监控平台可以看到非堆内存持 续增加不回收
。导致频繁FullGC,引起CPU100%

1.2 操作系统监控图

在这里插入图片描述

1.3 JVM内存监控

在这里插入图片描述

  从监控上看非堆内存永久代(JDK8 HotSpot JVM将移除永久区,使用本地内存来存储类元数据信息并称之为:元空间(Metaspace))一直增加且无法回收。这部分内存主要用于存储类的信息、方法数据、方法代码等,正常情况下元空间不会占用很大内存,所以对于动态生成类的情况 比较容易出现永久代的内存溢出。开始怀疑应该是使用反射、代理等技术生成了大量的类加载到元 空间无法回收。

1.4 问题排查

1.4.1 使用jstat 查看内存及GC情况

在这里插入图片描述

各列含义: jstat (oracle.com)

  • S0C:第一个幸存区的大小
  • S1C:第二个幸存区的大小
  • S0U:第一个幸存区的使用大小
  • S1U:第二个幸存区的使用大小
  • EC:伊甸园区的大小
  • EU:伊甸园区的使用大小
  • OC:老年代大小
  • OU:老年代使用大小
  • MC:方法区大小
  • MU:方法区使用大小
  • CCSC:压缩类空间大小
  • CCSU:压缩类空间使用大小
  • YGC:年轻代垃圾回收次数
  • YGCT:年轻代垃圾回收消耗时间
  • FGC:老年代垃圾回收次数
  • FGCT:老年代垃圾回收消耗时间
  • GCT:垃圾回收消耗总时间

  可以看到发生了FullGC, MU 和 CCSU 都很大,FullGC并没有回收掉,再次确认了元空间内存溢出的可能。

1.4.2 打印类加载信息 分析代码

在JAVA_OPION 中添加 -verbose:class 打印类加载信息。重启后观察日志输出
在这里插入图片描述  发现以上输出信息,并且一直不停的Loaded。 打印出了相关的业务类.BusinessMessageBody、BusinessMessage ,看样子是使用Jaxb 序列化成XML时产生的问题。在项目中搜索相关代码。

总之,不是自己的案例,s省略中间的源码分析过程…,直接给结论:

因为框架这一行,会重复执行类的定义,一定重复定义,重复定义,在jvm内部会有一次约束检查出LinkageError,但临时创建的结构,等待GC去回收。

1.4.3 思考,并分析dump文件

  那么回过来想,为什么 injectors 被回收后,对应的Class实例未被回收卸载掉呢? 此现象产生的环境差异是因为升级了JDK版本,由JDK7升级到JDK8, 那么JDK8对垃圾回收做了哪些改变,是否这些改变导致了此问题的产生。带着这个疑问检索 google,得到了答案。

  在JDK 7中,对Permgen中对象的回收会进行存活性检查,因此重复定义时产生的数据会在GC时被清理。然而在JDK 8中,Metaspace的回收只依赖classloader的存活,当classloader还活着时,它所产生的对象无论存活与否都不会被回收,由此引发了OOM。

从生产环境dump出来的内存文件分析后验证了这一点:
在这里插入图片描述
参见 :
假笨说-谨防JDK8重复类定义造成的内存泄漏
JVM源码分析之JDK8下的僵尸(无法回收)类加载器
JAXB导致的Metaspace OOM问题分析

  JAXB-impl 2.2.11 版本 inject 版本种增加了一段 findLoadedClass`` 逻辑,按名称查找是否已有加载的类,避免重复定义加载。

二、入库内存泄露总结

  由于条件问题,一个查询语句把一个表里所有的记录都查询出来了,数据量很大,把内存打爆,
造成现场验收卡,卡,卡…

2.1 用jmap -dump命令导出JVM的整个内存信息。

在这里插入图片描述
快速定位报表里的泄露嫌疑犯,点击进去,如图,发现这两个嫌疑犯占用了
49.90%+44.45%=94,35%的内存,还让其他功能咋用呢??

在这里插入图片描述
在这里插入图片描述
点击第一个嫌疑犯的堆栈信息链接,仔细看就能发现该嫌疑犯是如何行凶的,如下图:
在这里插入图片描述
总结:内存分析工具有很多,使用MAT是其中的一种方法。另外,除了手动导出JVM内存信息外,还可以通过设置JVM参数,在JVM发生内存泄露的时候自动导出文件。

三、ArrayList递归调用addAll方法导致内存溢出

过程略,直接给结论:

  本段递归原思想以ids(ArrayList)为容器,循环添加每个子节点包含的全部分组id,并加上自身节点,进行返回。
  但是,由于ids为引用传递,每次addAll操作都会将容器中已有的ids重复添加到容器中,造成每次添加容器中的数据都会发生倍增。
  子分组数小时,不会发生异常现象。ArrayList 的每次扩容包括分配1.5倍的新数组空间,和老数组历史数据的拷贝。
  当子分组数超过27个时,数组长度达到了134217726,当长度超过Integer.MAX_VALUE-8或系统无法给ArrayList分配足够长的连续内存空间时,就会抛OOM异常。
  ArrayList的频繁扩容与拷贝操作,也导致了在执行递归方法开始到抛出OOM异常这段过程中,Cpu一直处于100%的状态,无法处理其它请求。

3.1 经验总结

  1. 在写完递归方法后,自测一定要认真测试到递归方法层的输入与返回,功能通过不能代表代码没有问题。
  2. 在使用ArrayList的时候,如果可以预知数组的大小范围,尽量对其进行容量大小的初始化,避
    免其频繁扩容。

四、读取excel引发的内存泄露

4.1 现象

  1. 业务反馈****.com打开显示502,去查logbook日志发现应用出现内存溢出;
    在这里插入图片描述
  2. 去ump中jvm监控查dump文件输入路径“-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/export/Instances/las-im-manager-new/server1/logs”,登录运维自主平台下载dump文件;
  3. 使用MAT工具打开dump文件,查看大对象的具体堆栈信息;
    在这里插入图片描述
  4. 去程序中查代码,excel导入读取是使用WorkBook的方式,经测试几兆的excel文件导入就占用大量的内存(超1g);
  5. 去查poi文档 http://poi.apache.org/spreadsheet/how-to.html ,发现POI提供了2中读取Excel的模式,分别是:
  • 用户模式:也就是poi下的usermodel有关包,它对用户友好,有统一的接口在ss包下,但是它是
    把整个文件读取到内存中的,对于大量数据很容易内存溢出,所以只能用来处理相对较小量的数据;
  • 事件模式:在poi下的eventusermodel包下,相对来说实现比较复杂,但是它处理速度快,占用内存少,可以用来处理海量的Excel数据;
  1. 参考了poi官方example( http://svn.apache.org/repos/asf/poi/trunk/src/examples/src/org/apache/poi/xssf/eventusermodel/examples/FromHowTo.java ),使用事件模式读取excel,解决了问题;

五、通用sql查询条件全if导致的内存溢出

5.1 现象:

  目前JVM启动日志中都配置了 内存打满前自动dump,前往对应的机器dump日志进行分析,使用工具为jprofiler,经过分析 发现byte类型占用了435MB的数据,并且调用来源为ibatis.executor.resultset.ResultWrapper,经过jprofiler分析,怀疑有全表查询语句 返回了大对象
导致的内存溢出。
在这里插入图片描述

5.2 查询对应数据库是否存在慢sql全表查询

问题sql:
在这里插入图片描述

5.3 修改方案: 针对Dao层对一些基础参数进行强制校验(三要素)

在这里插入图片描述

六、JVM堆栈内存溢出问题分析

6.1 分析JVM内存溢出时生成的hprof

关键数据:
JVM堆栈报告中:大对象:
在这里插入图片描述
大对象中的占用内存最大的属性:
在这里插入图片描述
未来得及释放的XML解析数据: 该数据已经导出,在后面:
在这里插入图片描述
线程调用的HTTP响应方法:
在这里插入图片描述
用户上传文件:
在这里插入图片描述

6.2 原因分析:

  在查看JVM内存堆栈信息分析报告的,Orverview的大对象图表中,说名称为"http-1601-18"的线程事例时占用内存超过2G;继而查看该线程实例包含属性实例明细列表;

  发现线程中占用内存最大的实例类为:
com.sun.org.apache.xerces.internal.dom.DeferredDocumentImpl ,占用内存 2G 左右;初步可以确定此实例的创建造成了JVM内存溢出;
  而该实例是POI工具包用于解析xlsx格式文件中的XML文件用的,也就是说是因为该线程执行过程中需要有xlsx文件解析逻辑。

进而查询该线程的调用HTTP业务接口,是:
com…master.goods.controller.GoodsNewController#importBatGoods ;
结合代码,获知此方法是用于批量导入事业部商品数据的,其大致逻辑是:

  1. 接收用户导入的xlsx文件
  2. 使用POI包对文件进行解析
  3. 循环遍历解析后的数据,对他们进行数据验证、数据入库(及创建商品)操作
  4. 返回导入结果

  结合的代码逻辑,堆栈信息分析,初步结论为某一用户在执行改操作时,导入了过大文件,导致此次的JVM 内存溢出事故;
然后查询四台主机操作日志:果然发现同一个用户在每次告警前的几分钟,都在商家工作台执行了商品导入操作;

  因而得出本次JVM内存溢出的最终原因为:系统并未对导入文件大小做限制或限制太宽松; 从而导致用户导入较大的文件时在解析过程中出现 JVM内存溢出问题;

  因而得出本次JVM内存溢出的最终原因为:系统并未对导入文件大小做限制或限制太宽松; 从而导致用户导入较大的文件时在解析过程中出现 JVM内存溢出问题;

6.3 问题修复方案

  1. 对上传得文件进行大小限制;限制的原则就是根据模板和数据条目上限,计算出一个合理的数值;根据实践:1000条的数据文件大小约在135K,5000条的文件大小在619K;也就是说文件大小不应该超过1M;而本案例中,用户上传的文件大小在26M左右;
  2. 解析数据时,要先进行数据行数验证,在对行数据进行解析;尽量减少数据转换次数;

七、生产环境部分机器内存溢出问题处理

7.1 现象

  部分请求出现502的情况。经查看发现5台服务器中的一台宕机,Tomcat进程已经不存在了。

  对该服务器及负载下的其他服务器的运行状态进行观察发现:该服务器的jvm已经没有数据,其他服务器jvm内存均接近100%,看来其他服务器也岌岌可危啊。

  查看了下宕机的那台服务器日志,关键信息如下:
在这里插入图片描述
  为了查看该日志文件,和jdos同事,申请开通了 该服务器 和 另外一台濒临崩溃的服务器的 终端连接信息;

  该文件信息基本上全是当时崩溃是的内存的一些大小参数(忘记截图),参考意义不大;

7.2 分析

  紧接着为了查看内存中到底是什么对象导致了内存的崩溃,由于jvm崩溃了,获取不到现在的一些信息了,于是连接到另外一台“濒危”的服务器上,去查看jvm内存的一些状况,具体情况如下:

  1. 首先使用查看了下 jvm进程ID为 139;
  2. 使用该命令 :sudo -u admin ./jmap -histo 139|more 查看到的情况如下:
    在这里插入图片描述
     如图:有31889个“Thread”事例导致,初步分析,某个地方在疯狂创建线程。但是,在哪个地方创建呢?我和几个相关同事沟通,确定没人在这个项目里边创建过线程。

 其实这个时候离成功仅有一步之遥了。

  1. 正在兴奋的解决中,杨xx同学也参与进来建议用sudo -u admin ./jstack -l -F 139 > stack.txt 打印堆栈信息,如图:
    在这里插入图片描述
    查到有7000多个线程池,可以定位某个地方在疯狂创建线程池,肯定了第一步的判断。

  2. 然后全文搜 Executors,发现之前的同事有两处创建线程池,并且是每次该请求接口基本上都会创建一个固定4个大小的线程池!原因终于找到了;此处列举一处:
    在这里插入图片描述

  3. 于是,将宕掉的机子重启,然后频繁的去请求该接口,复现了异常情况,由此可以肯定了判断。此处修改为启动的时候创建一个线程池,请求的时候创建一个Callable实现即可。

续:补上此次服务器的jvm参数,以及dump文件大小
在这里插入图片描述
使用命令 sudo -u admin ./jmap -dump:format=b,file=a.hprof 139

7.3 事故反思总结

  1. 将服务器监控预警做到位,此次疏于预警,导致服务器异常,没有及时发现,而是由业务方通知发现的,以后应避免此类事情再次发生。
  2. 多人合作项目,项目时间长一些细节东西已经记的不清晰了,以后重要的设计点最好有设计问题,方便后人解决问题。
  3. 有必要进行codeReview,来相互学习,相互进步。
  4. 应继续加强深入学习jvm相关知识,此处的事故算是一个在学习jvm道路上的一个成功解决案例。

八、JVM异常终止问题排查

8.1 现象

jdos两台机器JVM突然挂掉(11.26.78.xx、10.190.183.xx),查看MDC监控,发现jvm挂掉的时间点 机器内存占用很高(99%);
内存很平稳,每次yong gc,都能降下来;至JVM挂掉之前,未触发过full gc;CPU和线程数也都很正常。

8.2 问题初步分析定位-尝试Google

在这里插入图片描述

8.3 linux的OOM killer

 Linux 内核有个机制叫OOM killer(Out-Of-Memory killer),该机制会监控那些占用内存过大,尤其是瞬间很快消耗大量内存的进程,为了防止内存耗尽而内核会把该进程杀掉。 因此,你发现java进程突然没了,首先要怀疑是不是被linux的OOM killer给干掉了!

OK,顺着这个思路,确定到底是不是被oom killer干掉的。

查看系统报错日志: /var/log/messages,发现此文件是空的(后来咨询jdos运维同事得知,docker实例写此日志被禁用)
在这里插入图片描述
查看内核日志:
在这里插入图片描述
dmesg输出信息有调用oom-killer,但是从内存来看貌似是宿主机的(totalvm:22120752kB),时间点无法确定;

联系运维查询:
在这里插入图片描述
在这里插入图片描述
可以确定JVM是被操作系统oom killer干掉了。

问题:docker规格4c4g,为何java进程会占用5.2g?

jdk1.8及之前,JVM是感知不到容器的存在的,所以会使用宿主机的信息来计算,docker -m参数一般用来限制应用内存大小,跟镜像版本也有很大关系,有的版本限制的差不多相当于物理内存的 百分之50。
模拟:OOMKilled

九、其他

上面两个案例说明一个问题:遇见jvm异常退出,先找dump文件,dump如果没有,找hs_err_pid.log日志。如果还没有,翻内核日志。

jvm建议配置:

以8G内存为例:
1-垃圾回收器的参数
2-元空间需要配?512,350
3-堆内存最大最小?4G
4-栈大小?512k
5-直接物理内存?
6-GC日志

不建议配置的: 各个区域的比例,默认值。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值