MAT工具使用与OMM问题排查篇


案例1:File#deleteOnExit()

实战案例:记一次dump文件分析历程


声明:本篇文章,根据上面大佬文章学习,自己试着分析了一波,下文中可能有些地方是错误的,或者有更简单的方式,请大佬不要吝啬评论指导一下,感谢!!!

本文示例导出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这个对象越来越大,最终内存溢出。

posted @ 2025-05-04 15:25  Lz_蚂蚱  阅读(102)  评论(0)    收藏  举报