MAT工具使用与OMM问题排查篇
案例1:File#deleteOnExit()
声明:本篇文章,根据上面大佬文章学习,自己试着分析了一波,下文中可能有些地方是错误的,或者有更简单的方式,请大佬不要吝啬评论指导一下,感谢!!!
本文示例导出dump下载地址:https://files.cnblogs.com/files/leizia/java_pid6220.7z
问题背景
生成临时文件保存在目录下,并删除文件。文件并没有直接删除,文件路径地址被保留占用内存,不能被gc回收,导致内存泄漏。
JDK版本:1.8.0_131
问题原因
删除文件使用File$deleteOnExit()方法。保存在DeleteOnExitHook的LinkedHashSet中文件路径不会释放使用的内存,从而导致内存泄漏。
- deleteOnExit()方法说明
方法并不是立刻删除文件,而是将该文件路径维护在类DeleteOnExitHook的一个LinkedHashSet中,最后在JVM关闭的时候,才会去删除这里面的文件。
示例代码与虚拟机参数
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.io.File;
import java.io.IOException;
/**
* 生成临时文件保存,并删除文件
*
* @author locust
*/
@Component
public class LeakSimulator {
/**
* 每秒执行一次,模拟不断创建临时文件并 deleteOnExit(内存泄漏)
*/
@Scheduled(fixedDelay = 100)
public void createLeakingTempFile() {
try {
// 获取当前工程根目录
String currentDir = System.getProperty("user.dir");
// 在工程目录下创建一个子目录(如:temp)
File tempDir = new File(currentDir, "temp");
if (!tempDir.exists()) {
// 如果目录不存在,则创建
tempDir.mkdirs();
}
// 批量创建 100 个文件并注册 deleteOnExit
for (int i = 0; i < 100; i++) {
File temp = File.createTempFile("leak_", ".tmp", tempDir);
// 注册删除,导致内存泄漏(DeleteOnExitHook)
temp.deleteOnExit(); // 内存泄漏点
}
System.out.println("Created 100 leaking temp files.");
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**************************↓↓↓↓↓ 源码 ↓↓↓↓↓**************************/
public class File implements Serializable, Comparable<File> {
public void deleteOnExit() {
SecurityManager security = System.getSecurityManager();
if (security != null) {
security.checkDelete(path);
}
if (isInvalid()) {
return;
}
DeleteOnExitHook.add(path);
}
}
class DeleteOnExitHook {
private static LinkedHashSet<String> files = new LinkedHashSet<>();
static {
// DeleteOnExitHook must be the last shutdown hook to be invoked.
// Application shutdown hooks may add the first file to the
// delete on exit list and cause the DeleteOnExitHook to be
// registered during shutdown in progress. So set the
// registerShutdownInProgress parameter to true.
sun.misc.SharedSecrets.getJavaLangAccess()
.registerShutdownHook(2 /* Shutdown hook invocation order */,
true /* register even if shutdown in progress */,
new Runnable() {
public void run() {
runHooks();
}
}
);
}
private DeleteOnExitHook() {}
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);
}
// 这里是jvm关闭时才调用delete()方法删除文件
static void runHooks() {
LinkedHashSet<String> theFiles;
synchronized (DeleteOnExitHook.class) {
theFiles = files;
files = null;
}
ArrayList<String> toBeDeleted = new ArrayList<>(theFiles);
// reverse the list to maintain previous jdk deletion order.
// Last in first deleted.
Collections.reverse(toBeDeleted);
for (String filename : toBeDeleted) {
(new File(filename)).delete();
}
}
}
-Xmx32m -XX:+HeapDumpOnOutOfMemoryError


MAT工具分析

使用MAT工具打开dump文件,在上述VM配置中-XX:+HeapDumpOnOutOfMemoryError内存溢出时自动打印dump文件。
Leak Suspects疑似泄漏报告

在工具中也给出了泄漏点说明:


Histogram直方图分析

在直方图中可以看到浅堆占用最大的是char[],第二是LinkedHashMap。
浅堆说明:Eclipse-MAT工具


排除软、弱、虚引用后,看到一个线程占用的浅堆很大

点开线程详情,可以看到有很多的LinkedHashMap,在map中保存了String类型文件路径地址。

分析到这里,得出的结论是有一个LinkedHashMap中保存了大量的文件路径地址,不能被回收导致泄漏。
上面示例代码中,DeleteOnExitHook类里是LinkedHashSet,这里为什么显示的是LinkedHashMap,原因是LinkedHashSet继承了HashSet,而HashSet底层是HashMap,set的元素其实是map的key,所以这里看到的是LinkedHashMap,而不是LinkedHashSet。
因为文件路径被强引用,所以不能被回收,我们找到它的入引用位置,查看它的线程堆栈信息,就可以定位到代码位置。



Dominator Tree支配树分析

点进支配树,发现有深堆了,在直方图中深堆是没有的,并且深堆还挺大,不知道深堆是啥,不管它了,直接点浅堆,看最大的怎么个事。

浅堆最大的是HashMap,排除其他引用类型,找强引用的地方。



查看table里也可以看到DeleteOnExitHook类,查看线程堆栈信息,可以找到代码位置了。


解决方法
改用File#delete() 方法,不管是否存在文件都删除。
再次说明下原因:
问题定位于File#deleteOnExit()方法的调用,导致内存泄漏。调用该方法只会将需要删除文件的路径,维护在类DeleteOnExitHook的一个LinkedHashSet中,在JVM关闭时,才会去真正执行删除文件操作。这样导致DeleteOnExitHook这个对象越来越大,最终内存溢出。
本文来自博客园,作者:Lz_蚂蚱,转载请注明原文链接:https://www.cnblogs.com/leizia/p/18859258

浙公网安备 33010602011771号