5

记一次dump文件分析历程 - Hans_Hu

 2 years ago
source link: https://www.cnblogs.com/hans-hu/p/15999074.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

今天下午,正酣畅淋漓的搬砖,突然运维同事在群里通知,核心服务某个节点内存异常,服务假死。神经一下子紧张起来,赶紧跑到运维那边观察现象。

观察的结果是服务内存溢出,该服务是核心服务,分配了5G内存。运维在转存快照后,立刻重启服务后正常。在接下来的一段时间里,另一台服务节点也发生了同样的情况。

二、分析过程

这个服务是另外一个同事负责开发的,本着学习的态度,在拿到运维转存的dump文件后,就准备尝试着分析下问题,由于之前没有类似的经历,于是先在网上查了下一般怎么分析类似的问题。

首先尝试使用MAT(Memory Analyzer)工具进行分析,下载后就准备载入dump文件,很不幸由于dump文件过大,载入失败了,于是调大了内存大小,尝试再次载入,但此时这个文件不再尝试重新载入,直接提示载入失败。

先不纠结工具的问题,然后网上说JDK自带的jvisualvm也可以用来分析dump文件, 但也遇到了同样内存不足的问题,再尝试修改jvisualvm的内存限制后, 成功载入了。

看到的界面是这样的,很明显看到char[]占用了近70%的内存,接近4G,这太不正常了,点进去看对应的实例(加载的非常慢,需要耐心)。

在实例数界面中看到实例数达到了千万级,大部分都是一些文件的路径字符串信息。在业务中,我们会生成很多临时文件,然后这些临时文件会删除,这里面大部分保存的是这些临时文件路径。

到这里导致内存泄露的原因似乎找到了,但好像又还不够,是什么原因导致这些临时变量没有被回收呢。

回到家后,还是想着这个事情,于是又开始研究起来,这个时候想起来可以再用MAT试着分析下,毕竟据说工具很强大。重启了电脑之后,经过漫长的等待,载入成功了(果然重启能解决一切问题)。

MAT的界面是这样的,里面包含的信息比较多,对于我这个菜鸟来说,确实一下子不知道看哪里。
那就一个个慢慢看吧,Histogram里面的与使用jvisualvm中看到的信息是相同的。

接下来进入到Dominator Tree视图, 列出当前存活的对象的内存大小,这看起来像是我需要关注的重点。然后查了下这个类 java.io.DeleteOnExitHook 与 内存泄露的相关问题。

这个问题在下面两个链接中给出了说明,大概意思是在删除文件使用 File.deleteOnExit() 方法时,并不是立刻删除文件,而是将该文件路径维护在类DeleteOnExit的一个LinkedHashSet中,最后在JVM关闭的时候,才会去删除这里面的文件,这个方法不能用于长时间运行的服务。
https://stackoverflow.com/questions/40119188/memory-leak-on-deleteonexithook
https://bugs.openjdk.java.net/browse/JDK-6664633

上面的描述,通过源码和JDK文档也都得到了证明。

java
// java.io.File
// Requests that the file or directory denoted by this abstract pathname be deleted when the virtual machine terminates.
public void deleteOnExit() {
	SecurityManager security = System.getSecurityManager();
	if (security != null) {
		security.checkDelete(path);
	}
	if (isInvalid()) {
		return;
	}
	DeleteOnExitHook.add(path);
}

// java.io.DeleteOnExitHook
private static LinkedHashSet<String> files = new LinkedHashSet<>();

static synchronized void add(String file) {
	if(files == null) {
		// DeleteOnExitHook is running. Too late to add a file
		throw new IllegalStateException("Shutdown in progress");
	}

	files.add(file);
}

问题定位于File.deleteOnExit()方法的调用,导致内存泄漏。调用该方法只会将需要删除文件的路径,维护在类DeleteOnExit的一个LinkedHashSet中,在JVM关闭时,才会去真正执行删除文件操作。这样导致DeleteOnExitHook这个对象越来越大,最终内存溢出。

File.delete()File.deleteOnExit() 的区别:
当调用delete()方法时,直接删除文件,不管该文件是否存在,一经调用立即执行
当调用deleteOnExit()方法时,只是相当于对deleteOnExit()作一个声明,当程序运行结束,JVM终止时才真正调用deleteOnExit()方法实现删除操作。

我写了下面这个测试方法,对比 delete()deleteOnExit()的区别,现象会比较明显。使用deleteOnExit时是在文件全部创建,JVM关闭的时候,才一个个删除文件,delete会立刻删除文件。(所以这个方法的使用场景是怎样的,我就不太清楚了)

java
public static void loopTest() throws IOException {
	String root = "D:\\C_Temp\\files\\";

	File path = new File(root);
	if (!path.exists()) {
		path.mkdirs();
	}
	int i = 0;
	while (i < 40000) {
		File file = new File(path, "Hello-" + i + ".txt");
		file.createNewFile();
		file.delete();
//            file.deleteOnExit();
		i++;
	}
}

本次排查经历最大的收获就是尝试利用工具分析dump文件,以前对这种都是望而却步,感觉很难。但这次带着问题去分析、思考,这样下来也不算过于复杂。有些问题不是问题本身难,是自己把它想得很难。

下面是本次的一些思考和踩过的坑,以作备忘。

1. 获取dump文件有两种方法

1)通过 jmap 工具生成可以生成任意Java进程的dump文件

shell
# 先找到PID
ps -ef | grep java

# jmap 转存快照
jmap -dump:format=b,file=/opt/dump/test.dump {PID}

2)通过配置JVM启动参数

shell
#  当程序出现OutofMemory时,将会在相应的目录下生成一份dump文件,如果不指定选项HeapDumpPath则在当前目录下生成dump文件
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/opt/dumps

2. MAT需要JDK11才能运行

解决办法是,打开MAT的安装目录,有一个配置文件MemoryAnalyzer.ini。打开这个文件,在文件中指定JDK版本即可。新增两行配置:

shell
-vm D:/jdkPath/bin/javaw.exe

**3. 在使用jvisualvm分析大的dump文件时,堆查器使用的内存不足

修改JAVA_HOME/lib/visualvm/etc/visualvm.conf文件中 visualvm_default_options="-J-client -J-Xms24 -J-Xmx256m",然后重启jvisualVM即可

4. MAT修改内存空间

分析堆转储文件需要消耗很多的堆空间,为了保证分析的效率和性能,在有条件的情况下,建议分配给 MAT 尽可能多的内存资源。两种方式分配内存资源给 MAT:
1)修改启动参数 MemoryAnalyzer.exe -vmargs -Xmx4g
2)编辑文件 MemoryAnalyzer.ini 添加 -vmargs – Xmx4g

这里也列一个代办项

  • 学习MAT工具的使用

参考的一些文章:

__EOF__


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK