前段时间,作为工具人的我被要求排查某台服务器CPU 100%。当时听了内心狂喜,终于让我遇到CPU 100%问题了。但是,当时服务器着急重启,没有足够的时间给我排查问题,但是我还是在重启之前把堆Dump文件保存了下来。

这两天忙里偷闲就想着分析一下原因。

问题

CPU 100%

虽然之前没遇到过CPU 100%的问题吧,但是,这个问题该怎么查心里还是有数的。

  1. 使用top命令找到吃CPU的Java进程pid。

    查到pid为:31387,CPU 接近400%,几乎4个核全吃满了。

  2. 查询耗CPU的线程id。

    1
    
    top -Hp 31387
    

    这里,查到了4个线程,每个线程都吃满了一个核接近100%。

    选取一个线程id:31390

  3. 线程id转16进制。

    1
    
    printf "%x\n" 31390
    

    得到结果:7a9e

  4. 输出线程堆栈信息。

    1
    
    jstack 31387 | grep 7a9e -A 60|less
    

    结果忘截图了。不过结果很明显,4个线程都是垃圾收集线程

注意

执行jstack的用户需要与启动Java进程的用户一致。可通过以下命令查看启动Java进程的用户。

1
ps -ef | grep java

垃圾收集

所以,究竟是为什么导致垃圾收集吃满了CPU呢?

1
jstat -gcutil  31387 1000 100

这个我截图了。

这是一张图片

EdenOld全满了,这很明显是内存泄漏了。

但是,内存泄漏的原因是啥呢?

1
jmap -dump:format=b,file=dump.txt 31387

我将堆dump文件保存了下来。

排查

大dump文件分析

当时Dump文件保存下来之后,一看大小。好家伙,7G多。然后我就面临一个问题,没法将这种大文件从服务器上拿下来。所以当时我就立马进行了打包压缩。

1
tar -xcvf dump.tar.gz dump.txt

打包之后,就只有1G多一点,然后我就从服务器上将压缩包拿到本地然后解压。

开始了漫长的分析过程。

jvisualvm

我知道JDK的bin目录下有个叫jvisualvm的程序是可以分析堆dump文件的。

直接点击菜单栏的文件->装入dump.txt加载进去。

注意

这里不要使用VM 核心 dump载入,不然会报错:无效的核心dump文件

但是,由于我们的dump文件太大了,所以会报OOM。所以需要修改jvisualvm的配置。

配置文件在%JAVA_HOME%\lib\visualvm\etc\visualvm.conf

我将配置修改为-J-Xms2048m -J-Xmx2048m,修改之后就可以将dump文件加载进去,但是,很。基本上无法进行分析工作。

jhat

1
jhat -J-mx8g dump.txt

我电脑一共才8G内存,不管我咋调内存参数都会OOM。我电脑可用内存太小了,看来分析需要长驻内存的数据已经大于可用内存,内存置换解决不了问题。

MAT

因为用上面两个工具都太慢了,所以,这次我直接使用Linux版的MAT,直接在服务器上分析,借助服务器的强大性能,加快分析的速度。

  1. 下载Linux版MAT。

    下载地址

  2. 将MAT传到服务器解压后,执行以下命令。

    1
    
    ./mat/ParseHeapDump.sh dump.txt org.eclipse.mat.api:suspects org.eclipse.mat.api:overview org.eclipse.mat.api:top_components
    
  3. 然后将结果中的dump_Leak_Suspects.zipdump_System_Overview.zipdump_Top_Components.zip下载到本地分析。

这里可能会有疑问,org.eclipse.mat.api:suspects org.eclipse.mat.api:overview org.eclipse.mat.api:top_components这些参数是啥,我怎么知道要这么填参数。

附上贴心的传送门:Batch mode

注意

因为环境的原因,可能需要配置MemoryAnalyzer.ini

比如:我添加了-vm配置,将其配置为JDK的bin目录。另外,将内存直接给了8g。

详细配置传送门:Memory Analyzer Configuration

内存泄漏分析报告

这是一张图片

这是一张图片

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
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);
    }

    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();
        }
    }
}

很明显,就是DeleteOnExitHook的静态变量的这个LinkedHashSet内存泄漏了。

另外,除了在服务器上分析。我又在本地机器上使用Windows版的MAT分析了试试。

结果出乎我的意料:完全没问题

MAT,YYDS。

大胆猜测jvisualvm不行,MAT行! 的原因是:

MAT分析会生成文件落到磁盘,所以没有jvisualvm那么吃内存。

解决办法

其实,知道有内存泄漏的问题的时候,我就知道是因为Spring Cloud Config版本太低了。之前升级过,但是,这台服务器的Spring Cloud Config一直是弃用的还是跑的老版本,只要升级版本即可。

在Github上对于这个内存泄漏问题也有相关issue。

MemoryLeak related to the HealthCheck

另外,这不是只有Spring Cloud Config会出现这种问题,因为究其原因还是File.deleteOnExit API 存在缺陷。

详见:JDK-4872014

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public void deleteOnExit() {
        SecurityManager security = System.getSecurityManager();
        if (security != null) {
            security.checkDelete(path);
        }
        if (isInvalid()) {
            return;
        }
        DeleteOnExitHook.add(path);
    }

File.deleteOnExit的作用就是在JVM退出的时候删除文件。

调用File.deleteOnExit会将路径添加到DeleteOnExitHook中的LinkedHashSet里面,如果不断添加,那么就会内存溢出。

所以File.deleteOnExit不能随便作为File.delete()的候选方法,比如:我怕File.delete()没删掉,然后调用File.deleteOnExit。这么做,如果是会创建大量文件的话,那么就很容易把内存撑爆了。