0. 引言
上一章,我们讲解了OOM问题出现的原因,和解决的办法,本章我们来具体落实这些方法。
1. 解决OOM问题思路
知道了OOM问题的原因,那么我就来看解决问题的思路,问题的具体案例多种多样,但是核心思路是不变的。
针对于栈内存溢出:一般通过日志找到报错的代码位置,针对性排查是否有循环调用、死循环的问题即可,
针对堆内存溢出:
(1)如果是调用量激增导致的内存不足,那么考虑增加机器或拓展内存资源
(2)机器资源充足,只是JVM分配的内存较小,考虑调大JVM内存,通过参数最大内存-Xmx
和最小内存-Xms
(2)如果是超大对象,那么考虑业务场景,是否需要查询如此大的对象,考虑分步查出
(3)如果是对象引用没有释放,就排查代码逻辑,查看是否有没有正常释放对象引用的地方,比如ThreadLocal没有正常remove,导致对象一致引用。
实际上大部分开发在掌握了详细的报错信息后,都能定位对应的代码位置,来排查到错误,而OOM问题让很多同学望而生畏的原因是,不知道怎么定位报错,不知道怎么查看报错信息。
下面我们就来讲解如何定位报错信息
2. 定位OOM报错
1、首先找到是哪个服务有问题,一般大型系统会部署预警提醒,根据预警信息找到对应服务器即可,小型系统没有部署预警的直接到服务器上逐一查看。如果你说你们是大型系统,又没有部署预警提醒的,那怎么排查? 方法根据反馈信息,一台一台服务器查吧,事后赶紧让运维把预警系统部署上。
2、定位到服务器后,到对应服务器上通过top
指令,查看是哪个进程占用内存资源较高
这里为了模拟操作,我写了一个程序用来产生OOM问题,同时为了尽快模拟出OOM,我们将堆内存限定为200M,想要更快显示效果你可以限定的更小
java -jar -Xms200M -Xmx200M cpu_oom_demo-0.0.1-SNAPSHOT.jar
top指令可以看到内存占用情况,因为这里我限定了
200M
,如果线上真实情况,可能会打到80%+
同时在日志中也会报错堆内存溢出
如果定位到是java进程导致,可以通过jps -l
指令来查看所有正在运行的java进程
jps支持的参数
jps -q:查看所有运行的Java进程,但只显示进程号pid
jps -m:只显示传递给main方法的参数
jps -l:只显示运行程序主类的包名,或者运行程序jar包的完整路径
jps -v:单独显示JVM启动时,显式指定的参数
jps -V:显示主类名或者jar包名。
3、然后通过top -Hp 进程pid
指令来查看是哪个线程占用的内存资源高,如果自定义了线程名了,可以通过此处打印的线程名就能定位到具体是哪块功能线程引起的问题,从而定位问题(这也是阿里java规范中要求自定义线程池,自定义线程名的重要性)
上述看到java程序的pid是1826,那么执行top -Hp 1826
看到从线程无法定位到业务功能
4、如果没有自定义线程名,或者根据线程名也看不出具体原因,那么就需要导出堆日志了,通过jmap
指令导出堆日志,因为该指令执行期间会导致业务线程无法运行,所以在导出前我们要确保有其他节点顶着,同时将该节点从注册中心/负载均衡中下架(注意不能关闭服务,关闭后JVM日志就导不出来了),然后执行jmap
我们先导出进程中占用内存空间最大的前20的对象名,1826为进程ID
jmap -histo 1826 | head -20
可以看到是User对象的占用过高导致,这里如果这个对象你能够定位到具体的使用位置,或者说这个对象使用的地方并不多,那么次数就能根据这个信息定位到问题。比如这里是用户实体类占用过高,那理所当然考虑是不是对用户信息的访问高飙升,或者用户信息访问后没有正常释放,通过这些信息就可以进行辅助排查。但如果你调用这个对象的地方很多,那还不足以定位问题,我们还要进一步进行排查
5、这里如果服务是正常的调用,并且服务器资源还算可以,比如我这里出现了OOM,但实际服务器内存占用才22%。那么我们可以通过jps -lv
将java进程中设置的JVM参数打印出来
可以看到这里因为设置了JVM内存为200M,如果资源允许将该内存调大即可。注意这里显示的是JVM启动时,显式指定的参数,如果没有查询到说明使用的都是JVM默认参数值。但如果调大后仍然有OOM问题并且理论调用量也不高,或者在承受范围的,那就是对象没有正常回收导致的了,我们继续排查
如果想要查询更多JVM运行时参数,可以通过jnifo
指令
jinfo [option1] 进程pid
其中[option1]可选项如下: :第一个参数不写,默认输出JVM的全部参数和系统属性。
-flag :输出与指定名称对应的所有参数,以及参数值。
-flag [+|-]:开启或者关闭与指定名称对应的参数。
-flag =:设置与指定名称对应参数的值。
-flags:输出JVM全部的参数。
-sysprops:输出JVM全部的系统属性。
jinfo -flags 2019
6、如果根据对象无法排查问题,我们还要进一步打印堆日志,堆日志导出指令
jmap -dump:format=b,file=文件名 进程pid
# eg 这里演示使用的自定义文件后缀,不影响分析,但建议按照规范使用.hprof
jmap -dump:format=b,file=/usr/local/oom.dump 1826
当然上述方法属于事后处理了,如果想要出现OOM问题时自动生成dump文件,可以在配置中开启
-XX:+HeapDumpOnOutOfMemoryError 参数表示当JVM发生OOM时,自动生成DUMP文件。
-XX:HeapDumpPath=${目录}参数表示生成DUMP文件的路径,也可以指定文件名称,例如:-XX:HeapDumpPath=${目录}/java_heapdump.hprof。如果不指定文件名,默认会在项目根目录下生成一个文件,文件名格式为:java_<pid>_<date>_<time>_heapDump.hprof。
7、将dump文件下载到本地,利用各类工具进行分析:
- (1)通过
jhat
分析类内存占用情况(与jmap -histo指令效果类似)
执行分析工具占用内存为1024M
jhat -J-Xmx1024M oom.dump
jhat是jdk自带的一种虚拟机堆转储快照分析工具,只要安装了jdk就能执行,执行完毕后访问localhost:7000可查看分析结果,7000端口可以通过-port
参数调整:
jhat -J-Xmx1024M -port 7001 oom.dump
分析成功后会出现server is ready
在页面最底下可以看到堆对象内存占用情况,也能显示JVM中的各类实例数量,加载的class等
其效果与之前用jmap -histo
的效果类似,但是这里可以显示详细的内存占用数据
- (2)通过
mat
工具分析(推荐)
mat是一个内存分析工具,如果使用的是eclipse作为开发工具,可以直接通过插件安装mat,因为我使用的是idea,所以单独安装下mat,mat工具的安装和使用参考我这篇文章:MAT工具安装
点击Open a Heap Dump
打开之前导出的dump文件
这里选择Leak Suspects Report
,重点分析内存泄漏问题
之后会产生一个内存泄漏的可能原因的分析报告,可根据实际情况参考
我们点击回Overview
,选择Unreachable Objects Histogram
查看可被回收的对象占用统计
和我们之前看到的类似,这个User对象就占用了100多M的内存,并且这些都还在内存里没有被回收,那么我们就要看是什么东西在引用这个对象,导致没有被回收掉。
这里我们可以再右键选择with incoming references
,查看这个类被哪些类引用。(补充:with outgoing references
表示这个类引用到了哪些类)
然后神奇的发现它的引用列表是空的,也就是说并没有被其他对象引用
那么既没有被引用,而本身占用又大,说明这个对象是直接返回的,那么可以定位到最外部层,比如Controller
层,这时我们再去排查代码,就能发现问题所在了。
比如我这里的这个问题,找到controller中用到User的地方发现,有一个死循环再不停创建User,导致了内存溢出,那么问题的原因也就定位到了,再下面的解决就好办了。
3. 总结
OOM问题的实际原因各种各样,就像我们开发时遇到的空指针错误,导致的原因可能有很多,但是排查的思路却差不多,大家之所以对OOM问题避而远之,是因为不能直接看到报错的代码位置,这一点需要我们借助jhat
,jmap
,MAT
等工具来实现。
但只要大家多操作,多积累经验,你会发现这个的排查也没有那么难,那么下期我们将结合实际的线上案例,来一起带大家推导OOM问题解决。