1. 概述

转储是从存储介质中提取并保存到特定位置的数据,用于后续分析。Java虚拟机(JVM)负责管理Java应用的内存,当发生错误时,我们可以从JVM获取转储文件来诊断问题。

本文将深入探讨三种常见的Java转储文件——堆转储、线程转储和核心转储,并解析它们的应用场景。

2. 堆转储

运行时,JVM会创建堆内存,用于存放Java应用中正在使用的对象引用。堆转储包含运行时所有使用中对象的当前状态快照

此外,它主要用于分析Java中的内存溢出错误(OutOfMemoryError)

堆转储有两种格式——经典格式和可移植堆格式(PHD)。经典格式可读性强,而PHD是二进制格式,需要工具分析。PHD是堆转储的默认格式。

现代堆转储还包含部分线程信息。从Java 6 Update 14开始,堆转储会包含线程的堆栈跟踪。堆转储中的堆栈跟踪能将对象与使用它们的线程关联起来

Eclipse Memory Analyzer等分析工具支持提取这些信息。

2.1. 应用场景

堆转储在分析Java应用的内存溢出错误时特别有用

看一段抛出OutOfMemoryError的示例代码:

public class HeapDump {
    public static void main(String[] args) {
        List numbers = new ArrayList<>();
        try {
            while (true) {
                numbers.add(10);
            }
        } catch (OutOfMemoryError e) {
            System.out.println("Out of memory error occurred!");
        }
    }
}

上述代码通过无限循环模拟堆内存耗尽场景。在Java中,new关键字负责在堆上分配内存。

要捕获上述代码的堆转储,需要工具支持。最常用的工具之一是jmap

首先通过jps命令获取所有Java进程ID:

$ jps

命令输出所有运行中的Java进程:

12789 Launcher
13302 Jps
7517 HeapDump

我们关注的是HeapDump进程。使用jmap命令配合进程ID捕获堆转储:

 $ jmap -dump:live,file=hdump.hprof 7517

该命令在项目根目录生成hdump.hprof文件。

最后可用Eclipse Memory Analyzer等工具分析转储文件

3. 线程转储

线程转储包含特定时刻Java程序中所有线程的快照

线程是进程的最小执行单元,通过并发执行多个任务提升程序效率。

线程转储可帮助诊断Java应用的性能问题。当应用变慢时,它是分析性能瓶颈的关键工具。

此外,它能检测陷入无限循环的线程还能识别死锁(deadlock)——多个线程互相等待对方释放资源

线程转储还能发现某些线程未获得足够CPU时间的情况,帮助定位性能瓶颈。

3.1. 应用场景

下面是一个因长时间运行任务导致性能问题的示例程序:

public class ThreadDump {
    public static void main(String[] args) {
        longRunningTask();
    }
    
    private static void longRunningTask() {
        for (int i = 0; i < Integer.MAX_VALUE; i++) {
            if (Thread.currentThread().isInterrupted()) {
                System.out.println("Interrupted!");
                break;
            }
            System.out.println(i);
        }
    }
}

上述代码创建了一个循环到Integer.MAX_VALUE的方法,并输出值到控制台。这是一个长时间运行的操作,可能引发性能问题

要分析性能,我们可以捕获线程转储。首先获取所有Java进程ID:

$ jps

jps命令输出所有Java进程:

3042 ThreadDump
964 Main
3032 Launcher
3119 Jps

我们关注ThreadDump进程ID。使用jstack命令配合进程ID获取线程转储

$ jstack -l 3042 > slow-running-task-thread-dump.txt

该命令捕获线程转储并保存为txt文件供后续分析。

4. 核心转储

核心转储(也称崩溃转储)包含程序崩溃或异常终止时的快照

JVM运行的是字节码而非本地代码,因此纯Java代码不会导致核心转储。

但某些Java程序使用Java本地接口(JNI)直接运行本地代码。JNI可能因外部库崩溃导致JVM崩溃,此时可捕获核心转储进行分析。

核心转储是操作系统级别的转储,用于在JVM崩溃时查找本地调用的详细信息

4.1. 应用场景

看一个使用JNI生成核心转储的示例。

首先创建CoreDump类加载本地库:

public class CoreDump {
    private native void core();
    public static void main(String[] args) {
        new CoreDump().core();
    }
    static {
        System.loadLibrary("nativelib");
    }
}

javac编译Java代码:

$ javac CoreDump.java

通过javac -h生成本地方法实现的头文件:

$ javac -h . CoreDump.java

最后用C实现一个会导致JVM崩溃的本地方法:

#include <jni.h>
#include "CoreDump.h"
    
void core() {
    int *p = NULL;
    *p = 0;
}
JNIEXPORT void JNICALL Java_CoreDump_core (JNIEnv *env, jobject obj) {
    core();
};
void main() {
}

gcc编译本地代码:

$ gcc -fPIC -I"/usr/lib/jvm/java-17-graalvm/include" -I"/usr/lib/jvm/java-17-graalvm/include/linux" -shared -o libnativelib.so CoreDump.c

生成名为libnativelib.so的共享库。用共享库编译Java代码:

$ java -Djava.library.path=. CoreDump

本地方法导致JVM崩溃,并在项目目录生成核心转储:

// ...
# A fatal error has been detected by the Java Runtime Environment:
# SIGSEGV (0xb) at pc=0x00007f9c48878119, pid=65743, tid=65744
# C  [libnativelib.so+0x1119]  core+0x10
# Core dump will be written. Default location: Core dumps may be processed with 
# "/usr/lib/systemd/systemd-coredump %P %u %g %s %t %c %h" (or dumping to /core-java-perf/core.65743)
# An error report file with more information is saved as:
# ~/core-java-perf/hs_err_pid65743.log
// ...

输出展示了崩溃信息和转储文件位置。

5. 关键差异

三种Java转储文件的关键差异总结:

转储类型 应用场景 包含内容
堆转储 诊断内存溢出等内存问题 Java堆中所有对象的快照
线程转储 排查性能问题、死锁和无限循环 JVM中所有线程状态的快照
核心转储 调试本地库导致的崩溃 JVM崩溃时的进程状态

6. 结论

本文通过实际应用场景,解析了堆转储、线程转储和核心转储的区别。我们还展示了不同问题的示例代码,并生成了转储文件供分析。每种转储文件在Java应用故障排查中都有独特用途。

如往常一样,示例源码可在GitHub获取。


原始标题:Differences Between Heap Dump, Thread Dump and Core Dump