一次内存溢出的填坑经历(转)
在项目运行过程中,可能会出现内存溢出,内存溢出的原因多种多样,而在内存溢出后,我们如何查找和分析内存溢出的原因呢?这里来说一说我遇到的次遇到的内存溢出经历。
大致情况是这样的:应用在启动后,过一段时间(这个时间不确定),内存忽然爆满,然后频繁的YGC,一会过后,老年代爆满,然后是频繁的FGC,最终撑爆内存,抛出OOM。重启应用后,还是这个过程。
1、查看java进程的内存使用情况和GC情况
通过jastat工具查看GC情况,以及各generation占用比例:
jstat -gcutil <pid> 1000
通过观察分析是否是JVM的运行内存分配不合理,可通过新生代、老年代内存占用情况,以及GC情况观察得出。
如果FGC很频繁,说明老年代被迅速占用满,这有可能是老年代本身分配的内存太小,或者是内存中大量对象被引用,导致无法被YGC等等,最终这些对象都进入老年代导致老年代爆满。
要分析是什么原因造成的,可以获取内存溢出时JVM的内存快照,通过对快照的分析,来定位内存溢出的原因。
通过jmap查看JVM堆内存概况:
jmap -heap <pid>
这个内存概况会根据使用的垃圾收集器的不同而有所差异。
2、获取jvmdump
获取jvmdump可通过添加应用服务器(如tomcat)启动参数:
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/home/mikan/jvmdump
配置该参数后,在内存溢出时,JVM就会自动dump出当时的内存快照到HeapDumpPath指定的目录。
也可通过jmap在线获取内存dump,只是在使用jmap获取dump期间应用会被卡住,这可能对线上应用会有影响。如
jmap -J-d64 -dump:format=b,file=/home/mumq/jvmdump.hprof <pid>
其中pid为java进程的ID。
获取到JVM内存快照后,可以使用相关的工具来对它进行分析。
3、分析jvmdump
jvmdump文件一般比较大(几个G),所以在做离线分析时,千万别在服务器上,因为在解析jvmdump时,会占用极大的内存,如果在服务器上,很可能影响线上应用的正常运行。所以一般情况是down到空闲的机器上做离线分析。
分析jvmdump的工具有很多,这里推荐使用的是Memory Analyzer (MAT),下载地址是:http://www.eclipse.org/mat/downloads.php。
当前最新Release版本是1.4.0,提供了两种版本:eclipse插件和独立版本,都可通过上面链接下载。
这里以独立版本为例。在开始分析dump文件之前,先设置MAT的内存,可根据分析的dump文件大小来设置,如果分析的dump文件为2G,那设置2G内存的话,那么解析dump文件生成report会很快,否则,等几个小时都不知道。。
如何设置?在与MAT相同目录下的MemoryAnalyzer.ini文件中添加如下配置:
-
-vmargs
-
-Xms2048m
-
-Xmx4096m
具体MAT的使用方法,可以直接查看MAT自带的示例教程,相对来说还是比较全面的。
在MAT解析完内存快照后,会生成相应的report,通过report可以看到当时内存使用情况、内存溢出的可疑之处(Leak Suspects)。如:
点击Leak Suspects,可以查看到具体的详细信息:
点击detail,查看详细信息,通过 Accumulated Objects by Class in Dominator Tree列表可以看到:
User对象有10W+,怎么会这样呢?通过 Accumulated Objects in Dominator Tree列表可以看到:
是调用了UserServiceImpl.login方法,一次性查询出了10W+的用户数据。如果频繁的调用该方法,则会很快的导致OOM。再查看代码发现在调用login方法时,如果用户名参数传null时,会查询出所有用户名为null的用户。这在数据量还很小或者请求量不大的时候,可能不会有什么问题,一旦数据量上来了,或请求量上来了,那很快就会出现OOM。
找到问题原因,如何解决就容易了。所以在写代码的时候如果考虑不周全,就有可能导致这种OOM的bug。
血的教训啊。