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获取。