内存溢出问题排查
1. 背景
现场云库环境很卡,top显示xx进程还在,且物理内存已经达到11G(配置给xx的最大内存为8G)。查看xx日志,报错OOM
2. Mat内存分析工具
Mat可以用来分析内存溢出时候的dump文件,一般用到比较多的2个功能。
1) Histogram
这个功能主要查看类和对象的关系、大对象和对象之间的引用关系,打开后如下图:
名词解释:
Class Name:类名。
Objects:对象个数
Shallow Heap:对象本身占用内存的大小,不包含其引用的对象内存
Retained Heap:如果一个对象被释放掉,那会因为该对象的释放而减少引用进而被释放的所有的对象(包括被递归释放的)所占用的heap大小
2) Leak Suspects
Leak Suspects 界面提示可能存在内存的泄露。
然后,是问题一的描述,列出了一些比较大的实例。
3) Java编译时类的命名规范
- 实体类的类名就是它本身,如java.util.concurrent.LinkedBlockingQueue
- Lambda表达式命名规则一般遵循以下格式:
KaTeX parse error: Can't use function '$' in math mode at position 7: Lambda$̲<LambdaUniqueId…Lambda$1675类,并不是代表HardDiskICheckHandler类,而是HardDiskICheckHandler类中的Lambda表达式。 - 内部类使用 $ 符号作为分隔符,如java.util.concurrent.LinkedBlockingQueue$Node表示java.util.concurrent.LinkedBlockingQueue类中的内部类Node。
3. 现象与分析
1) 现象
从现场传回快照文件,用mat工具打开,结果如图二、三、四所示,可以得到如下结果:
- 如图三,java.util.concurrent.LinkedBlockingQueue类的对象占据了99.36%的内存,共计6.1G,这意味着内存溢出很可能与LinkedBlockingQueue有关
- class com.xxx.dse.handler.HardDiskICheckHandler$$Lambda 1675 类和 j a v a . u t i l . c o n c u r r e n t . L i n k e d B l o c k i n g Q u e u e 1675类和java.util.concurrent.LinkedBlockingQueue 1675类和java.util.concurrent.LinkedBlockingQueueNode类的对象多达五千多万,并且所有的java.util.concurrent.LinkedBlockingQueue N o d e 对象的 i t e m 都为 n u l l , n e x t 全指向另一个 j a v a . u t i l . c o n c u r r e n t . L i n k e d B l o c k i n g Q u e u e Node对象的item都为null,next全指向另一个java.util.concurrent.LinkedBlockingQueue Node对象的item都为null,next全指向另一个java.util.concurrent.LinkedBlockingQueueNode对象。
2) 分析
class com.xxx.dse.handler.HardDiskICheckHandler类中的Lambda表达式如下,其中diskCommonCheck()是HardDiskICheckHandler中的一个方法:
这是一个磁盘监测的方法,图五中3这个Lambda表达式用来实现某个ip环境的磁盘监测任务,它有五千多万个实例,并且和java.util.concurrent.LinkedBlockingQueue$Node实例数相等。它们之间有什么关系呢?
查看线程池Executors.newFixedThreadPool方法可知,java.util.concurrent.LinkedBlockingQueue$Node表示的是线程池任务队列的任务,也就是说线程池已经阻塞了五千多万个任务。
3) 猜测
已知现场48个节点,而当时系统的可用线程数是40,也就是说有8个任务被阻塞,且没有执行Lambda表达式中的iterator.next()。并且在这8个阻塞的任务被执行之前,iterator.hasNext()返回总为true,一直往任务队列中插入空任务。
4) 验证
验证的代码见附件,下图是关键代码,主要用于实现上述这种死循环下,任务队列的任务数是否一直增加:
运行结果:
总共就三个任务,执行23秒,但是却阻塞了三百多万个任务,这对内存是一个巨大的开销。
5) 复现
1、 实验组
将HardDiskICheckHandler类的Lambda表达式的线程池的核心线程数改成1,启动参数-Xmx参数改成1024后,出现了内存溢出。分析dump文件,结果如下:
由于家里环境只有两个节点,陷入死循环的时间不多,因此对象的个数不如现场那么多,但占据的内存也高达800M。
2、对比组
线程池的核心线程数改回Runtime.getRuntime().availableProcessors(),启动参数改为1024,没有出现内存溢出了。
4) 结论
当节点数比系统的可用线程数多的时候,就可能出现内存溢出。
5) 代码修改
HardDiskICheckHandler类中,遍历Constant.allIps的方式不用iterator,改用其他循环方式,如for循环:
或者