现象
生产 oom,分析如何产生的原因。
dump 文件
下载工具
https://github.com/electerm/electerm
下载
通过 sz 命令下载。
如果没有下载,那么使用 yum install lrzsz
安装。
解析 dump 文件
分析dump文件,我们可以用jdk里面提供的jhat工具,执行
jhat xxx.dump
jhat加载解析xxx.dump文件,并开启一个简易的web服务,默认端口为7000,可以通过浏览器查看内存中的一些统计信息
一般使用方法: 浏览器打开 http:/127.0.0.1:7000
jhat 解析
到 jdk 的 bin 目录
cd C:\Program Files\Java\jdk1.8.0_192\bin
$ jhat D:\data\xxx-heapdump.hprof
解析的文件耗时,根据 dump 文件会有差异。需要耐心等待。
OOM
λ jhat -J-mx6g D:\data\star-wxpa-web-heapdump.hprof\star-wxpa-web-heapdump.hprof
Reading from D:\data\star-wxpa-web-heapdump.hprof\star-wxpa-web-heapdump.hprof...
Dump file created Tue Feb 14 22:05:09 CST 2023
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.Hashtable.rehash(Hashtable.java:402)
at java.util.Hashtable.addEntry(Hashtable.java:426)
at java.util.Hashtable.put(Hashtable.java:477)
at com.sun.tools.hat.internal.model.Snapshot.addHeapObject(Snapshot.java:166)
at com.sun.tools.hat.internal.parser.HprofReader.readArray(HprofReader.java:824)
at com.sun.tools.hat.internal.parser.HprofReader.readHeapDump(HprofReader.java:501)
at com.sun.tools.hat.internal.parser.HprofReader.read(HprofReader.java:275)
at com.sun.tools.hat.internal.parser.Reader.readFile(Reader.java:92)
at com.sun.tools.hat.Main.main(Main.java:159)
如果执行的时候,OOM。可以加入对应的内存。
可以扩大一下内存。
$ jhat -J-Xmx9000m D:\data\star-wxpa-web-heapdump.hprof\star-wxpa-web-heapdump.hprof
Eclipse Memory Analyzer(MAT)
Eclipse Memory Analyzer(MAT)是Eclipse提供的一款用于Heap Dump文件的工具,操作简单明了,下面将详细进行介绍。
下载
https://www.eclipse.org/mat/previousReleases.php 下载地址。
使用
启动之后打开 File - Open Heap Dump… 菜单,然后选择生成的Heap DUmp文件,选择 “Leak Suspects Report”,然后点击 “Finish” 按钮。
jvisualvm jvm
后来发现,jhat 执行的特别慢。
MAT 又是各种限制和问题。
发现 jvm 自带的 jvisualvm 还是挺好用的。
dump 处理时内存
分析dump文件比较大的时候,超过了软件设置的默认的内存大小会报错。
解决办法
1.应用程序–本地选择VisualVM–概述–JVM参数。
修改配置文件:C:\Program Files\Java\jdk1.8.0_192\lib\visualvm\etc\visiaulvm.conf
修改 jvm 启动的最大参数,重启服务。
分析
【文件】-【装入】,选择过滤的文件类型。
找到 dump 文件所在的位置。
加载文件,选择对应的过滤类型。xxx.hprof
然后等待加载处理。
概要:
PS: 检索不方便,可以全选把内容放在外边。
基本信息:
生成的日期: Tue Feb 14 22:05:09 CST 2023
文件: D:\data\star-wxpa-web-heapdump.hprof\star-wxpa-web-heapdump.hprof
文件大小: 3,892.7 MB
字节总数: 4,237,102,262
类总数: 27,486
实例总数: 102,977,924
类加载器: 639
垃圾回收根节点: 7,553
等待结束的暂挂对象数: 32
在出现 OutOfMemoryError 异常错误时进行了堆转储
导致 OutOfMemoryError 异常错误的线程: ConsumeMessageThread_Q_STARWXPA_HOLIDAY_WX_10
找到这个 oom 的线程,我们点击进入。
内容比较多,那么是哪一个对象占用内存比较大呢?
那个对象占用内存大?
个人理解,可以先从【类】这里开始看。
可以发现 byte[] 和 mysql 相关查询的比较大。
选择具体的对象分析
根据类中的占用大小,从下面的 Local Variable
选择对比即可。
一般是一些 list/array 导致的对象。
日志比较多:
"ConsumeMessageThread_Q_STARWXPA_HOLIDAY_WX_10" prio=5 tid=529 RUNNABLE
at java.lang.OutOfMemoryError.<init>(OutOfMemoryError.java:48)
at com.mysql.cj.protocol.a.NativePacketPayload.readBytes(NativePacketPayload.java:558)
at com.mysql.cj.protocol.a.NativePacketPayload.readBytes(NativePacketPayload.java:508)
at com.mysql.cj.protocol.a.TextRowFactory.createFromMessage(TextRowFactory.java:66)
Local Variable: com.mysql.cj.protocol.a.NativePacketPayload#5
Local Variable: byte[][]#1570652
at com.mysql.cj.protocol.a.TextRowFactory.createFromMessage(TextRowFactory.java:42)
at com.mysql.cj.protocol.a.ResultsetRowReader.read(ResultsetRowReader.java:87)
at com.mysql.cj.protocol.a.ResultsetRowReader.read(ResultsetRowReader.java:42)
at com.mysql.cj.protocol.a.NativeProtocol.read(NativeProtocol.java:1576)
at com.mysql.cj.protocol.a.TextResultsetReader.read(TextResultsetReader.java:87)
Local Variable: com.mysql.cj.protocol.a.TextResultsetReader#11
Local Variable: com.mysql.cj.protocol.a.TextRowFactory#2
Local Variable: com.mysql.cj.result.DefaultColumnDefinition#57
Local Variable: java.util.ArrayList#96135
at com.mysql.cj.protocol.a.TextResultsetReader.read(TextResultsetReader.java:48)
at com.mysql.cj.protocol.a.NativeProtocol.read(NativeProtocol.java:1589)
at com.mysql.cj.protocol.a.NativeProtocol.readAllResults(NativeProtocol.java:1643)
Local Variable: com.mysql.cj.jdbc.result.ResultSetFactory#152
at com.mysql.cj.protocol.a.NativeProtocol.sendQueryPacket(NativeProtocol.java:951)
Local Variable: byte[]#38169401
Local Variable: com.mysql.cj.protocol.a.NativeProtocol#11
Local Variable: com.mysql.cj.util.LazyString#39
at com.mysql.cj.NativeSession.execSQL(NativeSession.java:1075)
Local Variable: com.mysql.cj.protocol.a.NativePacketPayload#34
Local Variable: com.mysql.cj.NativeSession#11
at com.mysql.cj.jdbc.ClientPreparedStatement.executeInternal(ClientPreparedStatement.java:930)
at com.mysql.cj.jdbc.ClientPreparedStatement.execute(ClientPreparedStatement.java:370)
Local Variable: com.mysql.cj.jdbc.ClientPreparedStatement#141
Local Variable: com.mysql.cj.jdbc.ConnectionImpl#11
at com.alibaba.druid.pool.DruidPooledPreparedStatement.execute(DruidPooledPreparedStatement.java:498)
Local Variable: com.alibaba.druid.pool.DruidPooledPreparedStatement#7
at sun.reflect.GeneratedMethodAccessor233.invoke(<unknown string>)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.apache.ibatis.logging.jdbc.PreparedStatementLogger.invoke(PreparedStatementLogger.java:59)
at com.sun.proxy.$Proxy209.execute(<unknown string>)
at org.apache.ibatis.executor.statement.PreparedStatementHandler.query(PreparedStatementHandler.java:63)
Local Variable: org.apache.ibatis.executor.statement.PreparedStatementHandler#7
at org.apache.ibatis.executor.statement.RoutingStatementHandler.query(RoutingStatementHandler.java:79)
at org.apache.ibatis.executor.SimpleExecutor.doQuery(SimpleExecutor.java:63)
Local Variable: com.sun.proxy.$Proxy209#7
at org.apache.ibatis.executor.BaseExecutor.queryFromDatabase(BaseExecutor.java:324)
Local Variable: org.apache.ibatis.cache.CacheKey#6
at org.apache.ibatis.executor.BaseExecutor.query(BaseExecutor.java:156)
Local Variable: org.apache.ibatis.executor.SimpleExecutor#6
at org.apache.ibatis.executor.CachingExecutor.query(CachingExecutor.java:109)
at com.github.pagehelper.PageInterceptor.intercept(PageInterceptor.java:143)
Local Variable: org.apache.ibatis.binding.MapperMethod$ParamMap#5
at org.apache.ibatis.plugin.Plugin.invoke(Plugin.java:61)
at com.sun.proxy.$Proxy207.query(<unknown string>)
at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:148)
at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:141)
at sun.reflect.GeneratedMethodAccessor202.invoke(<unknown string>)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:433)
Local Variable: org.apache.ibatis.session.defaults.DefaultSqlSession#6
at com.sun.proxy.$Proxy97.selectList(<unknown string>)
at org.mybatis.spring.SqlSessionTemplate.selectList(SqlSessionTemplate.java:230)
at org.apache.ibatis.binding.MapperMethod.executeForMany(MapperMethod.java:137)
at org.apache.ibatis.binding.MapperMethod.execute(MapperMethod.java:75)
at org.apache.ibatis.binding.MapperProxy.invoke(MapperProxy.java:59)
at com.sun.proxy.$Proxy131.selectList(<unknown string>)
at com.baomidou.mybatisplus.service.impl.ServiceImpl.selectList(ServiceImpl.java:414)
at XXXXXXXXX.star.wxpa.web.service.service.impl.WxMerTagInfoServiceImpl.queryTagList(WxMerTagInfoServiceImpl.java:68)
Local Variable: java.util.ArrayList#96139
Local Variable: com.baomidou.mybatisplus.mapper.EntityWrapper#6
Local Variable: java.util.ArrayList#96140
at sun.reflect.GeneratedMethodAccessor421.invoke(<unknown string>)
Local Variable: java.lang.Object[]#114312
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:333)
at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:207)
at com.sun.proxy.$Proxy132.queryTagList(<unknown string>)
Local Variable: java.util.ArrayList#96137
Local Variable: java.util.ArrayList#96136
Local Variable: java.util.ArrayList#96138
at XXXXXXXXX.star.wxpa.web.service.biz.task.WxMerHolidayPushTaskBiz.handlePushTransReport(WxMerHolidayPushTaskBiz.java:151)
Local Variable: java.lang.String#436709
Local Variable: XXXXXXXXX.star.wxpa.web.service.bo.wxpa.WxpaHolidayPushMqBo#9
Local Variable: java.lang.String#436710
Local Variable: XXXXXXXXX.star.wxpa.web.dal.entity.WxMerPushConfig#9
Local Variable: java.lang.String#436711
Local Variable: java.util.ArrayList#96141
at XXXXXXXXX.star.wxpa.web.service.biz.task.WxMerHolidayPushTaskBiz.consumerMq(WxMerHolidayPushTaskBiz.java:73)
at XXXXXXXXX.star.wxpa.web.service.mq.archer.ArcherWxHolidayPushCallback.doConsumeMessage(ArcherWxHolidayPushCallback.java:26)
at XXXXXXXXX.star.wxpa.web.service.mq.archer.ArcherBaseCallback.consumeMessage(ArcherBaseCallback.java:35)
Local Variable: java.lang.String#436676
at XXXXXXXXX.archer.consumer.RocketMQArcherConsumer.consumeMessage(RocketMQArcherConsumer.java:312)
Local Variable: XXXXXXXXX.archer.report.MessageConsumeInfo#29
Local Variable: XXXXXXXXX.archer.hooks.ConsumeMessageContext#20
Local Variable: XXXXXXXXX.archer.consumer.MessageEventArgs#20
at XXXXXXXXX.archer.rocketmq.client.impl.consumer.ConsumeMessageConcurrentlyService$ConsumeRequest.run(ConsumeMessageConcurrentlyService.java:423)
Local Variable: XXXXXXXXX.archer.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext#20
Local Variable: XXXXXXXXX.archer.rocketmq.client.impl.consumer.ConsumeMessageConcurrentlyService$ConsumeRequest#3575
at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
Local Variable: java.util.concurrent.Executors$RunnableAdapter#3948
at java.util.concurrent.FutureTask.run(FutureTask.java:266)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
Local Variable: java.util.concurrent.FutureTask#3582
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
Local Variable: java.util.concurrent.ThreadPoolExecutor$Worker#253
at java.lang.Thread.run(Thread.java:748)
可以点进去具体看对应的引用。
其实这里是因为 mysql 查询的时候全表扫,导致加载的内存特别大。
都是 byte[] 网络传入,转换为 row list。都是特别大的对象。
导致不断晋升,FULL GC。
然后 GC 又释放不掉,直接导致内存 OOM,应用挂掉。
jvm 对象晋升的方式:存活的周期比较长,超过一定次数,会晋升;或者对象太大,超过了 young 区,则直接进入了老年代。
GC 的时候,就算是 full-GC,也只是从根节点开始遍历引用。这个大对象太大,还没处理完。导致无法是双方
代码原因分析
对于 queryTagList 的入参
使用的是 mybatis-plus。
wrapper.in("mer_id", merIdList)
如果 merIdList 对应的信息为空,会导致 mybatis-plus 把这个条件过滤掉。
从而全表扫,但是对应的表信息又特别大,几千万的数据量全部查出来,直接把内存打爆了。
解决方式
(1)查询标签信息的时候,不要返回列表,通过 count 总数替代。
(2)查询的时候一定判空。
如果 merIdList 为空,则直接返回 count = 0;
(3)一些建议
有些同事建议不要使用动态拼接,但是这个属于代码风格。
其实主要是对用法的理解问题。
小结
mq 的方式虽然很好,但是会导致服务全部挂掉,也比较危险。
写代码一定要注意,避免全表扫的情况。
参考资料
https://www.jianshu.com/p/94d03049b41e
下载liunx服务器上的文件到Windows本地、或者上传到服务器的方法
免费的 XShell 替代品,我推荐这5款软件,一个比一个香!
https://www.jb51.net/article/201435.htm
JVM性能调优监控工具jps、jstack、jmap、jhat、jstat等使用详解
MAT(Memory Analyzer Tool)工具入门介绍