跳过正文
全网最硬核 JDK 解析 - 5. Heap dump 与错误处理诊断相关演进与最佳实践解析
  1. 文章/

全网最硬核 JDK 解析 - 5. Heap dump 与错误处理诊断相关演进与最佳实践解析

·27165 字·55 分钟
NeatGuyCoding
作者
NeatGuyCoding
目录

0. 观前提醒
#

本文深入分析 JVM 错误处理和诊断相关参数的设计原理、实现机制和版本演进。文章涵盖以下内容:

  1. Heap Dump 相关参数:HeapDumpOnOutOfMemoryError、HeapDumpBeforeFullGC、HeapDumpAfterFullGC 等
  2. OutOfMemoryError 处理参数:OnOutOfMemoryError、CrashOnOutOfMemoryError、ExitOnOutOfMemoryError
  3. 错误日志和诊断参数:ErrorFile、ShowMessageBoxOnError、CreateCoredumpOnCrash 等
  4. 其他诊断参数:MaxJavaStackTraceDepth、SelfDestructTimer

本文涉及 JVM 内部实现细节,包括 C++ 源码分析、安全点机制、VM_Operation 机制等。文章中的设计思想和实现原理分析主要基于个人对源码的理解,如有不准确之处,欢迎指正!本文主要基于 JDK 11、17、21、25 的 HotSpot 源码进行分析。

这些里面我们可能最常见的就是 -XX:+HeapDumpOnOutOfMemoryError,但是无论哪个版本,都不推荐在生产环境开启任何 Heap Dump 参数。后续章节会说明原因,并告诉你如何不依赖这个参数去定位 Java 对象堆 OOM 问题。

我们最常见的需要定位的错误就是 java.lang.OutOfMemoryError,在引入虚拟线程之后,这个问题会更加明显。原来的时候没有虚拟线程,因为 I/O 阻塞线程,阻塞任务队列后,就卡住了吞吐量。引入虚拟线程之后,I/O 阻塞线程会被挂起,继续调度其他虚拟线程去跑任务,这样就会让吞吐量提升很多。但是目前大部分框架针对这个背压问题以及限流问题还没有形成一个比较成熟的方案,所以在高并发场景下,虚拟线程可能会让内存消耗更快,从而更容易触发 OutOfMemoryError并且,发生 OutOfMemoryError 时,JVM 状态已经不健康了,这个 OutOfMemoryError 可能在任意一个触发对象分配的代码中抛出来,但是不论是哪里的 Java 代码,就算是 JDK 内部的 Java 代码也没有通过 catch (Throwable t) 的方式捕获 OutOfMemoryError,这就造成了某些内部状态不一致的问题。比如 JDK 内部的 HashMap,在 put 的时候触发 OutOfMemoryError,这个 put 操作可能会导致 HashMap 内部的数组扩容,而扩容过程中如果发生 OutOfMemoryError,那么这个 HashMap 可能就处于一个不一致的状态,导致后面这个 HashMap 无法正常使用。如果是业务代码,可能会有更严重的问题。发生 OutOfMemoryError 后,我们最安全的做法就是尽快让这个进程退出,避免继续运行导致更多的问题。而 JDK 内部的参数机制正好可以让我们实现这一点

1. 概述
#

1.1. 错误处理和诊断参数分类
#

JVM 的错误处理和诊断参数可以分为以下几类:

1.1.1. Heap Dump 相关参数
#

用于在特定时机转储 Java 对象堆内存快照,便于后续分析:

  • -XX:+HeapDumpOnOutOfMemoryError:Java 对象堆 OOM 时转储 Java 对象堆
    • JDK 1.6 引入(不考虑 Back Port):https://bugs.openjdk.org/browse/JDK-6280629
    • 默认值:false(JDK 11、17、21、25)
  • -XX:+HeapDumpBeforeFullGC:Full GC 前转储 Java 对象堆
    • JDK 1.7 引入(不考虑 Back Port):https://bugs.openjdk.org/browse/JDK-6797870
    • 默认值:false(JDK 11、17、21、25)
  • -XX:+HeapDumpAfterFullGC:Full GC 后转储 Java 对象堆
    • JDK 1.7 引入(不考虑 Back Port):https://bugs.openjdk.org/browse/JDK-6797870
    • 默认值:false(JDK 11、17、21、25)
  • -XX:HeapDumpPath=<path>:Java 对象堆转储文件路径
    • JDK 1.6 引入(不考虑 Back Port):https://bugs.openjdk.org/browse/JDK-6280629
    • 默认值:NULL(JDK 11、17),nullptr(JDK 21、25)
  • -XX:HeapDumpGzipLevel=<level>:Java 对象堆转储文件压缩级别
    • JDK 17 引入(不考虑 Back Port):https://bugs.openjdk.org/browse/JDK-8260282
    • 默认值:不支持(JDK 11),0(JDK 17、21、25)
  • -XX:FullGCHeapDumpLimit=<count>:Full GC Java 对象堆转储次数限制
    • JDK 23 引入(不考虑 Back Port):https://bugs.openjdk.org/browse/JDK-8321442
    • 默认值:不支持(JDK 11、17、21),0(JDK 25)

1.1.2. OutOfMemoryError 处理参数
#

用于在发生 Java 对象堆 OOM 时执行特定操作:

  • -XX:OnOutOfMemoryError=<command>:执行用户定义的命令或脚本
    • JDK 1.6 引入(不考虑 Back Port)
    • 默认值:""(空字符串,JDK 11、17、21、25)
  • -XX:+CrashOnOutOfMemoryError:Java 对象堆 OOM 时触发 JVM 崩溃
    • JDK 9 引入(不考虑 Back Port):https://bugs.openjdk.org/browse/JDK-8138745
    • 默认值:false(JDK 11、17、21、25)
  • -XX:+ExitOnOutOfMemoryError:Java 对象堆 OOM 时退出 JVM
    • JDK 9 引入(不考虑 Back Port):https://bugs.openjdk.org/browse/JDK-8138745
    • 默认值:false(JDK 11、17、21、25)

1.1.3. 错误日志和诊断参数
#

用于控制错误日志输出和诊断行为:

  • -XX:ErrorFile=<file>:错误日志文件路径
    • JDK 1.6 引入(不考虑 Back Port):https://bugs.openjdk.org/browse/JDK-6872355
    • 默认值:NULL(JDK 11、17、21、25)
  • -XX:+ShowMessageBoxOnError:在致命错误时显示消息框,这个一般用不到了
    • JDK 1.4 引入(不考虑 Back Port)
    • 默认值:false(JDK 11、17、21、25)
  • -XX:+CreateCoredumpOnCrash / -XX:-CreateCoredumpOnCrash:在致命错误时创建核心转储
    • 早期版本引入
    • 默认值:true(JDK 11、17、21、25)
  • -XX:+SuppressFatalErrorMessage:抑制致命错误消息
    • 早期版本引入
    • 默认值:false(JDK 11、17、21、25)
  • -XX:OnError=<command>:在致命错误时执行用户定义的命令
    • 早期版本引入
    • 默认值:""(空字符串,JDK 11、17、21、25)
  • -XX:ErrorLogTimeout=<seconds>:错误日志写入超时时间
    • 早期版本引入
    • 默认值:120(2 分钟,JDK 11、17、21、25)

1.1.4. 其他诊断参数
#

  • -XX:MaxJavaStackTraceDepth=<depth>:Java 异常堆栈跟踪最大深度
    • 早期版本引入
    • 默认值:1024(JDK 11、17、21、25)
  • -XX:SelfDestructTimer=<minutes>:自毁定时器(用于测试)
    • 早期版本引入
    • 默认值:0(关闭,JDK 11、17、21、25)

1.2. 版本演进概览
#

版本主要特性关键改进
JDK 11基础功能基本的 Heap Dump 和 Java 对象堆 OOM 处理支持
JDK 17压缩支持新增 gzip 压缩,改进错误处理
JDK 21快速退出使用 _exit 快速退出,C++11 现代化
JDK 25并行转储支持并行转储,%p 占位符,栈上分配,FullGC 转储次数限制

2. Heap Dump 相关参数
#

2.1. 为何不推荐开启 Heap Dump 参数?
#

重要提醒:无论哪个版本,都不推荐在生产环境开启任何 Heap Dump 参数。下面会说明原因,后续章节会告诉你如何不依赖这个参数去定位 Java 对象堆 OOM 问题。

2.1.1. 磁盘 IO 性能限制
#

Heap Dump 需要输出到硬盘,而硬盘 IO 速度相对有限:

  • HDD:顺序存储 100–200 MB/s
  • SSD:顺序存储 500 MB/s 到数 GB/s 不等

Heap Dump 是顺序写入,但是 Java 对象堆可能非常大,比如 8GB、16GB,甚至更大。如果 Java 对象堆非常大,写入时间会非常长。而且你跑 Java 应用的机器,不可能每台都配备高速 SSD,并且现在云环境、容器环境下,磁盘 IO 可能会被限速和共享。

2.1.2. 压缩开销
#

JDK 17 虽然引入了 gzip 压缩 Java 对象堆的功能,并且 JDK 25 引入了多线程 Heap Dump,可以多线程压缩,但是压缩是 CPU 密集型操作。假设 4 个 CPU,一个经验指标是:

  • 级别 0/1(最快)
    • 压缩比:2.3×–2.7×
    • 吞吐:1.0–3.0 GB/s
    • 每 GB 耗时:0.33–1.0 s
  • 级别 3
    • 压缩比:2.5×–2.9×
    • 吞吐:0.9–2.2 GB/s
    • 每 GB 耗时:0.45–1.1 s
  • 级别 5(性价比常用)
    • 压缩比:2.7×–3.1×
    • 吞吐:0.7–1.8 GB/s
    • 每 GB 耗时:0.55–1.4 s
  • 级别 6(默认/稳妥)
    • 压缩比:2.8×–3.2×
    • 吞吐:0.6–1.6 GB/s
    • 每 GB 耗时:0.62–1.7 s
  • 级别 7
    • 压缩比:2.9×–3.3×
    • 吞吐:0.45–1.2 GB/s
    • 每 GB 耗时:0.83–2.2 s
  • 级别 8
    • 压缩比:2.9×–3.4×
    • 吞吐:0.35–0.9 GB/s
    • 每 GB 耗时:1.1–2.9 s
  • 级别 9(极致比)
    • 压缩比:3.0×–3.5×
    • 吞吐:0.25–0.7 GB/s
    • 每 GB 耗时:1.4–4.0 s

假设设置级别是 5,4 CPU 的情况下,针对 8GB Java 对象堆,压缩时间大概 4.4–11.2 秒,压缩后文件大概 2.6–3.2 GB,写入 HDD 大概 13–32 秒,一共大概 17.4–43.2 秒。这是理想情况下的估计,实际情况可能更差。

2.1.3. 执行顺序影响
#

一般线上会用 -XX:OnOutOfMemoryError=xxxxx 指定一个脚本,来做一些让 Java 进程微服务在注册中心下线的行为,因为一般发生 OutOfMemoryError 的进程都是不再健康的,运行业务可能有问题。但是,如果你开启了 HeapDumpOnOutOfMemoryError,那么必须等 HeapDumpOnOutOfMemoryError 执行完才能做 OnOutOfMemoryError 的脚本。

这意味着:服务无法更及时从注册中心下线,不健康的服务可能继续接收更多的请求。越早执行脚本下线越好,越晚下线可能会影响更多的请求。

2.2. HeapDumpOnOutOfMemoryError
#

2.2.1. 参数说明
#

说明:当发生 java.lang.OutOfMemoryError 时,自动转储 Java 对象堆内存到文件。

默认false

类型manageable(JDK 11)或 product + MANAGEABLE(JDK 17+),可通过 JMX 动态修改

举例-XX:+HeapDumpOnOutOfMemoryError

2.2.2. 触发场景
#

重要说明HeapDumpOnOutOfMemoryError 只会在调用 report_java_out_of_memory 的 OOM 类型时触发 Heap Dump。以下类型的 OOM 会触发 Heap Dump:

  1. java.lang.OutOfMemoryError: Java heap space:Java 对象堆空间耗尽(Java 对象堆 OOM)
  2. java.lang.OutOfMemoryError: GC overhead limit exceeded:GC 开销超限(Java 对象堆相关 OOM)
  3. java.lang.OutOfMemoryError: Metaspace / Compressed class space:元空间耗尽(会触发 Java 对象堆转储,因为加载的类在 Java 对象堆上有 Class 对象)
  4. java.lang.OutOfMemoryError: Requested array size exceeds VM limit:数组大小超限(会触发 Java 对象堆转储,因为通常表示集合过大)

不会触发 Heap Dump 的 OOM 类型

  • 线程创建失败(unable to create native thread
  • Unicode 字符串分配失败
  • 类验证器内存不足
  • Unsafe 分配失败
  • 反优化时重新分配对象失败(realloc_objects
  • 可重试分配失败(retry,JDK 17+)
  • 内部 OOM 标记场景(JDK 25+)

详细说明见 2.2.3.1 节。

2.2.3. 实现机制
#

2.2.3.1. OutOfMemoryError 类型与 Heap Dump 触发关系
#

JVM 中定义了多种 OutOfMemoryError 类型,但只有调用 report_java_out_of_memory 的类型才会触发 Heap Dump:

会触发 Heap Dump 的 OutOfMemoryError(✅):

  1. java.lang.OutOfMemoryError: Java heap space - Java 对象堆空间耗尽
  2. java.lang.OutOfMemoryError: GC overhead limit exceeded - GC 开销超限
  3. java.lang.OutOfMemoryError: Metaspace - 元空间耗尽
  4. java.lang.OutOfMemoryError: Compressed class space - 压缩类空间耗尽
  5. java.lang.OutOfMemoryError: Requested array size exceeds VM limit - 数组大小超限

不会触发 Heap Dump 的 OutOfMemoryError(❌):

  1. java.lang.OutOfMemoryError: realloc_objects - 反优化时重新分配对象失败(直接抛出,不调用 report_java_out_of_memory
  2. java.lang.OutOfMemoryError: retry(JDK 17+)- 可重试分配失败(内部机制,不调用 report_java_out_of_memory
  3. java.lang.OutOfMemoryError: Java heap space(无堆栈跟踪)(JDK 25+)- 内部 OOM 标记场景(不调用 report_java_out_of_memory
  4. java.lang.OutOfMemoryError: unable to create native thread: possibly out of memory or process/resource limits reached - 线程创建失败(使用 THROW_MSG,不调用 report_java_out_of_memory
  5. java.lang.OutOfMemoryError: could not allocate Unicode string - Unicode 字符串分配失败(使用 THROW_MSG_0,不调用 report_java_out_of_memory
  6. java.lang.OutOfMemoryError(类验证器内存不足) - 类验证过程中内存不足(使用 THROW_MSG_,不调用 report_java_out_of_memory
  7. java.lang.OutOfMemoryError(Unsafe 分配失败) - Unsafe 操作中内存分配失败(使用 THROW_0,不调用 report_java_out_of_memory

其他内存不足情况(不抛出 OutOfMemoryError):

  • CodeCache 内存不足 - 直接调用 vm_exit_out_of_memory,VM 直接退出,不抛出异常

触发机制:只有调用 report_java_out_of_memory 的 OOM 类型才会检查 HeapDumpOnOutOfMemoryError 标志并触发 Heap Dump。其他 OOM 类型直接抛出异常,不经过 report_java_out_of_memory 函数。

2.2.3.1.1. 不会触发 Heap Dump 的 OOM 类型源码说明
#

线程创建失败jdk-jdk-11-28/src/hotspot/share/prims/jvm.cpp

if (native_thread->osthread() == NULL) {
  // 线程创建失败,直接抛出 OutOfMemoryError,不调用 report_java_out_of_memory
  THROW_MSG(vmSymbols::java_lang_OutOfMemoryError(),
            os::native_thread_creation_failed_msg());
}

Unicode 字符串分配失败jdk-jdk-11-28/src/hotspot/share/classfile/javaClasses.cpp

// Unicode 字符串分配失败,直接抛出 OutOfMemoryError
THROW_MSG_0(vmSymbols::java_lang_OutOfMemoryError(), "could not allocate Unicode string");

类验证器内存不足jdk-jdk-11-28/src/hotspot/share/classfile/verifier.cpp

// 类验证过程中内存不足,直接抛出 OutOfMemoryError
THROW_MSG_(vmSymbols::java_lang_OutOfMemoryError(), message, NULL);

Unsafe 分配失败jdk-jdk-11-28/src/hotspot/share/prims/unsafe.cpp

u1* class_bytes = NEW_C_HEAP_ARRAY(u1, length, mtInternal);
if (class_bytes == NULL) {
  // Unsafe 操作中内存分配失败,直接抛出 OutOfMemoryError
  THROW_0(vmSymbols::java_lang_OutOfMemoryError());
}

CodeCache 内存不足(不抛出异常): jdk-jdk-11-28/src/hotspot/share/code/codeBlob.cpp

if (blob == NULL) {
  // CodeCache 内存不足,直接退出 VM,不抛出异常
  vm_exit_out_of_memory(size, OOM_MALLOC_ERROR, "CodeCache: no room for method handle adapter blob");
}

2.2.3.2. OOM 检测与报告流程图
#

flowchart TD
    A[各种资源分配场景] --> B{资源类型}
    B -->|Java Heap| C[MemAllocator::check_out_of_memory]
    B -->|Metaspace| D[Metaspace::allocate]
    B -->|数组大小| E1[TypeArrayKlass::allocate
JDK 11] B -->|数组大小| E2[Klass::check_array_allocation_length
JDK 17+] B -->|其他资源| F[其他分配路径
如: realloc_objects
不触发HeapDump] C --> G{分配失败原因} G -->|堆空间不足| H1{可重试分配检查
JDK 17+: in_retryable_allocation
JDK 25+: is_in_internal_oome_mark} G -->|GC开销超限| I1{可重试分配检查
JDK 17+: in_retryable_allocation
JDK 25+: is_in_internal_oome_mark} H1 -->|否| H[report_java_out_of_memory
Java heap space] H1 -->|是 JDK 17+| H2[抛出 retry OOM
不触发HeapDump] H1 -->|是 JDK 25+| H3[抛出无堆栈跟踪 OOM
不触发HeapDump] I1 -->|否| I[report_java_out_of_memory
GC overhead limit exceeded] I1 -->|是 JDK 17+| I2[抛出 retry OOM
不触发HeapDump] I1 -->|是 JDK 25+| I3[抛出无堆栈跟踪 OOM
不触发HeapDump] D --> J[report_java_out_of_memory
Metaspace或
Compressed class space] E1 --> K1[report_java_out_of_memory
Requested array size
exceeds VM limit] E2 --> K2{可重试分配检查
JDK 17+: in_retryable_allocation} K2 -->|否| K[report_java_out_of_memory
Requested array size
exceeds VM limit] K2 -->|是| K3[抛出 retry OOM
不触发HeapDump] H --> L[report_java_out_of_memory函数] I --> L J --> L K --> L K1 --> L L --> M1{Atomic::cmpxchg
JDK 11: cmpxchg 1, addr, 0
JDK 17+: cmpxchg addr, 0, 1} M1 -->|已报告| Z[跳过] M1 -->|首次报告| N{HeapDumpOnOutOfMemoryError
是否启用?} N -->|是| O[打印OOM消息] N -->|否| P[检查OnOutOfMemoryError] O --> Q[HeapDumper::dump_heap_from_oome] Q --> R[HeapDumper::dump_heap true] R --> S1[构建dump文件路径
JDK 11-21: os::malloc
JDK 25: 栈上分配] S1 --> S2{HeapDumpPath
JDK 25: 支持%p占位符} S2 --> T[创建HeapDumper对象] T --> U1[VMThread::execute
触发安全点] U1 --> U2[VM_HeapDumper::doit
在安全点执行转储] U2 --> U3{压缩
JDK 17+: HeapDumpGzipLevel} U3 -->|启用| U4[并行压缩
JDK 25: 多线程] U3 -->|禁用| U5[直接写入] U4 --> P U5 --> P P --> V{其他OOM处理选项} V --> W[CrashOnOutOfMemoryError] V --> X[ExitOnOutOfMemoryError] W --> Y1[fatal
JDK 11] W --> Y2[report_fatal
JDK 17+] X --> AA1[os::exit 3
JDK 11-17] X --> AA2[os::_exit 3
JDK 21+
快速退出] style H fill:#90EE90 style I fill:#90EE90 style J fill:#FFB6C1 style K fill:#FFB6C1 style K1 fill:#FFB6C1 style Q fill:#87CEEB style U2 fill:#87CEEB style H2 fill:#FFE4B5 style I2 fill:#FFE4B5 style K3 fill:#FFE4B5 style H3 fill:#FFE4B5 style I3 fill:#FFE4B5

2.2.3.3. OOM 检测与报告源码分析
#

2.2.3.3.1. Java heap space / GC overhead limit exceeded
#

jdk-jdk-11-28/src/hotspot/share/gc/shared/memAllocator.cpp

JDK 11

bool MemAllocator::Allocation::check_out_of_memory() {
  // ... 省略分配失败检查 ...
  
  if (!_overhead_limit_exceeded) {
    // Java 对象堆空间不足,调用 report_java_out_of_memory 触发 Heap Dump
    report_java_out_of_memory("Java heap space");
    // ... 省略 JVMTI 相关代码 ...
    THROW_OOP_(Universe::out_of_memory_error_java_heap(), true);
  } else {
    // GC 开销超限,调用 report_java_out_of_memory 触发 Heap Dump
    report_java_out_of_memory("GC overhead limit exceeded");
    // ... 省略 JVMTI 相关代码 ...
    THROW_OOP_(Universe::out_of_memory_error_gc_overhead_limit(), true);
  }
}

JDK 17+

bool MemAllocator::Allocation::check_out_of_memory() {
  // ... 省略分配失败检查 ...
  
  const char* message = _overhead_limit_exceeded ? 
    "GC overhead limit exceeded" : "Java heap space";
  
  // JDK 17+ 新增可重试分配检查,避免在可重试场景触发 Heap Dump
  if (!_thread->in_retryable_allocation()) {  // JDK 17-21
  // if (!_thread->is_in_internal_oome_mark()) {  // JDK 25+
    report_java_out_of_memory(message);
    // ... 省略 JVMTI 相关代码 ...
    THROW_OOP_(exception, true);
  } else {
    // 可重试分配失败,抛出 retry OOM(不触发 Heap Dump)
    THROW_OOP_(Universe::out_of_memory_error_retry(), true);  // JDK 17-21
    // THROW_OOP_(Universe::out_of_memory_error_java_heap_without_backtrace(), true);  // JDK 25+
  }
}

版本差异

  • JDK 17+:新增 in_retryable_allocation() 检查,可重试分配失败不触发 Heap Dump
  • JDK 25+:使用 is_in_internal_oome_mark() 替代,内部 OOM 标记场景不触发 Heap Dump

JDK 25+ 内部 OOM 标记场景说明

InternalOOMEMark 是一个 RAII(Resource Acquisition Is Initialization)标记类,用于标识线程处于 JVM 内部操作中的内存分配场景。在这些场景中,如果内存分配失败,不应该触发 Heap Dump 和完整的 OOM 处理流程,因为:发生在 JVM 内部(如反优化、编译、类加载等),不是用户代码直接触发的。用户代码无法捕获这些异常,并且可能可以快速恢复,或者如果是核心操作则可以快速失败退出。

使用场景

  • 反优化时重新分配对象:反优化过程中需要重新分配对象,如果失败,抛出无堆栈跟踪的 OOM
  • 编译时解析字符串常量:JIT 编译过程中解析字符串常量时分配失败
  • 类加载时的数组分配:类加载过程中分配数组失败
  • JVMCI 相关操作:JVMCI 编译器相关操作中的内存分配失败

在这些场景中,如果分配失败,会抛出 out_of_memory_error_java_heap_without_backtrace(),这是一个无堆栈跟踪的 OOM 异常,不调用 report_java_out_of_memory,因此不会触发 Heap Dump。

源码示例

jdk-jdk-25-36/src/hotspot/share/gc/shared/memAllocator.hpp

// RAII 类,用于标记线程处于内部 OOM 处理场景
// 在这些场景中,OOM 不会传播到用户代码,因此不需要堆栈跟踪
class InternalOOMEMark: public StackObj {
  explicit InternalOOMEMark(JavaThread* thread) {
    _outer = thread->is_in_internal_oome_mark();
    thread->set_is_in_internal_oome_mark(true);  // 设置标记
    _thread = thread;
  }
  
  ~InternalOOMEMark() {
    _thread->set_is_in_internal_oome_mark(_outer);  // 恢复标记
  }
};

jdk-jdk-25-36/src/hotspot/share/runtime/deoptimization.cpp

// 反优化时重新分配对象,使用 InternalOOMEMark 标记
if (obj == nullptr && !cache_init_error) {
  InternalOOMEMark iom(THREAD);  // 进入内部 OOM 标记场景
  obj = ik->allocate_instance(THREAD);  // 如果失败,抛出无堆栈跟踪的 OOM
}
2.2.3.3.2. Metaspace / Compressed class space
#

jdk-jdk-11-28/src/hotspot/share/memory/metaspace.cpp

void Metaspace::allocate(ClassLoaderData* cld, size_t word_size, 
                         bool read_only, MetaspaceObj::Type type, 
                         Metaspace** result, TRAPS) {
  // ... 省略元空间分配逻辑 ...
  
  // 元空间或压缩类空间耗尽,调用 report_java_out_of_memory 触发 Heap Dump
  // 合理性:加载的类在 Java 对象堆上有 Class 对象,Heap Dump 有助于分析类加载问题
  const char* space_string = out_of_compressed_class_space ?
    "Compressed class space" : "Metaspace";
  report_java_out_of_memory(space_string);
  // ... 省略异常抛出 ...
}
2.2.3.3.3. Requested array size exceeds VM limit
#

JDK 11jdk-jdk-11-28/src/hotspot/share/oops/typeArrayKlass.cpp

typeArrayOop TypeArrayKlass::allocate(int length, TRAPS) {
  // ... 省略数组分配逻辑 ...
  
  if (length > max_length()) {
    // 数组大小超过 VM 限制,调用 report_java_out_of_memory 触发 Heap Dump
    // 合理性:通常表示集合过大,可能存在内存泄漏
    report_java_out_of_memory("Requested array size exceeds VM limit");
    JvmtiExport::post_array_size_exhausted();
    THROW_OOP_0(Universe::out_of_memory_error_array_size());
  }
}

JDK 17+jdk-jdk-17-35/src/hotspot/share/oops/klass.cpp

void Klass::check_array_allocation_length(int length, int max_length, TRAPS) {
  if (length > max_length) {
    // JDK 17+ 新增可重试分配检查
    if (!THREAD->in_retryable_allocation()) {
      report_java_out_of_memory("Requested array size exceeds VM limit");
      JvmtiExport::post_array_size_exhausted();
      THROW_OOP(Universe::out_of_memory_error_array_size());
    } else {
      // 可重试分配失败,抛出 retry OOM(不触发 Heap Dump)
      THROW_OOP(Universe::out_of_memory_error_retry());
    }
  }
}

2.2.3.4. OOM 报告处理
#

jdk-jdk-11-28/src/hotspot/share/utilities/debug.cpp

JDK 11

void report_java_out_of_memory(const char* message) {
  static int out_of_memory_reported = 0;

  // 多个线程可能同时尝试报告 OutOfMemoryError,使用原子操作确保只执行一次
  // JDK 11: Atomic::cmpxchg(expected, addr, new_value) 返回旧值
  if (Atomic::cmpxchg(1, &out_of_memory_reported, 0) == 0) {
    // 在 OnOutOfMemoryError 命令执行之前创建 Java 对象堆转储
    if (HeapDumpOnOutOfMemoryError) {
      tty->print_cr("java.lang.OutOfMemoryError: %s", message);
      HeapDumper::dump_heap_from_oome();
    }

    // ... 省略 OnOutOfMemoryError 处理 ...
    
    if (CrashOnOutOfMemoryError) {
      fatal("OutOfMemory encountered: %s", message);
    }

    if (ExitOnOutOfMemoryError) {
      os::exit(3);
    }
  }
}

JDK 17+

void report_java_out_of_memory(const char* message) {
  static int out_of_memory_reported = 0;

  // JDK 17+: Atomic::cmpxchg(addr, expected, new_value) 参数顺序变化
  if (Atomic::cmpxchg(&out_of_memory_reported, 0, 1) == 0) {
    if (HeapDumpOnOutOfMemoryError) {
      tty->print_cr("java.lang.OutOfMemoryError: %s", message);
      HeapDumper::dump_heap_from_oome();
    }

    // ... 省略 OnOutOfMemoryError 处理 ...
    
    if (CrashOnOutOfMemoryError) {
      // JDK 17+: 使用 report_fatal 替代 fatal
      report_fatal(OOM_JAVA_HEAP_FATAL, __FILE__, __LINE__, "OutOfMemory encountered: %s", message);
    }

    if (ExitOnOutOfMemoryError) {
      os::exit(3);  // JDK 17
      // os::_exit(3);  // JDK 21+: 快速退出,不运行清理钩子
    }
  }
}

关键点

  • 原子操作版本差异:JDK 11 使用 cmpxchg(1, &addr, 0),JDK 17+ 使用 cmpxchg(&addr, 0, 1)(参数顺序变化)
  • 退出机制差异:JDK 21+ 使用 os::_exit(3) 快速退出,不运行清理钩子
  • 注意:所有调用 report_java_out_of_memory 的地方都会检查 HeapDumpOnOutOfMemoryError,没有对 message 类型进行过滤

2.2.3.5. 安全点执行机制
#

jdk-jdk-11-28/src/hotspot/share/services/heapDumper.cpp

// 由 OOM 错误报告调用,在 Java 线程中,不在安全点
void HeapDumper::dump_heap_from_oome() {
  HeapDumper::dump_heap(true);
}

// 由错误报告调用(不在安全点)或由 VM 线程在 GC 安全点调用
void HeapDumper::dump(const char* path) {
  // ... 省略 writer 创建等代码 ...
  
  // 创建 VM_HeapDumper 操作对象
  VM_HeapDumper dumper(&writer, _gc_before_heap_dump, _oome);
  
  if (Thread::current()->is_VM_thread()) {
    // 如果已经是 VM 线程,必须在安全点
    assert(SafepointSynchronize::is_at_safepoint(), "Expected to be called at a safepoint");
    dumper.doit();
  } else {
    // 否则通过 VMThread::execute 触发安全点,所有 Java 线程暂停
    VMThread::execute(&dumper);
  }
  // ... 省略其他代码 ...
}

安全点执行机制

  1. OOM 场景report_java_out_of_memory 由 Java 线程调用,不在安全点
  2. 转储触发:调用 HeapDumper::dump_heap_from_oome()HeapDumper::dump_heap()
  3. 安全点进入:通过 VMThread::execute(&dumper) 触发安全点,所有 Java 线程暂停
  4. 转储执行:VM_thread 在安全点执行 VM_HeapDumper::doit(),遍历 Java 对象堆

关键点

  • Java 对象堆转储在安全点执行,确保 Java 对象堆状态一致性
  • ✅ 通过 VM_Operation 机制,由 VM_thread 在安全点执行
  • ✅ 所有 Java 线程在转储期间暂停,避免并发修改

2.2.3.6. Java 对象堆转储执行
#

JDK 11jdk-jdk-11-28/src/hotspot/share/services/heapDumper.cpp

void HeapDumper::dump_heap(bool oome) {
  static char base_path[JVM_MAXPATHLEN] = {'\0'};
  static uint dump_file_seq = 0;
  char* my_path;
  
  const char* dump_file_name = "java_pid";
  const char* dump_file_ext  = ".hprof";

  if (dump_file_seq == 0) {
    // 首次调用:处理 HeapDumpPath,构建基础路径
    // ... 省略路径验证和目录检查 ...
    
    // 如果未指定 HeapDumpPath 或为目录,使用默认文件名
    if (use_default_filename) {
      jio_snprintf(&base_path[dlen], sizeof(base_path)-dlen, "%s%d%s",
                   dump_file_name, os::current_process_id(), dump_file_ext);
    }
    
    // JDK 11-21: 使用 os::malloc 从原生堆分配路径内存
    my_path = (char*)os::malloc(len, mtInternal);
    if (my_path == NULL) {
      warning("Cannot create heap dump file.  Out of system memory.");
      return;
    }
    strncpy(my_path, base_path, len);
  } else {
    // 后续转储:追加序列号
    jio_snprintf(my_path, len, "%s.%d", base_path, dump_file_seq);
  }
  dump_file_seq++;

  // 创建 HeapDumper 对象并执行转储(不支持压缩)
  HeapDumper dumper(false, true, oome);
  dumper.dump(my_path);
  os::free(my_path);
}

JDK 17+

void HeapDumper::dump_heap(bool oome) {
  // ... 省略路径处理逻辑(类似 JDK 11) ...
  
  // JDK 17+: 根据 HeapDumpGzipLevel 决定文件扩展名
  const char* dump_file_ext = (HeapDumpGzipLevel > 0) ? ".hprof.gz" : ".hprof";
  
  // ... 省略路径构建 ...
  
  // 创建 HeapDumper 对象,支持压缩
  HeapDumper dumper(false, true, oome);
  dumper.dump(my_path);
  os::free(my_path);
}

JDK 25+

void HeapDumper::dump_heap(bool oome) {
  static char base_path[JVM_MAXPATHLEN] = {'\0'};
  static uint dump_file_seq = 0;
  
  // JDK 25+: 使用栈上分配,避免在 OOM 场景下分配失败
  char my_path[JVM_MAXPATHLEN];
  
  if (dump_file_seq == 0) {
    // JDK 25+: 支持 %p 占位符,使用 Arguments::copy_expand_pid 展开
    if (HeapDumpPath != nullptr && strstr(HeapDumpPath, "%p") != nullptr) {
      Arguments::copy_expand_pid(HeapDumpPath, my_path, JVM_MAXPATHLEN);
    }
    // ... 省略其他路径处理 ...
  }
  
  // JDK 25+: 支持并行转储和压缩
  HeapDumper dumper(false, true, oome);
  dumper.dump(my_path);
}

版本差异总结

  • JDK 11:不支持压缩,使用 os::malloc 分配路径内存
  • JDK 17+:支持 gzip 压缩(HeapDumpGzipLevel),文件扩展名为 .hprof.hprof.gz
  • JDK 21+:使用 nullptr 替代 NULL(C++11 风格)
  • JDK 25+:支持栈上分配路径(避免 OOM 场景分配失败),支持 %p 占位符,支持并行转储和压缩

2.2.3.7. JDK 25+ 并行转储和压缩机制详解
#

JDK 25 引入了并行转储机制,可以充分利用多核 CPU 加速大 Java 对象堆的转储过程。并行转储采用分段写入 + 合并的两阶段设计。

2.2.3.7.1. 并行转储流程图
#
flowchart TD
    A[HeapDumper::dump] --> B[创建 DumpWriter
和 GZipCompressor] B --> C[VM_HeapDumper 构造
传入 num_dump_threads] C --> D[VMThread::execute
触发安全点] D --> E[VM_HeapDumper::doit
在安全点执行] E --> F[可选: GC before dump] F --> G[prepare_parallel_dump
确定实际线程数] G --> H{并行转储?} H -->|否| I[单线程转储
work VMDumperId] H -->|是| J[创建 ParallelObjectIterator
初始化堆切分] J --> K[workers->run_task
启动多个 worker 线程] K --> L1[Worker 0: VM Dumper] K --> L2[Worker 1-N: Parallel Dumpers] L1 --> M1[lock_global_writer
获取全局写入锁] M1 --> N1[写入文件头
HPROF_HEADER] N1 --> O1[写入 UTF8 记录
HPROF_UTF8] O1 --> P1[写入类加载记录
HPROF_LOAD_CLASS] P1 --> Q1[写入堆栈跟踪
HPROF_TRACE] Q1 --> R1[unlock_global_writer
释放全局写入锁] L2 --> M2[wait_for_start_signal
等待 VM Dumper 完成非堆数据] M2 --> N2[创建段文件
base_path.p1, .p2, ...] R1 --> S[并行阶段开始] S --> T1[VM Dumper: 写入类转储
HPROF_GC_CLASS_DUMP] S --> T2[VM Dumper: 写入线程对象
HPROF_GC_ROOT_THREAD_OBJ] S --> T3[VM Dumper: 写入 JNI 全局引用
HPROF_GC_ROOT_JNI_GLOBAL] T1 --> U[并行遍历堆对象] T2 --> U T3 --> U U --> V1[Worker 0: ParallelObjectIterator
遍历堆区域 0] U --> V2[Worker 1: ParallelObjectIterator
遍历堆区域 1] U --> V3[Worker N: ParallelObjectIterator
遍历堆区域 N] V1 --> W1[写入实例转储
HPROF_GC_INSTANCE_DUMP
到段文件 .p0] V2 --> W2[写入实例转储
HPROF_GC_INSTANCE_DUMP
到段文件 .p1] V3 --> W3[写入实例转储
HPROF_GC_INSTANCE_DUMP
到段文件 .pN] W1 --> X1{压缩启用?} W2 --> X2{压缩启用?} W3 --> X3{压缩启用?} X1 -->|是| Y1[GZip 压缩段数据
独立压缩缓冲区] X1 -->|否| Z1[直接写入段文件] X2 -->|是| Y2[GZip 压缩段数据
独立压缩缓冲区] X2 -->|否| Z2[直接写入段文件] X3 -->|是| Y3[GZip 压缩段数据
独立压缩缓冲区] X3 -->|否| Z3[直接写入段文件] Y1 --> AA1[finish_dump_segment
完成段写入] Y2 --> AA2[finish_dump_segment
完成段写入] Y3 --> AA3[finish_dump_segment
完成段写入] Z1 --> AA1 Z2 --> AA2 Z3 --> AA3 AA1 --> BB1[dumper_complete
通知完成] AA2 --> BB2[dumper_complete
通知完成] AA3 --> BB3[dumper_complete
通知完成] BB1 --> CC[VM Dumper 等待
wait_all_dumpers_complete] BB2 --> CC BB3 --> CC CC --> DD[安全点结束
返回调用线程] DD --> EE[DumpMerger::do_merge
合并段文件] EE --> FF[读取段文件 .p0, .p1, ..., .pN] FF --> GG{Linux?} GG -->|是| HH[sendfile 系统调用
零拷贝合并] GG -->|否| II[read + write
常规合并] HH --> JJ[写入 HPROF_HEAP_DUMP_END] II --> JJ JJ --> KK[删除临时段文件] KK --> LL[完成合并
生成最终 .hprof 文件] style L1 fill:#87CEEB style L2 fill:#90EE90 style U fill:#FFB6C1 style EE fill:#FFE4B5 style HH fill:#DDA0DD
2.2.3.7.2. 线程数量确定
#

jdk-jdk-25-36/src/hotspot/share/services/heapDumper.hpp

// 默认线程数:CPU 核心数的 3/8
static uint default_num_of_dump_threads() {
  return MAX2<uint>(1, (uint)os::initial_active_processor_count() * 3 / 8);
}

jdk-jdk-25-36/src/hotspot/share/services/heapDumper.cpp

int HeapDumper::dump(const char* path, outputStream* out, int compression, bool overwrite, uint num_dump_threads) {
  // OOM 场景下的线程数限制:每个线程需要约 20MB 内存
  if (_oome && num_dump_threads > 1) {
    // DumpWriter buffer, DumperClassCacheTable, GZipCompressor buffers
    julong max_threads = os::free_memory() / (20 * M);
    if (num_dump_threads > max_threads) {
      num_dump_threads = MAX2<uint>(1, (uint)max_threads);
    }
  }
  // ... 省略其他代码 ...
}

void VM_HeapDumper::prepare_parallel_dump(WorkerThreads* workers) {
  uint num_active_workers = workers != nullptr ? workers->active_workers() : 0;
  uint num_requested_dump_threads = _num_dumper_threads;
  
  // 检查是否可以并行转储
  if (num_active_workers <= 1 || num_requested_dump_threads <= 1) {
    _num_dumper_threads = 1;  // 单线程转储
  } else {
    // 限制在 2 到 active_workers 之间
    _num_dumper_threads = clamp(num_requested_dump_threads, 2U, num_active_workers);
  }
  
  _dumper_controller = new (std::nothrow) DumperController(_num_dumper_threads);
  // ... 记录日志 ...
}

线程数量确定规则

  1. 默认值CPU 核心数 * 3 / 8(例如 8 核 CPU 默认 3 个线程)
  2. OOM 场景限制:根据可用内存动态调整,每个线程需要约 20MB(DumpWriter buffer、DumperClassCacheTable、GZipCompressor buffers)
  3. 实际线程数clamp(请求线程数, 2, active_workers),至少 2 个线程才启用并行,最多不超过 GC worker 线程数
  4. 单线程回退:如果 active_workers <= 1请求线程数 <= 1,回退到单线程转储
2.2.3.7.3. 堆切分方式
#

并行转储使用 ParallelObjectIterator 来切分堆,不同 GC 实现有不同的切分策略:

G1 GC 切分方式jdk-jdk-25-36/src/hotspot/share/gc/g1/g1CollectedHeap.cpp

class G1ParallelObjectIterator : public ParallelObjectIteratorImpl {
  G1HeapRegionClaimer _claimer;  // 按 region 切分
  
public:
  G1ParallelObjectIterator(uint thread_num) :
      _heap(G1CollectedHeap::heap()),
      _claimer(thread_num == 0 ? G1CollectedHeap::heap()->workers()->active_workers() : thread_num) {}
  
  virtual void object_iterate(ObjectClosure* cl, uint worker_id) {
    // 每个 worker 处理不同的 region
    _heap->object_iterate_parallel(cl, worker_id, &_claimer);
  }
};

切分策略

  • G1 GC:按 Heap Region 切分,每个 worker 线程处理不同的 region
  • ZGC:按页面(Page)切分
  • Shenandoah:按 region 切分,使用任务队列
  • Parallel GC:按堆区域切分

关键点

  • 每个 worker 线程独立遍历分配的堆区域
  • 使用 G1HeapRegionClaimer 等机制确保 region 不重复处理
  • 切分粒度取决于 GC 实现,通常与 GC 的并行策略一致
2.2.3.7.4. 并行转储执行流程
#

jdk-jdk-25-36/src/hotspot/share/services/heapDumper.cpp

void VM_HeapDumper::doit() {
  CollectedHeap* ch = Universe::heap();
  
  // 可选:转储前执行 GC
  if (_gc_before_heap_dump) {
    ch->collect_as_vm_thread(GCCause::_heap_dump);
  }
  
  WorkerThreads* workers = ch->safepoint_workers();
  prepare_parallel_dump(workers);  // 确定实际线程数
  
  if (!is_parallel_dump()) {
    // 单线程转储
    work(VMDumperId);
  } else {
    // 并行转储:创建 ParallelObjectIterator 并启动多个 worker
    ParallelObjectIterator poi(_num_dumper_threads);
    _poi = &poi;
    workers->run_task(this, _num_dumper_threads);  // 启动 worker 线程
    _poi = nullptr;
  }
}

void VM_HeapDumper::work(uint worker_id) {
  int dumper_id = get_next_dumper_id();  // 原子获取 dumper ID
  
  if (is_vm_dumper(dumper_id)) {
    // VM Dumper (worker_id=0):负责非堆数据
    _dumper_controller->lock_global_writer();
    _dumper_controller->signal_start();
    
    // 写入文件头、UTF8、类加载、堆栈跟踪等
    writer()->write_raw("JAVA PROFILE 1.0.2", ...);
    SymbolTable::symbols_do(&sym_dumper);
    ClassLoaderDataGraph::classes_do(&loaded_class_dumper);
    dump_stack_traces(writer());
    
    _dumper_controller->unlock_global_writer();  // 释放锁,允许并行 dumpers 开始
  } else {
    // Parallel Dumpers (worker_id=1-N):等待 VM Dumper 完成非堆数据
    _dumper_controller->wait_for_start_signal();
  }
  
  // 创建段文件:base_path.p0, .p1, .p2, ...
  DumpWriter segment_writer(DumpMerger::get_writer_path(writer()->get_file_path(), dumper_id),
                            writer()->is_overwrite(), writer()->compressor());
  
  if (is_vm_dumper(dumper_id)) {
    // VM Dumper 还负责写入类转储、线程对象、JNI 全局引用等
    ClassDumper class_dumper(&segment_writer);
    ClassLoaderDataGraph::classes_do(&class_dumper);
    dump_threads(&segment_writer);
    // ... 省略其他 GC root ...
  }
  
  // 并行遍历堆对象
  HeapObjectDumper obj_dumper(&segment_writer, this);
  if (!is_parallel_dump()) {
    Universe::heap()->object_iterate(&obj_dumper);
  } else {
    // 并行转储:每个 worker 遍历分配的堆区域
    _poi->object_iterate(&obj_dumper, worker_id);
  }
  
  segment_writer.finish_dump_segment();
  segment_writer.flush();
  
  _dumper_controller->dumper_complete(&segment_writer, writer());
  
  if (is_vm_dumper(dumper_id)) {
    // VM Dumper 等待所有并行 dumpers 完成
    _dumper_controller->wait_all_dumpers_complete();
    writer()->flush();
    // 此时所有段文件已写入,需要在安全点外合并
  }
}

执行流程关键点

  1. VM Dumper(worker_id=0)

    • 获取全局写入锁,写入非堆数据(文件头、UTF8、类信息、堆栈跟踪)
    • 释放锁后,继续写入类转储、线程对象等
    • 最后等待所有并行 dumpers 完成
  2. Parallel Dumpers(worker_id=1-N)

    • 等待 VM Dumper 完成非堆数据写入
    • 创建独立的段文件(.p1, .p2, …, .pN
    • 并行遍历分配的堆区域,写入实例转储记录
    • 每个段文件独立压缩(如果启用)
  3. 同步机制

    • 使用 DumperController 协调多个 dumpers
    • lock_global_writer / unlock_global_writer:保护非堆数据写入
    • wait_for_start_signal / signal_start:确保并行 dumpers 在 VM Dumper 完成后开始
    • wait_all_dumpers_complete:VM Dumper 等待所有并行 dumpers 完成
2.2.3.7.5. 并行压缩机制
#

jdk-jdk-25-36/src/hotspot/share/services/heapDumperCompression.hpp

class GZipCompressor : public AbstractCompressor {
  int _level;           // 压缩级别 0-9
  size_t _block_size;  // 压缩块大小
  bool _is_first;       // 是否为第一个块
  
public:
  virtual char const* compress(char* in, size_t in_size, char* out, size_t out_size,
                               char* tmp, size_t tmp_size, size_t* compressed_size) {
    // 每个段文件独立压缩,使用独立的压缩缓冲区
    if (_is_first) {
      // 第一个块写入块大小注释
      jio_snprintf(buf, sizeof(buf), "HPROF BLOCKSIZE=%zu", _block_size);
      *compressed_size = ZipLibrary::compress(..., buf, ...);
      _is_first = false;
    } else {
      *compressed_size = ZipLibrary::compress(..., nullptr, ...);
    }
  }
};

并行压缩特点

  1. 独立压缩:每个段文件使用独立的 GZipCompressor 实例和压缩缓冲区
  2. 块级压缩:数据按块压缩,每个块独立处理
  3. 无全局同步:各 worker 线程独立压缩,无需同步
  4. 内存开销:每个线程需要独立的压缩缓冲区(约 20MB/线程)
2.2.3.7.6. 段文件合并机制
#

jdk-jdk-25-36/src/hotspot/share/services/heapDumper.cpp

int HeapDumper::dump(const char* path, ...) {
  // Phase 1: 在安全点内并行写入段文件
  VM_HeapDumper dumper(&writer, _gc_before_heap_dump, _oome, num_dump_threads);
  VMThread::execute(&dumper);
  
  // Phase 2: 在安全点外合并段文件(不占用 VM Thread)
  DumpMerger merger(path, &writer, dumper.dump_seq());
  merger.do_merge();
}

void DumpMerger::do_merge() {
  // 合并时不需要再次压缩(段文件已压缩)
  AbstractCompressor* saved_compressor = _writer->compressor();
  _writer->set_compressor(nullptr);
  
  // 按顺序合并所有段文件
  for (int i = 0; i < _dump_seq; i++) {
    const char* path = get_writer_path(_path, i);  // base_path.p0, .p1, ...
    merge_file(path);
    remove(path);  // 删除临时段文件
  }
  
  _writer->set_compressor(saved_compressor);
  // 写入 HPROF_HEAP_DUMP_END 记录
  DumperSupport::end_of_dump(_writer);
}

#ifdef LINUX
void DumpMerger::merge_file(const char* path) {
  // Linux: 使用 sendfile 零拷贝合并(高效)
  int segment_fd = os::open(path, O_RDONLY, 0);
  os::Linux::sendfile(_writer->get_fd(), segment_fd, &offset, st.st_size);
  ::close(segment_fd);
}
#else
void DumpMerger::merge_file(const char* path) {
  // 其他平台: 使用 read + write 合并
  fileStream segment_fs(path, "rb");
  while ((cnt = segment_fs.read(_writer->buffer(), 1, _writer->buffer_size())) != 0) {
    _writer->set_position(cnt);
    _writer->flush();
  }
}
#endif

合并机制特点

  1. 两阶段设计

    • Phase 1:在安全点内并行写入段文件(.p0, .p1, …, .pN
    • Phase 2:在安全点外合并段文件(不占用 VM Thread,不影响 GC)
  2. 合并优化

    • Linux:使用 sendfile 系统调用实现零拷贝合并
    • 其他平台:使用 read + write 常规合并
    • 合并时不需要再次压缩(段文件已压缩)
  3. 文件命名

    • 段文件:base_path.p0, base_path.p1, …, base_path.pN
    • 最终文件:base_path(合并后删除所有段文件)
2.2.3.7.7. 设计与限制
#

设计考虑

  1. 并行遍历:多个线程同时遍历堆的不同区域,充分利用多核 CPU
  2. 并行压缩:每个线程独立压缩,压缩速度随线程数线性提升(理想情况)
  3. 零拷贝合并:Linux 平台使用 sendfile 高效合并段文件

限制

  1. 内存开销:每个线程需要约 20MB 内存(DumpWriter buffer、压缩缓冲区等)
  2. OOM 场景限制:OOM 时可用内存有限,可能无法启用并行转储
  3. 磁盘 IO 瓶颈:即使并行转储,最终仍受磁盘 IO 速度限制
  4. 合并开销:合并阶段需要读取所有段文件并写入最终文件

2.3. HeapDumpBeforeFullGC / HeapDumpAfterFullGC
#

2.3.1. 参数说明
#

说明

  • HeapDumpBeforeFullGC:在 Full GC 之前转储 Java 对象堆
  • HeapDumpAfterFullGC:在 Full GC 之后转储 Java 对象堆

默认false

类型manageable(JDK 11)或 product + MANAGEABLE(JDK 17+),可通过 JMX 动态修改

举例

  • -XX:+HeapDumpBeforeFullGC
  • -XX:+HeapDumpAfterFullGC

2.3.2. 实现机制
#

在 Full GC 前后调用 CollectedHeap::full_gc_dump(),如果相应 flag 启用,则触发 Java 对象堆转储。

JDK 11-24 实现

jdk-jdk-11-28/src/hotspot/share/gc/shared/collectedHeap.cpp

void CollectedHeap::full_gc_dump(GCTimer* timer, bool before) {
  assert(timer != NULL, "timer is null");
  // 检查是否启用相应的 Heap Dump 参数
  if ((HeapDumpBeforeFullGC && before) || (HeapDumpAfterFullGC && !before)) {
    // 记录 GC 日志并触发 Java 对象堆转储
    GCTraceTime(Info, gc) tm(before ? "Heap Dump (before full gc)" : "Heap Dump (after full gc)", timer);
    HeapDumper::dump_heap();
  }
  // ... 省略其他代码 ...
}

JDK 25+ 变化:增加了 FullGCHeapDumpLimit 限制,防止频繁 Full GC 导致过多转储文件:

jdk-jdk-25-36/src/hotspot/share/gc/shared/collectedHeap.cpp

void CollectedHeap::full_gc_dump(GCTimer* timer, bool before) {
  assert(timer != nullptr, "timer is null");
  static uint count = 0;  // 静态计数器,记录已转储次数
  // 检查是否启用相应的 Heap Dump 参数
  if ((HeapDumpBeforeFullGC && before) || (HeapDumpAfterFullGC && !before)) {
    // 检查转储次数限制:0 表示无限制,否则检查是否超过限制
    if (FullGCHeapDumpLimit == 0 || count < FullGCHeapDumpLimit) {
      GCTraceTime(Info, gc) tm(before ? "Heap Dump (before full gc)" : "Heap Dump (after full gc)", timer);
      HeapDumper::dump_heap();
      count++;  // 转储后递增计数器
    }
  }
  // ... 省略其他代码 ...
}

关键点

  • Full GC Java 对象堆转储在 Full GC 的安全点期间执行
  • JDK 25 新增转储次数限制,防止频繁 Full GC 导致过多转储文件

2.4. HeapDumpPath
#

2.4.1. 参数说明
#

说明:指定 Java 对象堆转储文件的路径(文件名或目录)。

默认NULL(JDK 11-17)或 nullptr(JDK 21+)

类型manageable(JDK 11)或 product + MANAGEABLE(JDK 17+),可通过 JMX 动态修改

举例

  • -XX:HeapDumpPath=/path/to/dump.hprof(指定文件)
  • -XX:HeapDumpPath=/path/to/dumps/(指定目录,会自动生成文件名)

2.4.2. 文件命名规则
#

  • 如果未指定或为空,默认文件名为 java_pid<pid>.hprof
  • 如果指定为目录,会在目录下创建默认文件名
  • 如果多次转储,后续文件会追加序列号:java_pid<pid>.hprof.1java_pid<pid>.hprof.2

JDK 25 新增:支持 %p 占位符,会自动替换为进程 ID。

2.4.3. 实现机制
#

2.4.3.1. 参数定义与解析
#

参数定义

jdk-jdk-17-35/src/hotspot/share/runtime/globals.hpp

product(ccstr, HeapDumpPath, NULL, MANAGEABLE,
        "When HeapDumpOnOutOfMemoryError is on, the path (filename or "
        "directory) of the dump file (defaults to java_pid<pid>.hprof "
        "in the current working directory)")

参数类型说明

  • ccstr:C 字符串类型,存储指向字符串的指针
  • MANAGEABLE:可通过 JMX 动态修改
  • 默认值NULL(JDK 11-17)或 nullptr(JDK 21+)

参数解析

  • JVM 启动时通过 -XX:HeapDumpPath=<path> 指定
  • 运行时可通过 JMX HotSpotDiagnosticMXBean.setVMOption() 动态修改
  • 参数值存储在全局变量 HeapDumpPath

2.4.3.2. 路径处理逻辑
#

JDK 11-21 实现

jdk-jdk-17-35/src/hotspot/share/services/heapDumper.cpp

void HeapDumper::dump_heap(bool oome) {
  static char base_path[JVM_MAXPATHLEN] = {'\0'};
  static uint dump_file_seq = 0;
  char* my_path;
  
  const char* dump_file_name = "java_pid";
  const char* dump_file_ext  = HeapDumpGzipLevel > 0 ? ".hprof.gz" : ".hprof";
  
  if (dump_file_seq == 0) {
    // 首次调用:计算最长路径长度并验证
    const size_t total_length =
        (HeapDumpPath == NULL ? 0 : strlen(HeapDumpPath)) +
        strlen(os::file_separator()) + max_digit_chars +
        strlen(dump_file_name) + strlen(dump_file_ext) + 1;
    if (total_length > sizeof(base_path)) {
      warning("Cannot create heap dump file.  HeapDumpPath is too long.");
      return;
    }
    
    bool use_default_filename = true;
    if (HeapDumpPath == NULL || HeapDumpPath[0] == '\0') {
      // 未指定 HeapDumpPath,使用默认文件名
    } else {
      strcpy(base_path, HeapDumpPath);
      // 检查路径是否为已存在的目录
      DIR* dir = os::opendir(base_path);
      if (dir == NULL) {
        // 不是目录,视为文件名
        use_default_filename = false;
      } else {
        // 是目录,追加文件分隔符(如果需要)
        os::closedir(dir);
        size_t fs_len = strlen(os::file_separator());
        if (strlen(base_path) >= fs_len) {
          char* end = base_path;
          end += (strlen(base_path) - fs_len);
          if (strcmp(end, os::file_separator()) != 0) {
            strcat(base_path, os::file_separator());
          }
        }
      }
    }
    
    // 如果 HeapDumpPath 不是文件名,则追加默认文件名
    if (use_default_filename) {
      const size_t dlen = strlen(base_path);
      jio_snprintf(&base_path[dlen], sizeof(base_path)-dlen, "%s%d%s",
                   dump_file_name, os::current_process_id(), dump_file_ext);
    }
    
    // 使用 os::malloc 从原生堆分配路径内存
    const size_t len = strlen(base_path) + 1;
    my_path = (char*)os::malloc(len, mtInternal);
    if (my_path == NULL) {
      warning("Cannot create heap dump file.  Out of system memory.");
      return;
    }
    strncpy(my_path, base_path, len);
  } else {
    // 后续转储:追加序列号
    const size_t len = strlen(base_path) + max_digit_chars + 2; // for '.' and \0
    my_path = (char*)os::malloc(len, mtInternal);
    if (my_path == NULL) {
      warning("Cannot create heap dump file.  Out of system memory.");
      return;
    }
    jio_snprintf(my_path, len, "%s.%d", base_path, dump_file_seq);
  }
  dump_file_seq++;
  
  // ... 执行转储 ...
  os::free(my_path);
}

关键处理逻辑

  1. 路径长度验证:首次调用时计算最长可能路径长度,超过 JVM_MAXPATHLEN 则警告并返回
  2. 目录检测:使用 os::opendir() 检查路径是否为已存在的目录
  3. 文件分隔符处理:如果是目录,自动追加文件分隔符(如果需要)
  4. 默认文件名生成:未指定或为目录时,生成 java_pid<pid>.hprof 格式的文件名
  5. 序列号追加:多次转储时,在基础路径后追加 .1.2 等序列号
  6. 内存分配:使用 os::malloc() 从原生堆分配路径内存,转储完成后释放

JDK 25+ 变化:支持 %p 占位符和栈上分配路径这个变量:

jdk-jdk-25-36/src/hotspot/share/services/heapDumper.cpp

void HeapDumper::dump_heap(bool oome) {
  static char base_path[JVM_MAXPATHLEN] = {'\0'};
  static uint dump_file_seq = 0;
  char my_path[JVM_MAXPATHLEN];  // JDK 25+: 使用栈上分配,避免 OOM 场景分配失败
  const int max_digit_chars = 20;
  const char* dump_file_name = HeapDumpGzipLevel > 0 ? "java_pid%p.hprof.gz" : "java_pid%p.hprof";
  
  if (dump_file_seq == 0) {
    // 设置基础路径(文件名或目录,默认或自定义,不含序列号),执行 %p 替换
    const char *path_src = (HeapDumpPath != nullptr && HeapDumpPath[0] != '\0') ? HeapDumpPath : dump_file_name;
    // 使用 Arguments::copy_expand_pid 展开 %p 占位符
    if (!Arguments::copy_expand_pid(path_src, strlen(path_src), base_path, JVM_MAXPATHLEN - max_digit_chars)) {
      warning("Cannot create heap dump file.  HeapDumpPath is too long.");
      return;
    }
    
    // 检查路径是否为已存在的目录
    DIR* dir = os::opendir(base_path);
    if (dir != nullptr) {
      os::closedir(dir);
      // 路径是目录,追加文件分隔符(如果需要)
      size_t fs_len = strlen(os::file_separator());
      if (strlen(base_path) >= fs_len) {
        char* end = base_path;
        end += (strlen(base_path) - fs_len);
        if (strcmp(end, os::file_separator()) != 0) {
          strcat(base_path, os::file_separator());
        }
      }
      // 然后添加默认文件名,执行 %p 替换。使用 my_path 临时存储
      if (!Arguments::copy_expand_pid(dump_file_name, strlen(dump_file_name), my_path, JVM_MAXPATHLEN - max_digit_chars)) {
        warning("Cannot create heap dump file.  HeapDumpPath is too long.");
        return;
      }
      const size_t dlen = strlen(base_path);
      jio_snprintf(&base_path[dlen], sizeof(base_path) - dlen, "%s", my_path);
    }
    strncpy(my_path, base_path, JVM_MAXPATHLEN);
  } else {
    // 后续转储:追加序列号
    const size_t len = strlen(base_path) + max_digit_chars + 2; // for '.' and \0
    jio_snprintf(my_path, len, "%s.%d", base_path, dump_file_seq);
  }
  dump_file_seq++;
  
  // ... 执行转储 ...
}

JDK 25+ 关键变化

  1. 栈上分配:使用栈上数组 char my_path[JVM_MAXPATHLEN] 替代 os::malloc(),避免 OOM 场景下内存分配失败
  2. %p 占位符支持:使用 Arguments::copy_expand_pid() 展开 %p 占位符为进程 ID
  3. 默认文件名包含 %p:默认文件名格式改为 java_pid%p.hprof,在展开时替换为实际进程 ID

2.4.3.3. %p 占位符展开机制
#

JDK 25+ 引入Arguments::copy_expand_pid() 函数用于展开路径中的 %p 占位符:

jdk-jdk-17-35/src/hotspot/share/runtime/arguments.cpp

bool Arguments::copy_expand_pid(const char* src, size_t srclen,
                                char* buf, size_t buflen) {
  const char* p = src;
  char* b = buf;
  const char* src_end = &src[srclen];
  char* buf_end = &buf[buflen - 1];
  
  while (p < src_end && b < buf_end) {
    if (*p == '%') {
      switch (*(++p)) {
      case '%':         // "%%" ==> "%"(转义)
        *b++ = *p++;
        break;
      case 'p':  {       // "%p" ==> 当前进程 ID
        // 计算可用缓冲区大小
        size_t buf_sz = buf_end - b + 1;
        // 使用 jio_snprintf 格式化进程 ID
        int ret = jio_snprintf(b, buf_sz, "%d", os::current_process_id());
        
        // 如果格式化失败或缓冲区不足,返回 false
        if (ret < 0 || ret >= (int)buf_sz) {
          return false;
        } else {
          b += ret;  // 移动缓冲区指针
          assert(*b == '\0', "fail in copy_expand_pid");
          if (p == src_end && b == buf_end + 1) {
            // 到达缓冲区末尾
            return true;
          }
        }
        p++;  // 跳过 'p'
        break;
      }
      default:
        // 未知占位符,原样复制
        *b++ = '%';
        *b++ = *p++;
        break;
      }
    } else {
      // 普通字符,直接复制
      *b++ = *p++;
    }
  }
  
  *b = '\0';  // 确保字符串以 null 结尾
  return true;
}

占位符展开规则

  1. %p:替换为当前进程 ID(通过 os::current_process_id() 获取)
  2. %%:转义为单个 % 字符
  3. 其他 %X:未知占位符原样保留(% + X
  4. 缓冲区检查:确保展开后的路径不超过缓冲区大小,否则返回 false

使用示例

  • -XX:HeapDumpPath=./dump_%p.hprof./dump_12345.hprof(假设进程 ID 为 12345)
  • -XX:HeapDumpPath=/var/log/java_pid%p.hprof/var/log/java_pid12345.hprof
  • -XX:HeapDumpPath=/tmp/dumps//tmp/dumps/java_pid12345.hprof(目录 + 默认文件名)

关键点

  • 路径长度限制:展开后的路径长度不能超过 JVM_MAXPATHLEN - max_digit_chars(为序列号预留空间)
  • 目录检测时机%p 展开在目录检测之前执行,因此可以在目录路径中使用 %p
  • 多次转储:首次转储时展开 %p,后续转储使用已展开的基础路径追加序列号

2.5. HeapDumpGzipLevel
#

2.5.1. 参数说明
#

说明:指定 Java 对象堆转储文件的 gzip 压缩级别(0-9)。

默认0(禁用压缩)

类型product + MANAGEABLE(JDK 17+),可通过 JMX 动态修改

范围:0-9

  • 0:禁用压缩
  • 1-9:压缩级别,数字越大压缩比越高,但压缩时间越长

举例-XX:HeapDumpGzipLevel=5

2.5.2. 压缩性能参考
#

假设 4 个 CPU,针对 8GB Java 对象堆的压缩时间估算:

  • 级别 5(性价比常用):
    • 压缩比:2.7×–3.1×
    • 压缩时间:4.4–11.2 秒
    • 压缩后文件:2.6–3.2 GB
    • 写入 HDD:13–32 秒
    • 总耗时:17.4–43.2 秒

注意:这是理想情况下的估计,实际情况可能更差。

2.6. FullGCHeapDumpLimit
#

2.6.1. 参数说明
#

说明:限制 Full GC 触发的 Java 对象堆转储次数。

默认0(无限制)

类型product + MANAGEABLE(JDK 25+),可通过 JMX 动态修改

举例-XX:FullGCHeapDumpLimit=5(最多转储 5 次)

2.6.2. 使用场景
#

防止频繁 Full GC 导致过多 Java 对象堆转储文件,特别是在调试阶段。

3. OutOfMemoryError 处理相关参数
#

3.1. OnOutOfMemoryError
#

3.1.1. 参数说明
#

说明:在第一个 java.lang.OutOfMemoryError 发生时,执行用户定义的命令或脚本。

默认:空字符串(不执行)

类型product,不可通过 JMX 修改

举例

-XX:OnOutOfMemoryError="kill -9 %p"
-XX:OnOutOfMemoryError="/path/to/script.sh"

3.1.2. 实现机制
#

3.1.2.1. 参数定义与解析
#

参数定义

jdk-jdk-17-35/src/hotspot/share/runtime/globals.hpp

product(ccstrlist, OnOutOfMemoryError, "",
        "Run user-defined commands on first java.lang.OutOfMemoryError "
        "(see VMError::report_java_out_of_memory)")

参数类型说明

  • ccstrlist:C 字符串列表类型,支持分号分隔的多个命令
  • 默认值:空字符串(不执行任何命令)
  • 不可通过 JMX 修改product 类型,只能在启动时指定

参数解析

  • JVM 启动时通过 -XX:OnOutOfMemoryError="<command>" 指定
  • 支持多个命令,使用分号(;)分隔
  • 支持 %p 占位符,会自动替换为进程 ID
  • 参数值存储在全局变量 OnOutOfMemoryError

使用示例

# 单个命令
-XX:OnOutOfMemoryError="kill -9 %p"

# 多个命令(分号分隔)
-XX:OnOutOfMemoryError="echo OOM occurred; kill -9 %p"

# 执行脚本
-XX:OnOutOfMemoryError="/path/to/script.sh %p"

3.1.2.2. 命令解析机制
#

命令解析函数

jdk-jdk-17-35/src/hotspot/share/utilities/vmError.cpp

static char* next_OnError_command(char* buf, int buflen, const char** ptr) {
  if (ptr == NULL || *ptr == NULL) return NULL;
  
  const char* cmd = *ptr;
  
  // 跳过前导空格或分号
  while (*cmd == ' ' || *cmd == ';') cmd++;
  
  // 如果到达字符串末尾,返回 NULL
  if (*cmd == '\0') return NULL;
  
  // 查找命令结束位置(分号或字符串末尾)
  const char * cmdend = cmd;
  while (*cmdend != '\0' && *cmdend != ';') cmdend++;
  
  // 展开 %p 占位符并复制到缓冲区
  Arguments::copy_expand_pid(cmd, cmdend - cmd, buf, buflen);
  
  // 更新指针位置(跳过当前命令和分号)
  *ptr = (*cmdend == '\0' ? cmdend : cmdend + 1);
  return buf;
}

解析规则

  1. 分号分隔:使用分号(;)分隔多个命令
  2. 空格处理:自动跳过前导空格和分号
  3. %p 占位符展开:使用 Arguments::copy_expand_pid()%p 替换为进程 ID
  4. 顺序解析:按顺序解析每个命令,直到字符串末尾

解析示例

  • "cmd1; cmd2; cmd3" → 解析为 3 个命令:cmd1cmd2cmd3
  • "kill -9 %p" → 展开为 kill -9 12345(假设进程 ID 为 12345)
  • " cmd1 ; cmd2 " → 自动去除空格,解析为 cmd1cmd2

3.1.2.3. 命令执行机制
#

触发入口

jdk-jdk-17-35/src/hotspot/share/utilities/debug.cpp

void report_java_out_of_memory(const char* message) {
  static int out_of_memory_reported = 0;
  
  // 使用原子操作确保只执行一次(多个线程可能同时触发)
  if (Atomic::cmpxchg(&out_of_memory_reported, 0, 1) == 0) {
    // 1. 先执行 Java 对象堆转储(如果启用)
    if (HeapDumpOnOutOfMemoryError) {
      tty->print_cr("java.lang.OutOfMemoryError: %s", message);
      HeapDumper::dump_heap_from_oome();
    }
    
    // 2. 执行 OnOutOfMemoryError 命令(如果启用)
    if (OnOutOfMemoryError && OnOutOfMemoryError[0]) {
      VMError::report_java_out_of_memory(message);
    }
    
    // 3. 触发崩溃(如果启用)
    if (CrashOnOutOfMemoryError) {
      // ... 省略 ...
    }
    
    // 4. 退出 JVM(如果启用)
    if (ExitOnOutOfMemoryError) {
      // ... 省略 ...
    }
  }
}

VM_ReportJavaOutOfMemory 实现

jdk-jdk-17-35/src/hotspot/share/utilities/vmError.cpp

class VM_ReportJavaOutOfMemory : public VM_Operation {
 private:
  const char* _message;
 public:
  VM_ReportJavaOutOfMemory(const char* message) { _message = message; }
  VMOp_Type type() const { return VMOp_ReportJavaOutOfMemory; }
  void doit();
};

void VM_ReportJavaOutOfMemory::doit() {
  // 不在栈上分配大缓冲区
  static char buffer[O_BUFLEN];
  
  // 打印错误信息和命令信息
  tty->print_cr("#");
  tty->print_cr("# java.lang.OutOfMemoryError: %s", _message);
  tty->print_cr("# -XX:OnOutOfMemoryError=\"%s\"", OnOutOfMemoryError);
  
  // 确保 Java 对象堆可解析(不需要 retire TLABs)
  Universe::heap()->ensure_parsability(false);
  
  // 解析并执行每个命令
  char* cmd;
  const char* ptr = OnOutOfMemoryError;
  while ((cmd = next_OnError_command(buffer, sizeof(buffer), &ptr)) != NULL) {
    tty->print("#   Executing ");
#if defined(LINUX)
    tty->print("/bin/sh -c ");  // Linux 使用 /bin/sh -c 执行
#endif
    tty->print_cr("\"%s\"...", cmd);
    
    // 执行命令(fork 子进程执行,不等待完成)
    if (os::fork_and_exec(cmd, true) < 0) {
      // 执行失败时打印错误信息,但继续执行后续命令
      tty->print_cr("os::fork_and_exec failed: %s (%s=%d)",
                     os::strerror(errno), os::errno_name(errno), errno);
    }
  }
}

void VMError::report_java_out_of_memory(const char* message) {
  if (OnOutOfMemoryError && OnOutOfMemoryError[0]) {
    // 获取 Heap_lock 锁,确保线程安全
    MutexLocker ml(Heap_lock);
    // 创建 VM_Operation 并通过 VMThread 在安全点执行
    VM_ReportJavaOutOfMemory op(message);
    VMThread::execute(&op);
  }
}

执行机制关键点

  1. 安全点执行:通过 VMThread::execute() 在安全点执行,确保 Java 对象堆状态一致
  2. 堆可解析性:执行前调用 Universe::heap()->ensure_parsability(false),确保堆可被工具(如 jmap)解析
  3. 异步执行:使用 os::fork_and_exec() 在子进程中执行命令,不阻塞主线程
  4. 顺序执行:多个命令按顺序执行,但每个命令都是异步的(不等待完成)
  5. Linux 平台:使用 /bin/sh -c 执行命令,支持 shell 语法

3.1.2.4. 错误处理机制
#

错误处理策略

if (os::fork_and_exec(cmd, true) < 0) {
  // 执行失败时打印错误信息,但继续执行后续命令
  tty->print_cr("os::fork_and_exec failed: %s (%s=%d)",
                 os::strerror(errno), os::errno_name(errno), errno);
}

错误处理特点

  1. 非阻塞:命令执行失败不会阻止后续命令的执行
  2. 错误日志:执行失败时打印错误信息到控制台(tty),包括:
    • 错误描述(os::strerror(errno)
    • 错误名称(os::errno_name(errno)
    • 错误码(errno
  3. 继续执行:即使某个命令失败,仍会继续执行后续命令
  4. 返回值忽略os::fork_and_exec() 的返回值被忽略,命令在子进程中异步执行

常见错误场景

  • 命令不存在ENOENT(No such file or directory)
  • 权限不足EACCES(Permission denied)
  • 路径过长ENAMETOOLONG(File name too long)
  • 资源不足ENOMEM(Out of memory)或 EAGAIN(Resource temporarily unavailable)

3.1.2.5. 执行顺序与时机
#

在 OOM 处理流程中的位置

jdk-jdk-17-35/src/hotspot/share/utilities/debug.cpp

void report_java_out_of_memory(const char* message) {
  static int out_of_memory_reported = 0;
  
  // 原子操作确保只执行一次
  if (Atomic::cmpxchg(&out_of_memory_reported, 0, 1) == 0) {
    // 1. 最先执行:Java 对象堆转储(如果启用)
    if (HeapDumpOnOutOfMemoryError) {
      tty->print_cr("java.lang.OutOfMemoryError: %s", message);
      HeapDumper::dump_heap_from_oome();
    }
    
    // 2. 然后执行:OnOutOfMemoryError 命令(如果启用)
    if (OnOutOfMemoryError && OnOutOfMemoryError[0]) {
      VMError::report_java_out_of_memory(message);
    }
    
    // 3. 接着执行:触发崩溃(如果启用)
    if (CrashOnOutOfMemoryError) {
      tty->print_cr("Aborting due to java.lang.OutOfMemoryError: %s", message);
      report_fatal(OOM_JAVA_HEAP_FATAL, __FILE__, __LINE__, "OutOfMemory encountered: %s", message);
    }
    
    // 4. 最后执行:退出 JVM(如果启用)
    if (ExitOnOutOfMemoryError) {
      tty->print_cr("Terminating due to java.lang.OutOfMemoryError: %s", message);
      os::exit(3);  // JDK 17
      // os::_exit(3);  // JDK 21+
    }
  }
}

执行时机

  • 安全点执行OnOutOfMemoryError 命令在安全点执行,确保堆状态一致
  • 只执行一次:使用原子操作 Atomic::cmpxchg 确保多个线程同时触发时只执行一次
  • 异步执行:命令在子进程中异步执行,不阻塞主线程,但主线程会继续执行后续操作(如崩溃或退出)

3.2. CrashOnOutOfMemoryError
#

3.2.1. 参数说明
#

说明:在第一个 java.lang.OutOfMemoryError 发生时,触发 JVM 崩溃,生成错误日志和核心转储。

默认false

类型product,不可通过 JMX 修改

举例-XX:+CrashOnOutOfMemoryError

3.2.2. 实现机制
#

3.2.2.1. 触发入口
#

JDK 11 实现

jdk-jdk-11-28/src/hotspot/share/utilities/debug.cpp

void report_java_out_of_memory(const char* message) {
  // ... 省略其他代码 ...
  
  if (CrashOnOutOfMemoryError) {
    tty->print_cr("Aborting due to java.lang.OutOfMemoryError: %s", message);
    fatal("OutOfMemory encountered: %s", message);
  }
}

JDK 17+ 实现

jdk-jdk-17-35/src/hotspot/share/utilities/debug.cpp

void report_java_out_of_memory(const char* message) {
  // ... 省略其他代码 ...
  
  if (CrashOnOutOfMemoryError) {
    tty->print_cr("Aborting due to java.lang.OutOfMemoryError: %s", message);
    // JDK 17+: 使用 report_fatal 替代 fatal,提供更详细的错误信息
    report_fatal(OOM_JAVA_HEAP_FATAL, __FILE__, __LINE__, "OutOfMemory encountered: %s", message);
  }
}

关键变化

  • JDK 11:使用 fatal() 函数,直接触发崩溃
  • JDK 17+:使用 report_fatal() 函数,提供错误类型、文件名、行号等详细信息

实现机制CrashOnOutOfMemoryError 触发崩溃后,会执行完整的错误报告流程,包括生成错误日志和核心转储。详细实现机制请参考第四章"错误日志和诊断相关参数"中的相关章节。

3.3. ExitOnOutOfMemoryError
#

3.3.1. 参数说明
#

说明:在第一个 java.lang.OutOfMemoryError 发生时,退出 JVM。

默认false

类型product,不可通过 JMX 修改

举例-XX:+ExitOnOutOfMemoryError

3.3.2. 实现机制
#

3.3.2.1. 触发入口
#

JDK 11-20 实现

jdk-jdk-11-28/src/hotspot/share/utilities/debug.cpp

void report_java_out_of_memory(const char* message) {
  // ... 省略其他代码 ...
  
  if (ExitOnOutOfMemoryError) {
    tty->print_cr("Terminating due to java.lang.OutOfMemoryError: %s", message);
    os::exit(3);  // 调用 os::exit,会执行清理钩子
  }
}

JDK 21+ 实现

jdk-jdk-21-35/src/hotspot/share/utilities/debug.cpp

void report_java_out_of_memory(const char* message) {
  // ... 省略其他代码 ...
  
  if (ExitOnOutOfMemoryError) {
    tty->print_cr("Terminating due to java.lang.OutOfMemoryError: %s", message);
    os::_exit(3);  // JDK 21+: 快速退出,不运行清理钩子
  }
}

关键变化

  • JDK 11-20:使用 os::exit(3),会执行清理钩子(shutdown hooks)
  • JDK 21+:使用 os::_exit(3),直接退出,不执行清理钩子

3.3.2.2. Shutdown Hooks 机制
#

Shutdown Hooks 定义

Shutdown Hooks(清理钩子)是 JVM 在正常关闭时执行的清理任务。通过 Runtime.addShutdownHook() 注册的线程会在 JVM 退出前执行。

Shutdown Hooks 注册

jdk-jdk-17-35/src/java.base/share/classes/java/lang/Runtime.java

public void addShutdownHook(Thread hook) {
    SecurityManager sm = System.getSecurityManager();
    if (sm != null) {
        sm.checkPermission(new RuntimePermission("shutdownHooks"));
    }
    ApplicationShutdownHooks.add(hook);
}

Shutdown Hooks 执行顺序

jdk-jdk-17-35/src/java.base/share/classes/java/lang/Shutdown.java

class Shutdown {
    // 系统 shutdown hooks 按预定义槽位注册,执行顺序如下:
    // (0) Console restore hook - 恢复控制台状态
    // (1) ApplicationShutdownHooks - 执行所有应用注册的 shutdown hooks
    // (2) DeleteOnExit hook - 删除标记为退出时删除的文件
    private static final int MAX_SYSTEM_HOOKS = 10;
    private static final Runnable[] hooks = new Runnable[MAX_SYSTEM_HOOKS];
    
    private static void runHooks() {
        // 按顺序执行所有系统 shutdown hooks
        for (int i=0; i < MAX_SYSTEM_HOOKS; i++) {
            Runnable hook = hooks[i];
            if (hook != null) hook.run();
        }
        // 设置 shutdown 状态
        VM.shutdown();
    }
    
    static void exit(int status) {
        synchronized (Shutdown.class) {
            beforeHalt();  // 通知 VM 准备退出
            runHooks();    // 执行所有 shutdown hooks
            halt(status);  // 调用 native halt 终止进程
        }
    }
}

ApplicationShutdownHooks 执行

jdk-jdk-17-35/src/java.base/share/classes/java/lang/ApplicationShutdownHooks.java

class ApplicationShutdownHooks {
    private static IdentityHashMap<Thread, Thread> hooks;
    
    static void runHooks() {
        Collection<Thread> threads;
        synchronized(ApplicationShutdownHooks.class) {
            threads = hooks.keySet();
            hooks = null;  // 清空 hooks,防止重复执行
        }
        
        // 启动所有应用 shutdown hooks(并发执行)
        for (Thread hook : threads) {
            hook.start();
        }
        
        // 等待所有 hooks 完成
        for (Thread hook : threads) {
            while (true) {
                try {
                    hook.join();
                    break;
                } catch (InterruptedException ignored) {
                }
            }
        }
    }
}

Shutdown Hooks 常见用途

  1. 资源清理:关闭文件、网络连接、数据库连接等
  2. 临时文件删除:删除程序运行期间创建的临时文件
  3. 日志刷新:确保日志缓冲区内容写入磁盘
  4. 服务注销:从注册中心注销服务
  5. 状态保存:保存应用状态到持久化存储

3.3.2.3. os::exit 与 os::_exit 的区别
#

os::exit 实现

jdk-jdk-17-35/src/hotspot/os/posix/os_posix.cpp

void os::exit(int num) {
  ::exit(num);  // 调用标准 C 库的 exit(),会执行 atexit 注册的函数
}

os::exit 调用链

jdk-jdk-17-35/src/hotspot/share/runtime/java.cpp

void vm_direct_exit(int code) {
  notify_vm_shutdown();  // 通知 VM 准备关闭
  os::wait_for_keypress_at_exit();
  os::exit(code);  // 调用 os::exit
}

// Runtime.exit() 最终调用
void Java_java_lang_Shutdown_beforeHalt() {
  // 调用 Shutdown.beforeHalt(),触发 shutdown hooks 执行
  Shutdown::exit(status);
}

os::_exit 实现(JDK 21+)

jdk-jdk-21-35/src/hotspot/os/posix/os_posix.cpp

void os::_exit(int num) {
  ::_exit(num);  // 调用系统调用 _exit(),直接终止进程,不执行任何清理
}

关键区别

特性os::exit(3)os::_exit(3)
标准 C 库函数exit()_exit()
执行 atexit 函数✅ 是❌ 否
执行 shutdown hooks✅ 是❌ 否
刷新标准流缓冲区✅ 是❌ 否
关闭文件描述符✅ 是❌ 否(由操作系统关闭)
执行顺序1. atexit 函数
2. shutdown hooks
3. 刷新缓冲区
4. 关闭文件
5. 退出进程
直接退出进程
退出速度较慢(需要执行清理)快速(立即退出)
适用场景正常退出,需要清理资源异常退出(如 OOM),避免清理时再次分配内存

3.3.2.4. 清理操作详解
#

os::exit(3) 执行的清理操作

  1. Java 层清理

    • Shutdown Hooks:执行所有注册的 shutdown hooks(包括应用 hooks 和系统 hooks)
    • DeleteOnExit Hook:删除通过 File.deleteOnExit() 标记的文件
    • Console Restore Hook:恢复控制台状态(如果使用了 Console)
  2. JVM 层清理

    • VM Shutdown 通知:调用 notify_vm_shutdown() 通知各个子系统
    • PerfMemory 清理:清理性能内存资源
    • Attach Listener 清理:清理 Attach API 相关资源
    • 输出流刷新:刷新所有输出流缓冲区
  3. 系统层清理

    • atexit 函数:执行通过 atexit() 注册的函数
    • 标准流刷新:刷新 stdoutstderr 缓冲区
    • 文件描述符关闭:关闭所有打开的文件描述符

os::_exit(3) 不执行的清理操作

  1. 不执行 Shutdown Hooks

    • 应用注册的 shutdown hooks 不会执行
    • 系统 shutdown hooks(如 DeleteOnExit)不会执行
    • 可能导致资源泄漏(如文件未关闭、连接未释放)
  2. 不执行 atexit 函数

    • 通过 atexit() 注册的函数不会执行
    • 可能导致某些全局资源未清理
  3. 不刷新缓冲区

    • 标准输出/错误流的缓冲区不会刷新
    • 可能导致部分日志丢失
  4. 不关闭文件描述符

    • 文件描述符由操作系统自动关闭
    • 但不会触发文件关闭时的清理逻辑

os::shutdown() 执行的清理

jdk-jdk-17-35/src/hotspot/os/posix/os_posix.cpp

void os::shutdown() {
  // 允许 PerfMemory 尝试清理持久化资源
  perfMemory_exit();
  
  // 移除文件系统中的对象
  AttachListener::abort();
  
  // 刷新缓冲输出,完成日志文件
  ostream_abort();
  
  // 检查是否有 abort hook
  abort_hook_t abort_hook = Arguments::abort_hook();
  if (abort_hook != NULL) {
    abort_hook();
  }
}

注意os::shutdown()os::abort() 中也会被调用,但在 os::_exit()不会被调用。

3.3.2.5. OOM 场景下的问题
#

os::exit(3) 在 OOM 场景下的风险

  1. Shutdown Hooks 可能触发新的内存分配

    • Shutdown hooks 是 Java 线程,执行时可能需要分配内存
    • 在 Java 对象堆 OOM 的情况下,执行 shutdown hooks 可能再次触发 OOM
    • 导致退出过程卡住或失败
  2. 清理操作可能失败

    • 某些清理操作(如文件写入、网络请求)可能需要分配内存
    • 在内存不足的情况下,这些操作可能失败或阻塞
  3. 退出延迟

    • Shutdown hooks 的执行时间不确定
    • 如果某个 hook 阻塞或执行时间过长,会延迟进程退出

os::_exit(3) 的优势

  1. 快速退出:直接调用系统调用 _exit(),立即终止进程,不执行任何清理
  2. 避免二次 OOM:不执行 shutdown hooks,避免在 OOM 场景下再次分配内存
  3. 确定性强:退出时间确定,不会因为清理操作阻塞

JDK 21+ 改进原因

在 Java 对象堆 OOM 的场景下,使用 os::_exit(3) 可以:

  • 避免 shutdown hooks 执行时再次触发 OOM
  • 快速退出,让容器编排系统(如 Kubernetes)能够快速重启服务
  • 减少退出过程中的不确定性

注意事项

使用 os::_exit(3) 的代价是:

  • 不会执行任何清理操作,可能导致资源泄漏
  • 不会刷新缓冲区,可能导致日志丢失
  • 不会删除临时文件(除非通过 File.deleteOnExit() 标记,但该 hook 也不会执行)

因此,os::_exit(3) 仅适用于异常退出场景(如 OOM),不适合正常退出。

3.4. 执行顺序和优先级
#

3.4.1. 执行流程图
#

flowchart TD
    A[发生 OutOfMemoryError
包括 Java 对象堆 OOM] --> B[report_java_out_of_memory] B --> C{Atomic::cmpxchg
检查是否已报告} C -->|已报告| Z[跳过] C -->|首次报告| D{HeapDumpOnOutOfMemoryError?} D -->|是| E[执行 Java 对象堆 Heap Dump] D -->|否| F{OnOutOfMemoryError?} E --> F F -->|是| G[执行用户脚本
在安全点执行] F -->|否| H{CrashOnOutOfMemoryError?} G --> H H -->|是| I[触发 fatal
生成错误日志和核心转储] H -->|否| J{ExitOnOutOfMemoryError?} I --> K[退出] J -->|是| L[os::_exit 3
快速退出] J -->|否| M[抛出 OutOfMemoryError] L --> K M --> N[Java 代码捕获异常] style E fill:#87CEEB style G fill:#90EE90 style I fill:#FFB6C1 style L fill:#FFB6C1

3.4.2. 执行顺序总结
#

  1. HeapDumpOnOutOfMemoryError:最先执行(如果启用)
  2. OnOutOfMemoryError:在 Java 对象堆转储后执行(如果启用)
  3. CrashOnOutOfMemoryError:在脚本后执行(如果启用)
  4. ExitOnOutOfMemoryError:最后执行(如果启用)

注意:这些参数可以同时启用,会按顺序执行。

4. 错误日志和诊断相关参数
#

4.1. ErrorFile
#

4.1.1. 参数说明
#

说明:指定错误日志文件的路径。当发生致命错误时,JVM 会将错误信息写入此文件。

默认NULL(使用默认文件名 hs_err_pid%p.log

类型product,不可通过 JMX 修改

举例-XX:ErrorFile=/var/log/jvm/hs_err_pid%p.log

占位符:支持 %p 占位符,会自动替换为进程 ID。

4.1.2. 错误日志内容
#

错误日志文件(hs_err_pid*.log)包含以下信息:

  • JVM 版本和配置信息
  • 错误类型和原因
  • 线程堆栈跟踪
  • Java 对象堆内存信息
  • 本地变量信息
  • 系统信息

4.1.3. 实现机制
#

4.1.3.1. 错误日志生成流程
#

当发生致命错误(如 CrashOnOutOfMemoryError 触发)时,JVM 会调用 report_fatal() 函数,最终通过 VMError::report_and_die() 生成错误日志。

report_fatal 实现

jdk-jdk-17-35/src/hotspot/share/utilities/debug.cpp

void report_fatal(VMErrorType error_type, const char* file, int line, const char* detail_fmt, ...) {
  if (Debugging || error_is_suppressed(file, line)) return;
  va_list detail_args;
  va_start(detail_args, detail_fmt);
  void* context = NULL;
  
  // 打印单元测试错误信息
  print_error_for_unit_test("fatal error", detail_fmt, detail_args);
  
  // 调用 VMError::report_and_die 执行实际的错误报告和崩溃
  VMError::report_and_die(error_type, "fatal error", detail_fmt, detail_args,
                          Thread::current_or_null(), NULL, NULL, context,
                          file, line, 0);
  va_end(detail_args);
}

函数作用

  1. 参数验证:检查是否处于调试模式或错误被抑制
  2. 格式化错误信息:使用 va_list 格式化详细错误信息
  3. 调用报告函数:调用 VMError::report_and_die() 执行实际的错误报告和崩溃流程

VMError::report_and_die 主流程

jdk-jdk-17-35/src/hotspot/share/utilities/vmError.cpp

void VMError::report_and_die(int id, const char* message, const char* detail_fmt, va_list detail_args,
                             Thread* thread, address pc, void* siginfo, void* context, const char* filename,
                             int lineno, size_t size)
{
  static char buffer[O_BUFLEN];
  static const int fd_out = 1;  // stdout
  static int fd_log = -1;        // 错误日志文件描述符
  
  fdStream out(fd_out);
  fdStream log(fd_log);
  
  // 使用原子操作确保只执行一次(多个线程可能同时触发)
  intptr_t mytid = os::current_thread_id();
  if (_first_error_tid == -1 &&
      Atomic::cmpxchg(&_first_error_tid, (intptr_t)-1, mytid) == -1) {
    
    // 如果 SuppressFatalErrorMessage 启用,直接退出
    if (SuppressFatalErrorMessage) {
      os::abort(CreateCoredumpOnCrash);
    }
    
    // 初始化时间戳
    out.time_stamp().update_to(1);
    log.time_stamp().update_to(1);
    
    // 保存错误信息
    _id = id;
    _message = message;
    _thread = thread;
    _pc = pc;
    _siginfo = siginfo;
    _context = context;
    _filename = filename;
    _lineno = lineno;
    _size = size;
    jio_vsnprintf(_detail_msg, sizeof(_detail_msg), detail_fmt, detail_args);
    
    // 记录报告开始时间
    reporting_started();
    record_reporting_start_time();
    
    // 如果启用 ShowMessageBoxOnError,显示消息框
    if (ShowMessageBoxOnError || PauseAtExit) {
      show_message_box(buffer, sizeof(buffer));
      ShowMessageBoxOnError = false;
    }
    
    // 检查转储限制
    os::check_dump_limit(buffer, sizeof(buffer));
    
    // 安装辅助信号处理器
    install_secondary_signal_handler();
  }
  
  // Part 1: 打印简要版本到标准输出(verbose = false)
  if (!out_done) {
    if (!(ErrorFileToStdout && out.fd() == 1)) {
      report(&out, false);  // 打印简要错误信息
    }
    out_done = true;
  }
  
  // Part 2: 打印完整错误日志到文件(verbose = true)
  if (!log_done) {
    // 打开错误日志文件
    if (!log.is_open()) {
      if (ErrorFileToStdout) {
        fd_log = 1;  // 输出到标准输出
      } else if (ErrorFileToStderr) {
        fd_log = 2;  // 输出到标准错误
      } else {
        // 准备错误日志文件(默认:hs_err_pid%p.log)
        fd_log = prepare_log_file(ErrorFile, "hs_err_pid%p.log", true,
                                  buffer, sizeof(buffer));
        if (fd_log != -1) {
          out.print_raw("# An error report file with more information is saved as:\n# ");
          out.print_raw_cr(buffer);
        } else {
          out.print_raw_cr("# Can not save log file, dump to screen..");
          fd_log = 1;  // 无法创建文件,输出到标准输出
        }
      }
      log.set_fd(fd_log);
    }
    
    // 生成完整错误日志
    report(&log, true);
    log_done = true;
    
    // 关闭日志文件
    if (fd_log > 3) {
      close(fd_log);
      fd_log = -1;
    }
    log.set_fd(-1);
  }
  
  // 打印 NMT 统计信息(如果启用)
  if (PrintNMTStatistics) {
    fdStream fds(fd_out);
    MemTracker::final_report(&fds);
  }
  
  // 执行 OnError 命令(如果启用)
  // ... 省略 OnError 处理 ...
  
  // 生成核心转储并退出
  os::abort(dump_core && CreateCoredumpOnCrash, _siginfo, _context);
}

执行流程关键步骤

  1. 原子检查:使用原子操作确保只执行一次错误报告
  2. 保存错误信息:保存错误 ID、消息、线程、PC、上下文等信息
  3. 打印简要信息:先打印简要错误信息到标准输出
  4. 生成错误日志:创建 hs_err_pid%p.log 文件并写入完整错误信息
  5. 生成核心转储:如果 CreateCoredumpOnCrash 启用,生成核心转储文件
  6. 退出进程:调用 os::abort() 终止进程

4.1.3.2. 错误日志文件生成
#

错误日志文件生成

jdk-jdk-17-35/src/hotspot/share/utilities/vmError.cpp

// 准备错误日志文件
fd_log = prepare_log_file(ErrorFile, "hs_err_pid%p.log", true, buffer, sizeof(buffer));

错误日志文件命名

  • 默认文件名hs_err_pid<pid>.log%p 占位符会被替换为进程 ID)
  • 自定义路径:可通过 -XX:ErrorFile=<path> 指定
  • 文件位置:默认在当前工作目录,如果无法写入则输出到标准输出

4.1.3.3. 错误日志详细内容
#

错误日志包含的内容(通过 VMError::report() 生成):

jdk-jdk-17-35/src/hotspot/share/utilities/vmError.cpp

void VMError::report(outputStream* st, bool _verbose) {
  // 1. 错误类型和消息
  st->print_cr("# A fatal error has been detected by the Java Runtime Environment:");
  // 或
  st->print_cr("# There is insufficient memory for the Java Runtime Environment to continue.");
  
  // 2. 错误详情(信号/异常名称、PC、PID、TID)
  st->print("#  Out of Memory Error (debug.cpp:364)");
  st->print(", pid=%d", os::current_process_id());
  st->print(", tid=" UINTX_FORMAT, os::current_thread_id());
  
  // 3. JVM 版本信息
  report_vm_version(st, buf, sizeof(buf));
  
  // 4. 问题帧信息(如果有上下文)
  if (_context) {
    st->print_cr("# Problematic frame:");
    frame fr = os::fetch_frame_from_context(_context);
    fr.print_on_error(st, buf, sizeof(buf));
  }
  
  // 5. 核心转储信息
  if (CreateCoredumpOnCrash) {
    st->print("Core dump will be written. Default location: %s", coredump_message);
  }
  
  // 6. 摘要信息(verbose 模式)
  if (_verbose) {
    st->print_cr("---------------  S U M M A R Y ------------");
    // VM 选项摘要
    Arguments::print_summary_on(st);
    // 机器和 OS 信息
    os::print_summary_info(st, buf, sizeof(buf));
    // 日期和时间
    os::print_date_and_time(st, buf, sizeof(buf));
  }
  
  // 7. 线程信息(verbose 模式)
  if (_verbose) {
    st->print_cr("---------------  T H R E A D  ---------------");
    if (_thread) {
      _thread->print_on_error(st, buf, sizeof(buf));
    }
    // 堆栈边界
    // 寄存器信息
    // 堆栈跟踪
  }
  
  // 8. Java 对象堆信息(verbose 模式)
  if (_verbose) {
    st->print_cr("---------------  H E A P  ---------------");
    Universe::heap()->print_on_error(st);
  }
  
  // 9. 所有线程信息(verbose 模式)
  if (_verbose) {
    st->print_cr("---------------  T H R E A D S  ---------------");
    Threads::print_on_error(st, buf, sizeof(buf));
  }
  
  // 10. 动态库信息(verbose 模式)
  if (_verbose) {
    st->print_cr("---------------  D Y N A M I C  L I B R A R I E S  ---------------");
    os::print_dll_info(st);
  }
  
  // 11. 环境变量(verbose 模式)
  if (_verbose) {
    st->print_cr("---------------  E N V I R O N M E N T  V A R I A B L E S  ---------------");
    os::print_environment_variables(st, buf, sizeof(buf));
  }
  
  // 12. 信号处理器(verbose 模式)
  if (_verbose) {
    st->print_cr("---------------  S I G N A L S  ---------------");
    os::print_signal_handlers(st, buf, sizeof(buf));
  }
}

错误日志主要内容

  1. 错误类型和消息:致命错误类型和详细错误消息
  2. 进程和线程信息:进程 ID(PID)、线程 ID(TID)
  3. JVM 版本信息:Java 版本、JVM 版本、构建信息
  4. 问题帧信息:发生错误的代码位置和堆栈帧
  5. 核心转储信息:是否生成核心转储及位置
  6. VM 选项摘要:所有 JVM 启动参数
  7. 系统信息:操作系统、CPU、内存信息
  8. 当前线程信息:线程状态、堆栈边界、寄存器、堆栈跟踪
  9. Java 对象堆信息:堆大小、使用情况、GC 信息
  10. 所有线程信息:所有 Java 线程和本地线程的堆栈跟踪
  11. 动态库信息:加载的共享库列表
  12. 环境变量:JVM 相关的环境变量
  13. 信号处理器:已安装的信号处理器信息

4.2. ShowMessageBoxOnError
#

4.2.1. 参数说明
#

说明:在 VM 致命错误时显示消息框,保持进程存活,等待用户交互。

默认false

类型product,不可通过 JMX 修改

举例-XX:+ShowMessageBoxOnError

4.2.2. 使用场景
#

主要用于桌面应用程序的调试,现在一般不用了。

4.3. CreateCoredumpOnCrash
#

4.3.1. 参数说明
#

说明:在 VM 致命错误时创建核心转储(core dump)或迷你转储(minidump)。

默认true

类型product,不可通过 JMX 修改

举例

  • -XX:+CreateCoredumpOnCrash(默认,可省略)
  • -XX:-CreateCoredumpOnCrash(禁用)

4.3.2. 平台差异
#

  • Linux:生成 core dump 文件
  • Windows:生成 minidump 文件
  • macOS:生成 core dump 文件

4.3.3. 实现机制
#

核心转储生成

jdk-jdk-17-35/src/hotspot/share/utilities/vmError.cpp

// 在 report_and_die 的最后
os::abort(dump_core && CreateCoredumpOnCrash, _siginfo, _context);

os::abort 实现

jdk-jdk-17-35/src/hotspot/share/runtime/os.cpp

void os::abort(bool dump_core) {
  // 如果启用转储核心,调用平台特定的核心转储生成函数
  if (dump_core) {
    // Linux: 调用 abort() 系统调用,触发 SIGABRT,由系统生成 core dump
    // Windows: 调用 MiniDumpWriteDump() 生成 minidump
    // macOS: 调用 abort() 系统调用,触发 SIGABRT,由系统生成 core dump
    os::abort(dump_core, NULL, NULL);
  } else {
    // 不生成核心转储,直接退出
    os::die();
  }
}

核心转储生成条件

  1. CreateCoredumpOnCrash 启用:默认启用(true
  2. 系统配置允许:需要系统配置允许生成核心转储(如 ulimit -c unlimited
  3. 磁盘空间充足:核心转储文件可能很大(与 Java 对象堆大小相关)

核心转储文件位置

  • Linux:默认在当前工作目录,文件名为 corecore.<pid>
  • Windows:默认在当前工作目录,文件名为 hs_err_pid<pid>.mdmp
  • macOS:默认在 /cores/ 目录,文件名为 core.<pid>

核心转储文件大小

  • 核心转储包含进程的完整内存映像,大小通常等于进程的虚拟内存大小
  • 对于大 Java 对象堆的进程,核心转储文件可能达到数 GB 甚至数十 GB

核心转储用途

  • 使用调试器(如 gdb)分析崩溃原因
  • 查看崩溃时的内存状态、寄存器值、堆栈信息
  • 分析内存泄漏、内存损坏等问题

4.4. SuppressFatalErrorMessage
#

4.4.1. 参数说明
#

说明:抑制致命错误消息的输出,避免死锁。

默认false

类型product,不可通过 JMX 修改

举例-XX:+SuppressFatalErrorMessage

4.4.2. 使用场景
#

在某些特殊场景下,错误报告可能导致死锁,此时可以启用此参数抑制错误消息。

4.5. OnError
#

4.5.1. 参数说明
#

说明:在 VM 致命错误时执行用户定义的命令或脚本。

默认:空字符串(不执行)

类型product,不可通过 JMX 修改

举例

-XX:OnError="gdb -batch -ex 'thread apply all bt' %p"
-XX:OnError="/path/to/error_handler.sh"

4.5.2. 实现机制
#

OnError 命令执行

jdk-jdk-17-35/src/hotspot/share/utilities/vmError.cpp

static bool skip_OnError = false;
if (!skip_OnError && OnError && OnError[0]) {
  skip_OnError = true;  // 使用静态变量确保只执行一次
  // ... 省略其他代码 ...
  
  // 解析并执行每个命令(使用分号分隔)
  const char* ptr = OnError;
  while ((cmd = next_OnError_command(buffer, sizeof(buffer), &ptr)) != NULL) {
    // 在子进程中异步执行命令,不阻塞主线程
    os::fork_and_exec(cmd);
  }
}

关键点

  1. 只执行一次:使用静态变量 skip_OnError 确保只执行一次
  2. 支持多个命令:使用分号分隔多个命令,通过 next_OnError_command() 解析
  3. 执行时机:在错误报告过程中执行,在生成错误日志后、JVM 退出前执行
  4. 异步执行:使用 os::fork_and_exec() 在子进程中执行,不阻塞主线程

4.6. ErrorLogTimeout
#

4.6.1. 参数说明
#

说明:限制错误日志写入的超时时间(秒)。如果超时,错误报告会中断。

默认120(2 分钟)

类型product,不可通过 JMX 修改

范围:0 到 max_jlong/1000

  • 0:无超时限制

举例-XX:ErrorLogTimeout=60

4.6.2. 使用场景
#

在某些场景下,错误报告可能耗时过长(如大 Java 对象堆的堆栈跟踪),可以设置超时时间限制。

5. 其他诊断参数
#

5.1. MaxJavaStackTraceDepth
#

5.1.1. 参数说明
#

说明:限制 Java 异常堆栈跟踪的最大深度(行数)。

默认1024

类型product,不可通过 JMX 修改

范围:0 到 max_jint/2

  • 0:无限制(打印所有堆栈)

举例-XX:MaxJavaStackTraceDepth=512

5.1.2. 实现机制
#

堆栈深度限制检查

jdk-jdk-17-35/src/hotspot/share/runtime/thread.cpp

// 在打印堆栈跟踪时检查深度限制
if (MaxJavaStackTraceDepth > 0 && MaxJavaStackTraceDepth == count) {
  return;  // 达到限制,停止打印后续堆栈信息
}

关键点

  • 当堆栈深度达到 MaxJavaStackTraceDepth 限制时,停止打印后续堆栈信息
  • MaxJavaStackTraceDepth == 0 表示无限制,打印所有堆栈
  • 限制检查在每次打印堆栈帧时进行,确保不会超过限制

5.1.3. 使用场景
#

用于限制异常堆栈跟踪的输出长度,特别是在深度递归场景下。

5.2. SelfDestructTimer
#

5.2.1. 参数说明
#

说明:自毁定时器,在指定时间(分钟)后导致 VM 终止。

默认0(关闭)

类型product,不可通过 JMX 修改

范围:0 到 max_intx

  • 0:关闭

举例-XX:SelfDestructTimer=60(60 分钟后终止)

5.2.2. 实现机制
#

自毁定时器检查

jdk-jdk-17-35/src/hotspot/share/runtime/vmThread.cpp

// 在 VMThread 的循环中定期检查
if ((SelfDestructTimer != 0) && !VMError::is_error_reported() &&
    (os::elapsedTime() > (double)SelfDestructTimer * 60.0)) {
  // 达到定时器时间,终止 VM
  vm_exit(0);
}

关键点

  • 在 VMThread 的循环中定期检查自毁定时器
  • 检查条件:SelfDestructTimer != 0(已启用)且没有错误已报告
  • 时间计算:os::elapsedTime() 返回秒数,与 SelfDestructTimer * 60.0(分钟转秒)比较
  • 达到时间后调用 vm_exit(0) 正常退出 VM

6. 版本对比总结
#

6.1. 各版本特性对比表
#

6.1.1. Heap Dump 相关参数
#

特性JDK 11JDK 17JDK 21JDK 25
HeapDumpGzipLevel❌ 不支持✅ 引入
并行转储✅ 引入
并行压缩✅ 引入
HeapDumpPath %p 占位符✅ 引入
路径内存分配方式os::mallocos::mallocos::malloc✅ 栈上分配
HeapDumpPath 默认值NULLNULLnullptrnullptr
FullGCHeapDumpLimit✅ 引入
可重试分配检查in_retryable_allocation()in_retryable_allocation()is_in_internal_oome_mark()
内部 OOM 标记机制InternalOOMEMark

6.1.2. OutOfMemoryError 处理相关参数
#

特性JDK 11JDK 17JDK 21JDK 25
CrashOnOutOfMemoryError 实现fatal()report_fatal()report_fatal()report_fatal()
ExitOnOutOfMemoryError 实现os::exit(3)os::exit(3)os::_exit(3)os::_exit(3)
OnOutOfMemoryError %p 占位符✅ 支持✅ 支持✅ 支持✅ 支持
Atomic::cmpxchg 参数顺序cmpxchg(1, &addr, 0)cmpxchg(&addr, 0, 1)cmpxchg(&addr, 0, 1)cmpxchg(&addr, 0, 1)

6.1.3. 错误日志和诊断相关参数
#

特性JDK 11JDK 17JDK 21JDK 25
ErrorFile %p 占位符✅ 支持✅ 支持✅ 支持✅ 支持
CreateCoredumpOnCrash✅ 默认启用✅ 默认启用✅ 默认启用✅ 默认启用
OnError %p 占位符✅ 支持✅ 支持✅ 支持✅ 支持
ErrorLogTimeout✅ 默认 120 秒✅ 默认 120 秒✅ 默认 120 秒✅ 默认 120 秒

6.1.4. 其他诊断参数
#

特性JDK 11JDK 17JDK 21JDK 25
MaxJavaStackTraceDepth✅ 默认 1024✅ 默认 1024✅ 默认 1024✅ 默认 1024
SelfDestructTimer✅ 支持✅ 支持✅ 支持✅ 支持

6.2. 关键变化点分析
#

6.2.1. JDK 17 主要变化
#

6.2.1.1. Heap Dump 压缩支持
#

新增参数-XX:HeapDumpGzipLevel=<level>

  • 引入版本:JDK 17
  • JBS 链接:https://bugs.openjdk.org/browse/JDK-8260282
  • 功能:支持 gzip 压缩 Java 对象堆转储文件,压缩级别 0-9
  • 默认值0(禁用压缩)
  • 影响:可以显著减少转储文件大小,但会增加转储时间

6.2.1.2. 错误处理改进
#

CrashOnOutOfMemoryError 实现变化

  • JDK 11:使用 fatal() 函数,直接触发崩溃
  • JDK 17+:使用 report_fatal() 函数,提供错误类型、文件名、行号等详细信息
  • 影响:错误日志更详细,便于问题诊断

6.2.1.3. 可重试分配机制
#

新增检查in_retryable_allocation()

  • 功能:识别可重试的分配失败场景,避免在这些场景触发 Heap Dump
  • 触发场景
    • GC 后重试分配
    • 堆扩展后重试分配
  • 影响:减少不必要的 Heap Dump,提高性能

6.2.1.4. 原子操作 API 变化
#

Atomic::cmpxchg 参数顺序变化

  • JDK 11Atomic::cmpxchg(1, &addr, 0) - 参数顺序:(expected, addr, new_value)
  • JDK 17+Atomic::cmpxchg(&addr, 0, 1) - 参数顺序:(addr, expected, new_value)
  • 影响:与 C++11 标准库 std::atomic::compare_exchange_weak 保持一致

6.2.2. JDK 21 主要变化
#

6.2.2.1. 快速退出机制
#

ExitOnOutOfMemoryError 实现变化

  • JDK 11-20:使用 os::exit(3),会执行清理钩子(shutdown hooks)
  • JDK 21+:使用 os::_exit(3),直接退出,不执行清理钩子
  • 原因:在 Java 对象堆 OOM 场景下,执行 shutdown hooks 可能再次触发 OOM,导致退出过程卡住
  • 影响:快速退出,让容器编排系统能够快速重启服务

6.2.2.2. C++11 现代化
#

代码风格改进

  • HeapDumpPath 默认值NULLnullptr
  • 影响:代码更符合 C++11 标准,类型安全

6.2.3. JDK 25 主要变化
#

6.2.3.1. 并行转储和压缩支持
#

并行转储机制

  • 功能:使用多个线程并行遍历堆对象并写入段文件,最后合并
  • 线程数量:默认 CPU 核心数 * 3 / 8,OOM 场景下根据可用内存动态调整
  • 堆切分方式:不同 GC 使用不同切分策略(G1 按 region,ZGC 按页面等)
  • 性能提升:对于大 Java 对象堆(8GB+),可以显著减少转储时间

并行压缩

  • 功能:每个转储线程独立压缩,压缩速度随线程数线性提升(理想情况)
  • 限制:受 CPU 核心数和内存带宽限制

6.2.3.2. %p 占位符支持
#

HeapDumpPath 支持 %p 占位符

  • 功能:在路径中使用 %p 占位符,自动替换为进程 ID
  • 实现:使用 Arguments::copy_expand_pid() 函数展开占位符
  • 使用示例-XX:HeapDumpPath=./dump_%p.hprof./dump_12345.hprof
  • 影响:更灵活的文件命名,避免多进程转储时文件名冲突

6.2.3.3. 栈上分配路径
#

路径内存分配方式变化

  • JDK 11-21:使用 os::malloc() 从原生堆分配路径内存
  • JDK 25+:使用栈上数组 char my_path[JVM_MAXPATHLEN]
  • 原因:在 OOM 场景下,os::malloc() 可能分配失败,导致无法生成转储文件
  • 影响:提高 OOM 场景下转储成功率

6.2.3.4. FullGC 转储次数限制
#

新增参数-XX:FullGCHeapDumpLimit=<count>

  • 引入版本:JDK 25(JDK 23 引入,但本文分析 LTS 版本)
  • JBS 链接:https://bugs.openjdk.org/browse/JDK-8321442
  • 功能:限制 Full GC 触发的 Java 对象堆转储次数
  • 默认值0(无限制)
  • 影响:防止频繁 Full GC 导致过多转储文件

6.2.3.5. 内部 OOM 标记机制
#

新增机制InternalOOMEMark(RAII 类)

  • 功能:标记内部 JVM 内存分配路径,这些路径中的 OOM 不应触发完整的 Heap Dump 或堆栈跟踪
  • 使用场景
    • 类加载过程中的内存分配
    • 异常对象创建
    • 其他内部 JVM 操作
  • 实现:使用 is_in_internal_oome_mark() 替代 in_retryable_allocation()
  • 影响:更精确地识别内部 OOM 场景,避免不必要的 Heap Dump 和性能开销

6.3. 版本演进总结
#

6.3.1. 演进趋势
#

  1. 性能优化

    • JDK 17:引入压缩支持,减少文件大小
    • JDK 25:引入并行转储,提高转储速度
  2. 可靠性提升

    • JDK 17:可重试分配检查,避免不必要的 Heap Dump
    • JDK 21:快速退出机制,避免 OOM 场景下二次 OOM
    • JDK 25:栈上分配路径,提高 OOM 场景下转储成功率
  3. 功能增强

    • JDK 25:%p 占位符支持,更灵活的文件命名
    • JDK 25:FullGC 转储次数限制,防止过多转储文件
    • JDK 25:内部 OOM 标记机制,更精确的 OOM 识别
  4. 代码现代化

    • JDK 21:C++11 风格(nullptr 替代 NULL
    • JDK 17+:原子操作 API 与 C++11 标准库保持一致

7. 最佳实践和建议
#

7.1. 生产环境配置建议
#

7.1.1. 推荐配置
#

今天我们分析的参数,在线上我们仅使用 OnOutOfMemoryError

-XX:StartFlightRecording=disk=true,maxsize=5000m,maxage=2d,dumpOnExit=true,filename=JFRdump文件名.jfr
-XX:+FlightRecorder 
-XX:FlightRecorderOptions=maxchunksize=128m,repository=/jfr临时文件目录,preserve-repository=true
-XX:OnOutOfMemoryError="curl -X POST http://registry/unregister?service=my-service; cp /jfr临时文件目录 /容器挂载目录;"

这样配置:

  • 开启 JFR,同时指定了会把 JFR 事件写到磁盘的临时文件目录,并且最多保存 5000 MB,最长跨度为最近 2 天的事件。
  • JDK 14 之后,引入了定时 JFR 事件元数据写入临时文件 chunk 的机制,避免 Java 堆 OOM 时因为内存不足导致最后一个 chunk 的 JFR 文件格式非法。参考:https://bugs.openjdk.org/browse/JDK-8184193
  • 开启了 JFR 的 dumpOnExit 功能,确保 JVM 退出时会把 JFR 事件写入文件。
  • 但是 Java 对象 OOM 的时候,可能 dumpOnExit 无法完成。
  • 保底是通过 OnOutOfMemoryError 指定执行命令,把 JFR 临时文件目录拷贝到容器挂载目录,确保能拿到 JFR 文件进行后续分析。
  • 同时,OnOutOfMemoryError 还会执行服务下线等操作。
  • 如前面分析,OnOutOfMemoryError 是异步执行,一条命令一个子进程执行。

这样,我们可以最大程度保证线上 Java 对象堆 OOM 时能拿到 JFR 文件进行分析。通过 JFR 文件,我们基本可以分析出 Java 对象堆 OOM 的原因,这篇文章我们不会详细讲解 JFR 文件的分析方法,下一篇文章我们会详细分析。基本通过 JFR 文件我们可以快速找到几乎一切 Java 对象堆 OOM 的原因。

7.2. 常见问题解答
#

7.2.1. 为什么 OutOfMemoryError 的 JVM 实例最好下线?
#

发生 OutOfMemoryError 时,JVM 状态已经不健康了,这个 OutOfMemoryError 可能在任意一个触发对象分配的代码中抛出来,但是不论是哪里的 Java 代码,就算是 JDK 内部的 Java 代码也没有通过 catch (Throwable t) 的方式捕获 OutOfMemoryError,这就造成了某些内部状态不一致的问题**。比如 JDK 内部的 HashMap,在 put 的时候触发 OutOfMemoryError,这个 put 操作可能会导致 HashMap 内部的数组扩容,而扩容过程中如果发生 OutOfMemoryError,那么这个 HashMap 可能就处于一个不一致的状态,导致后面这个 HashMap 无法正常使用。如果是业务代码,可能会有更严重的问题。

7.2.2. 为什么 Heap Dump 不推荐在生产环境使用?
#

  • OnOutOfMemoryError 脚本的尽快执行更重要,但是这个脚本是在 Heap Dump 生成之后才执行的,如果生成 Heap Dump 很慢,可能会导致脚本执行更晚,影响业务下线等操作。
  • 即使有了多线程 + Gzip 压缩的支持,Heap Dump 生成依然可能非常慢,特别是对于大 Java 对象堆(比如 8 GB 以上),生成 Heap Dump 可能需要几分钟甚至更久的时间,这个时间对于业务下线来说太长了。
  • 几乎所有 Java 对象堆 OOM 的原因都可以通过 JFR 文件快速分析出来,Heap Dump 并不是必须的。

7.2.3. OnOutOfMemoryError 和 OnError 的区别是什么?
#

  • OnOutOfMemoryError:只在 java.lang.OutOfMemoryError 时执行(包括 Java 对象堆 OOM 和其他类型的 OOM)
  • OnError:在 VM 致命错误导致 Crash 的时候执行(Java 对象堆 OOM 如果开启了 CrashOnOutOfMemoryError 也会触发)

7.2.4. ExitOnOutOfMemoryError 和 CrashOnOutOfMemoryError 的区别是什么?
#

  • ExitOnOutOfMemoryError:直接退出,退出码为 3,不触发崩溃生成错误日志,但是 JDK 21+ 会使用 _exit(3) 快速退出,避免执行清理钩子
  • CrashOnOutOfMemoryError:触发 JVM 崩溃,生成错误日志和核心转储,然后退出