Skip to main content
JDK Tough Way - 5. Heap Dump and Error Handling Diagnostic Evolution and Best Practices
  1. Posts/

JDK Tough Way - 5. Heap Dump and Error Handling Diagnostic Evolution and Best Practices

·9151 words·43 mins
NeatGuyCoding
Author
NeatGuyCoding
Table of Contents

0. Important Notice
#

This article provides an in-depth analysis of JVM error handling and diagnostic parameter design principles, implementation mechanisms, and version evolution. The article covers:

  1. Heap Dump Related Parameters: HeapDumpOnOutOfMemoryError, HeapDumpBeforeFullGC, HeapDumpAfterFullGC, etc.
  2. OutOfMemoryError Handling Parameters: OnOutOfMemoryError, CrashOnOutOfMemoryError, ExitOnOutOfMemoryError
  3. Error Logging and Diagnostic Parameters: ErrorFile, ShowMessageBoxOnError, CreateCoredumpOnCrash, etc.
  4. Other Diagnostic Parameters: MaxJavaStackTraceDepth, SelfDestructTimer

This article involves JVM internal implementation details, including C++ source code analysis, safepoint mechanisms, VM_Operation mechanisms, etc. The design philosophy and implementation principle analysis in the article are mainly based on my personal understanding of the source code. If there are any inaccuracies, please feel free to correct them! This article is primarily based on analysis of HotSpot source code from JDK 11, 17, 21, and 25.

Among these parameters, the most commonly seen is probably -XX:+HeapDumpOnOutOfMemoryError, but regardless of version, it is not recommended to enable any Heap Dump parameters in production environments. Subsequent sections will explain why and show you how to diagnose Java heap OOM issues without relying on this parameter.

The most common error we need to diagnose is java.lang.OutOfMemoryError, and this issue becomes even more prominent after introducing virtual threads. Before virtual threads, I/O blocking would block threads and task queues, limiting throughput. After introducing virtual threads, I/O-blocked threads are suspended, allowing the scheduler to continue running other virtual threads to process tasks, significantly improving throughput. However, most frameworks haven’t yet developed mature solutions for backpressure and rate limiting in this context, so in high-concurrency scenarios, virtual threads may cause faster memory consumption, making it easier to trigger OutOfMemoryError. Furthermore, when OutOfMemoryError occurs, the JVM state is already unhealthy, and this error can be thrown at any code location that triggers object allocation. Regardless of where in the Java code—even internal JDK Java code—there’s no catching of OutOfMemoryError through catch (Throwable t), which causes internal state inconsistency issues. For example, JDK’s internal HashMap, when triggering OutOfMemoryError during put, may cause the HashMap’s internal array expansion, and if OutOfMemoryError occurs during expansion, the HashMap may be left in an inconsistent state, making it unusable afterward. Business code may have even more serious problems. After OutOfMemoryError occurs, our safest approach is to exit the process as quickly as possible to avoid continued operation causing more problems. And JDK’s internal parameter mechanisms allow us to achieve exactly this.

1. Overview
#

1.1. Error Handling and Diagnostic Parameter Categories
#

JVM error handling and diagnostic parameters can be categorized as follows:

1.1.1. Heap Dump Related Parameters#

Used to dump Java heap memory snapshots at specific times for subsequent analysis:

1.1.2. OutOfMemoryError Handling Parameters
#

Used to perform specific operations when Java heap OOM occurs:

  • -XX:OnOutOfMemoryError=<command>: Execute user-defined command or script
    • Introduced in JDK 1.6 (not considering backports)
    • Default: "" (empty string, JDK 11, 17, 21, 25)
  • -XX:+CrashOnOutOfMemoryError: Trigger JVM crash on Java heap OOM
  • -XX:+ExitOnOutOfMemoryError: Exit JVM on Java heap OOM

1.1.3. Error Logging and Diagnostic Parameters
#

Used to control error log output and diagnostic behavior:

  • -XX:ErrorFile=<file>: Error log file path
  • -XX:+ShowMessageBoxOnError: Show message box on fatal error (rarely used now)
    • Introduced in JDK 1.4 (not considering backports)
    • Default: false (JDK 11, 17, 21, 25)
  • -XX:+CreateCoredumpOnCrash / -XX:-CreateCoredumpOnCrash: Create core dump on fatal error
    • Introduced in early versions
    • Default: true (JDK 11, 17, 21, 25)
  • -XX:+SuppressFatalErrorMessage: Suppress fatal error messages
    • Introduced in early versions
    • Default: false (JDK 11, 17, 21, 25)
  • -XX:OnError=<command>: Execute user-defined command on fatal error
    • Introduced in early versions
    • Default: "" (empty string, JDK 11, 17, 21, 25)
  • -XX:ErrorLogTimeout=<seconds>: Error log write timeout
    • Introduced in early versions
    • Default: 120 (2 minutes, JDK 11, 17, 21, 25)

1.1.4. Other Diagnostic Parameters
#

  • -XX:MaxJavaStackTraceDepth=<depth>: Maximum Java exception stack trace depth
    • Introduced in early versions
    • Default: 1024 (JDK 11, 17, 21, 25)
  • -XX:SelfDestructTimer=<minutes>: Self-destruct timer (for testing)
    • Introduced in early versions
    • Default: 0 (disabled, JDK 11, 17, 21, 25)

1.2. Version Evolution Overview
#

VersionMain FeaturesKey Improvements
JDK 11Basic functionalityBasic Heap Dump and Java heap OOM handling support
JDK 17Compression supportAdded gzip compression, improved error handling
JDK 21Fast exitUses _exit for fast exit, C++11 modernization
JDK 25Parallel dumpingSupports parallel dumping, %p placeholder, stack allocation, FullGC dump count limit

2. Heap Dump Related Parameters#

2.1. Why Not Enable Heap Dump Parameters?
#

Important Reminder: Regardless of version, it is not recommended to enable any Heap Dump parameters in production environments. The reasons are explained below, and subsequent sections will show you how to diagnose Java heap OOM issues without relying on these parameters.

2.1.1. Disk I/O Performance Limitations
#

Heap Dump needs to write to disk, and disk I/O speed is relatively limited:

  • HDD: Sequential write 100–200 MB/s
  • SSD: Sequential write 500 MB/s to several GB/s

Heap Dump is sequential write, but the Java heap can be very large, such as 8GB, 16GB, or even larger. If the Java heap is very large, write time will be very long. Moreover, machines running Java applications won’t all be equipped with high-speed SSDs, and in cloud/container environments, disk I/O may be rate-limited and shared.

2.1.2. Compression Overhead
#

Although JDK 17 introduced gzip compression for Java heap and JDK 25 introduced multi-threaded Heap Dump with multi-threaded compression, compression is CPU-intensive. Assuming 4 CPUs, an empirical metric is:

  • Level 0/1 (fastest)
    • Compression ratio: 2.3×–2.7×
    • Throughput: 1.0–3.0 GB/s
    • Time per GB: 0.33–1.0 s
  • Level 3
    • Compression ratio: 2.5×–2.9×
    • Throughput: 0.9–2.2 GB/s
    • Time per GB: 0.45–1.1 s
  • Level 5 (commonly used for cost-effectiveness)
    • Compression ratio: 2.7×–3.1×
    • Throughput: 0.7–1.8 GB/s
    • Time per GB: 0.55–1.4 s
  • Level 6 (default/safe)
    • Compression ratio: 2.8×–3.2×
    • Throughput: 0.6–1.6 GB/s
    • Time per GB: 0.62–1.7 s
  • Level 7
    • Compression ratio: 2.9×–3.3×
    • Throughput: 0.45–1.2 GB/s
    • Time per GB: 0.83–2.2 s
  • Level 8
    • Compression ratio: 2.9×–3.4×
    • Throughput: 0.35–0.9 GB/s
    • Time per GB: 1.1–2.9 s
  • Level 9 (maximum compression)
    • Compression ratio: 3.0×–3.5×
    • Throughput: 0.25–0.7 GB/s
    • Time per GB: 1.4–4.0 s

Assuming level 5, with 4 CPUs, for an 8GB Java heap, compression time is approximately 4.4–11.2 seconds, compressed file is approximately 2.6–3.2 GB, writing to HDD takes approximately 13–32 seconds, totaling approximately 17.4–43.2 seconds. This is an ideal estimate; actual performance may be worse.

2.1.3. Execution Order Impact
#

Typically, production systems use -XX:OnOutOfMemoryError=xxxxx to specify a script that performs actions like deregistering the Java process microservice from the registry, because processes experiencing OutOfMemoryError are generally unhealthy and may have issues running business logic. However, if you enable HeapDumpOnOutOfMemoryError, the OnOutOfMemoryError script can only execute after HeapDumpOnOutOfMemoryError completes.

This means: The service cannot deregister from the registry promptly, and unhealthy services may continue receiving more requests. The earlier the script executes and deregisters, the better; delayed deregistration may affect more requests.

2.2. HeapDumpOnOutOfMemoryError
#

2.2.1. Parameter Description
#

Description: Automatically dump Java heap memory to file when java.lang.OutOfMemoryError occurs.

Default: false

Type: manageable (JDK 11) or product + MANAGEABLE (JDK 17+), can be modified dynamically via JMX

Example: -XX:+HeapDumpOnOutOfMemoryError

2.2.2. Trigger Scenarios
#

Important Note: HeapDumpOnOutOfMemoryError only triggers Heap Dump when OOM types that call report_java_out_of_memory occur. The following OOM types will trigger Heap Dump:

  1. java.lang.OutOfMemoryError: Java heap space: Java heap space exhausted (Java heap OOM)
  2. java.lang.OutOfMemoryError: GC overhead limit exceeded: GC overhead limit exceeded (Java heap-related OOM)
  3. java.lang.OutOfMemoryError: Metaspace / Compressed class space: Metaspace exhausted (triggers Java heap dump because loaded classes have Class objects on Java heap)
  4. java.lang.OutOfMemoryError: Requested array size exceeds VM limit: Array size exceeds limit (triggers Java heap dump as it typically indicates oversized collections)

OOM types that will NOT trigger Heap Dump:

  • Thread creation failure (unable to create native thread)
  • Unicode string allocation failure
  • Class verifier out of memory
  • Unsafe allocation failure
  • Deoptimization realloc_objects failure
  • Retryable allocation failure (retry, JDK 17+)
  • Internal OOM marking scenarios (JDK 25+)

See section 2.2.3.1 for details.

2.2.3. Implementation Mechanism
#

2.2.3.1. OutOfMemoryError Types and Heap Dump Trigger Relationship
#

JVM defines multiple OutOfMemoryError types, but only types that call report_java_out_of_memory will trigger Heap Dump:

OutOfMemoryErrors that WILL trigger Heap Dump (✅):

  1. java.lang.OutOfMemoryError: Java heap space - Java heap space exhausted
  2. java.lang.OutOfMemoryError: GC overhead limit exceeded - GC overhead limit exceeded
  3. java.lang.OutOfMemoryError: Metaspace - Metaspace exhausted
  4. java.lang.OutOfMemoryError: Compressed class space - Compressed class space exhausted
  5. java.lang.OutOfMemoryError: Requested array size exceeds VM limit - Array size exceeds limit

OutOfMemoryErrors that will NOT trigger Heap Dump (❌):

  1. java.lang.OutOfMemoryError: realloc_objects - Deoptimization reallocation failure (thrown directly, doesn’t call report_java_out_of_memory)
  2. java.lang.OutOfMemoryError: retry (JDK 17+) - Retryable allocation failure (internal mechanism, doesn’t call report_java_out_of_memory)
  3. java.lang.OutOfMemoryError: Java heap space (no stack trace) (JDK 25+) - Internal OOM marking scenario (doesn’t call report_java_out_of_memory)
  4. java.lang.OutOfMemoryError: unable to create native thread: possibly out of memory or process/resource limits reached - Thread creation failure (uses THROW_MSG, doesn’t call report_java_out_of_memory)
  5. java.lang.OutOfMemoryError: could not allocate Unicode string - Unicode string allocation failure (uses THROW_MSG_0, doesn’t call report_java_out_of_memory)
  6. java.lang.OutOfMemoryError (class verifier out of memory) - Out of memory during class verification (uses THROW_MSG_, doesn’t call report_java_out_of_memory)
  7. java.lang.OutOfMemoryError (Unsafe allocation failure) - Memory allocation failure in Unsafe operations (uses THROW_0, doesn’t call report_java_out_of_memory)

Other out-of-memory situations (don’t throw OutOfMemoryError):

  • CodeCache out of memory - Directly calls vm_exit_out_of_memory, VM exits directly without throwing exception

Trigger mechanism: Only OOM types that call report_java_out_of_memory will check the HeapDumpOnOutOfMemoryError flag and trigger Heap Dump. Other OOM types throw exceptions directly without going through the report_java_out_of_memory function.

2.2.3.1.1. Source Code Explanation for OOM Types That Won’t Trigger Heap Dump
#

Thread Creation Failure: jdk-jdk-11-28/src/hotspot/share/prims/jvm.cpp

if (native_thread->osthread() == NULL) {
  // Thread creation failed, directly throw OutOfMemoryError without calling report_java_out_of_memory
  THROW_MSG(vmSymbols::java_lang_OutOfMemoryError(),
            os::native_thread_creation_failed_msg());
}

Unicode String Allocation Failure: jdk-jdk-11-28/src/hotspot/share/classfile/javaClasses.cpp

// Unicode string allocation failed, directly throw OutOfMemoryError
THROW_MSG_0(vmSymbols::java_lang_OutOfMemoryError(), "could not allocate Unicode string");

Class Verifier Out of Memory: jdk-jdk-11-28/src/hotspot/share/classfile/verifier.cpp

// Out of memory during class verification, directly throw OutOfMemoryError
THROW_MSG_(vmSymbols::java_lang_OutOfMemoryError(), message, NULL);

Unsafe Allocation Failure: jdk-jdk-11-28/src/hotspot/share/prims/unsafe.cpp

u1* class_bytes = NEW_C_HEAP_ARRAY(u1, length, mtInternal);
if (class_bytes == NULL) {
  // Memory allocation failure in Unsafe operation, directly throw OutOfMemoryError
  THROW_0(vmSymbols::java_lang_OutOfMemoryError());
}

CodeCache Out of Memory (doesn’t throw exception): jdk-jdk-11-28/src/hotspot/share/code/codeBlob.cpp

if (blob == NULL) {
  // CodeCache out of memory, directly exit VM without throwing exception
  vm_exit_out_of_memory(size, OOM_MALLOC_ERROR, "CodeCache: no room for method handle adapter blob");
}

2.2.3.2. OOM Detection and Reporting Flow Diagram
#

flowchart TD
A[Various Resource Allocation Scenarios] --> B{Resource Type}
B -->|Java Heap| C[MemAllocator::check_out_of_memory]
B -->|Metaspace| D[Metaspace::allocate]
B -->|Array Size| E1[TypeArrayKlass::allocate
JDK 11] B -->|Array Size| E2[Klass::check_array_allocation_length
JDK 17+] B -->|Other Resources| F[Other Allocation Paths
e.g., realloc_objects
Won't Trigger HeapDump] C --> G{Allocation Failure Reason} G -->|Heap Space Exhausted| H1{Retryable Allocation Check
JDK 17+: in_retryable_allocation
JDK 25+: is_in_internal_oome_mark} G -->|GC Overhead Limit| I1{Retryable Allocation Check
JDK 17+: in_retryable_allocation
JDK 25+: is_in_internal_oome_mark} H1 -->|No| H[report_java_out_of_memory
Java heap space] H1 -->|Yes JDK 17+| H2[Throw retry OOM
Won't Trigger HeapDump] H1 -->|Yes JDK 25+| H3[Throw OOM without stack trace
Won't Trigger HeapDump] I1 -->|No| I[report_java_out_of_memory
GC overhead limit exceeded] I1 -->|Yes JDK 17+| I2[Throw retry OOM
Won't Trigger HeapDump] I1 -->|Yes JDK 25+| I3[Throw OOM without stack trace
Won't Trigger HeapDump] D --> J[report_java_out_of_memory
Metaspace or
Compressed class space] E1 --> K1[report_java_out_of_memory
Requested array size
exceeds VM limit] E2 --> K2{Retryable Allocation Check
JDK 17+: in_retryable_allocation} K2 -->|No| K[report_java_out_of_memory
Requested array size
exceeds VM limit] K2 -->|Yes| K3[Throw retry OOM
Won't Trigger HeapDump] H --> L[report_java_out_of_memory function] 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 -->|Already Reported| Z[Skip] M1 -->|First Report| N{HeapDumpOnOutOfMemoryError
Enabled?} N -->|Yes| O[Print OOM Message] N -->|No| P[Check OnOutOfMemoryError] O --> Q[HeapDumper::dump_heap_from_oome] Q --> R[HeapDumper::dump_heap true] R --> S1[Build dump file path
JDK 11-21: os::malloc
JDK 25: Stack allocation] S1 --> S2{HeapDumpPath
JDK 25: Supports %p placeholder} S2 --> T[Create HeapDumper object] T --> U1[VMThread::execute
Trigger safepoint] U1 --> U2[VM_HeapDumper::doit
Execute dump at safepoint] U2 --> U3{Compression
JDK 17+: HeapDumpGzipLevel} U3 -->|Enabled| U4[Parallel compression
JDK 25: Multi-threaded] U3 -->|Disabled| U5[Direct write] U4 --> P U5 --> P P --> V{Other OOM Handling Options} 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+
Fast exit] 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 Detection and Reporting Source Code Analysis
#

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() {
  // ... Omit allocation failure check ...
  
  if (!_overhead_limit_exceeded) {
    // Java heap space exhausted, call report_java_out_of_memory to trigger Heap Dump
    report_java_out_of_memory("Java heap space");
    // ... Omit JVMTI related code ...
    THROW_OOP_(Universe::out_of_memory_error_java_heap(), true);
  } else {
    // GC overhead limit exceeded, call report_java_out_of_memory to trigger Heap Dump
    report_java_out_of_memory("GC overhead limit exceeded");
    // ... Omit JVMTI related code ...
    THROW_OOP_(Universe::out_of_memory_error_gc_overhead_limit(), true);
  }
}

JDK 17+:

bool MemAllocator::Allocation::check_out_of_memory() {
  // ... Omit allocation failure check ...
  
  const char* message = _overhead_limit_exceeded ? 
    "GC overhead limit exceeded" : "Java heap space";
  
  // JDK 17+ added retryable allocation check to avoid triggering Heap Dump in retryable scenarios
  if (!_thread->in_retryable_allocation()) {  // JDK 17-21
  // if (!_thread->is_in_internal_oome_mark()) {  // JDK 25+
    report_java_out_of_memory(message);
    // ... Omit JVMTI related code ...
    THROW_OOP_(exception, true);
  } else {
    // Retryable allocation failure, throw retry OOM (won't trigger 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+
  }
}

Version Differences:

  • JDK 17+: Added in_retryable_allocation() check, retryable allocation failures don’t trigger Heap Dump
  • JDK 25+: Uses is_in_internal_oome_mark() instead, internal OOM marking scenarios don’t trigger Heap Dump

JDK 25+ Internal OOM Marking Scenario Explanation:

InternalOOMEMark is a RAII (Resource Acquisition Is Initialization) marking class used to identify memory allocation scenarios within internal JVM operations. In these scenarios, if memory allocation fails, it should not trigger Heap Dump and complete OOM handling flow because: It occurs within JVM internals (such as deoptimization, compilation, class loading, etc.), not directly triggered by user code. User code cannot catch these exceptions, and they may be quickly recoverable, or if they’re core operations, they can fail fast and exit.

Usage Scenarios:

  • Deoptimization object reallocation: During deoptimization, objects need to be reallocated; if it fails, throw OOM without stack trace
  • Compile-time string constant resolution: Allocation failure when resolving string constants during JIT compilation
  • Array allocation during class loading: Array allocation failure during class loading
  • JVMCI related operations: Memory allocation failures in JVMCI compiler-related operations

In these scenarios, if allocation fails, it throws out_of_memory_error_java_heap_without_backtrace(), which is an OOM exception without stack trace that doesn’t call report_java_out_of_memory, thus won’t trigger Heap Dump.

Source Code Example:

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

// RAII class to mark thread in internal OOM handling scenario
// In these scenarios, OOM won't propagate to user code, so no stack trace needed
class InternalOOMEMark: public StackObj {
  explicit InternalOOMEMark(JavaThread* thread) {
    _outer = thread->is_in_internal_oome_mark();
    thread->set_is_in_internal_oome_mark(true);  // Set mark
    _thread = thread;
  }
  
  ~InternalOOMEMark() {
    _thread->set_is_in_internal_oome_mark(_outer);  // Restore mark
  }
};

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

// Reallocate objects during deoptimization, use InternalOOMEMark to mark
if (obj == nullptr && !cache_init_error) {
  InternalOOMEMark iom(THREAD);  // Enter internal OOM marking scenario
  obj = ik->allocate_instance(THREAD);  // If fails, throw OOM without stack trace
}
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) {
  // ... Omit metaspace allocation logic ...
  
  // Metaspace or compressed class space exhausted, call report_java_out_of_memory to trigger Heap Dump
  // Rationale: Loaded classes have Class objects on Java heap, Heap Dump helps analyze class loading issues
  const char* space_string = out_of_compressed_class_space ?
    "Compressed class space" : "Metaspace";
  report_java_out_of_memory(space_string);
  // ... Omit exception throwing ...
}
2.2.3.3.3. Requested array size exceeds VM limit
#

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

typeArrayOop TypeArrayKlass::allocate(int length, TRAPS) {
  // ... Omit array allocation logic ...
  
  if (length > max_length()) {
    // Array size exceeds VM limit, call report_java_out_of_memory to trigger Heap Dump
    // Rationale: Usually indicates oversized collections, possible memory leak
    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+ added retryable allocation check
    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 {
      // Retryable allocation failure, throw retry OOM (won't trigger Heap Dump)
      THROW_OOP(Universe::out_of_memory_error_retry());
    }
  }
}

2.2.3.4. OOM Report Handling
#

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;

  // Multiple threads may try to report OutOfMemoryError simultaneously, use atomic operation to ensure only once
  // JDK 11: Atomic::cmpxchg(expected, addr, new_value) returns old value
  if (Atomic::cmpxchg(1, &out_of_memory_reported, 0) == 0) {
    // Create Java heap dump before OnOutOfMemoryError command execution
    if (HeapDumpOnOutOfMemoryError) {
      tty->print_cr("java.lang.OutOfMemoryError: %s", message);
      HeapDumper::dump_heap_from_oome();
    }

    // ... Omit OnOutOfMemoryError handling ...
    
    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) parameter order changed
  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();
    }

    // ... Omit OnOutOfMemoryError handling ...
    
    if (CrashOnOutOfMemoryError) {
      // JDK 17+: Use report_fatal instead of 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+: Fast exit, doesn't run cleanup hooks
    }
  }
}

Key Points:

  • Atomic operation version difference: JDK 11 uses cmpxchg(1, &addr, 0), JDK 17+ uses cmpxchg(&addr, 0, 1) (parameter order changed)
  • Exit mechanism difference: JDK 21+ uses os::_exit(3) for fast exit without running cleanup hooks
  • Note: All places calling report_java_out_of_memory check HeapDumpOnOutOfMemoryError, no filtering by message type

2.2.3.5. Safepoint Execution Mechanism
#

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

// Called by OOM error reporting, in Java thread, not at safepoint
void HeapDumper::dump_heap_from_oome() {
  HeapDumper::dump_heap(true);
}

// Called by error reporting (not at safepoint) or by VM thread at GC safepoint
void HeapDumper::dump(const char* path) {
  // ... Omit writer creation code ...
  
  // Create VM_HeapDumper operation object
  VM_HeapDumper dumper(&writer, _gc_before_heap_dump, _oome);
  
  if (Thread::current()->is_VM_thread()) {
    // If already VM thread, must be at safepoint
    assert(SafepointSynchronize::is_at_safepoint(), "Expected to be called at a safepoint");
    dumper.doit();
  } else {
    // Otherwise trigger safepoint via VMThread::execute, all Java threads pause
    VMThread::execute(&dumper);
  }
  // ... Omit other code ...
}

Safepoint Execution Mechanism:

  1. OOM Scenario: report_java_out_of_memory called by Java thread, not at safepoint
  2. Dump Trigger: Calls HeapDumper::dump_heap_from_oome()HeapDumper::dump_heap()
  3. Safepoint Entry: Triggers safepoint via VMThread::execute(&dumper), all Java threads pause
  4. Dump Execution: VM_thread executes VM_HeapDumper::doit() at safepoint, traverses Java heap

Key Points:

  • Java heap dump executes at safepoint, ensuring Java heap state consistency
  • ✅ Via VM_Operation mechanism, executed by VM_thread at safepoint
  • ✅ All Java threads pause during dump, avoiding concurrent modifications

2.2.3.6. Java Heap Dump Execution
#

JDK 11: jdk-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) {
    // First call: Process HeapDumpPath, build base path
    // ... Omit path validation and directory check ...
    
    // If HeapDumpPath not specified or is directory, use default filename
    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: Use os::malloc to allocate path memory from native heap
    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 {
    // Subsequent dumps: Append sequence number
    jio_snprintf(my_path, len, "%s.%d", base_path, dump_file_seq);
  }
  dump_file_seq++;

  // Create HeapDumper object and execute dump (no compression support)
  HeapDumper dumper(false, true, oome);
  dumper.dump(my_path);
  os::free(my_path);
}

JDK 17+:

void HeapDumper::dump_heap(bool oome) {
  // ... Omit path processing logic (similar to JDK 11) ...
  
  // JDK 17+: Determine file extension based on HeapDumpGzipLevel
  const char* dump_file_ext = (HeapDumpGzipLevel > 0) ? ".hprof.gz" : ".hprof";
  
  // ... Omit path construction ...
  
  // Create HeapDumper object, supports compression
  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+: Use stack allocation to avoid allocation failure in OOM scenarios
  char my_path[JVM_MAXPATHLEN];
  
  if (dump_file_seq == 0) {
    // JDK 25+: Supports %p placeholder, use Arguments::copy_expand_pid to expand
    if (HeapDumpPath != nullptr && strstr(HeapDumpPath, "%p") != nullptr) {
      Arguments::copy_expand_pid(HeapDumpPath, my_path, JVM_MAXPATHLEN);
    }
    // ... Omit other path processing ...
  }
  
  // JDK 25+: Supports parallel dump and compression
  HeapDumper dumper(false, true, oome);
  dumper.dump(my_path);
}

Version Difference Summary:

  • JDK 11: No compression support, uses os::malloc to allocate path memory
  • JDK 17+: Supports gzip compression (HeapDumpGzipLevel), file extension is .hprof or .hprof.gz
  • JDK 21+: Uses nullptr instead of NULL (C++11 style)
  • JDK 25+: Supports stack allocation for path (avoids allocation failure in OOM scenarios), supports %p placeholder, supports parallel dump and compression

2.2.3.7. JDK 25+ Parallel Dump and Compression Mechanism Details
#

JDK 25 introduced a parallel dump mechanism that can fully utilize multi-core CPUs to accelerate the dump process for large Java heaps. Parallel dump adopts a two-phase design of segmented writing + merging.

2.2.3.7.1. Parallel Dump Flow Diagram
#
flowchart TD
A[HeapDumper::dump] --> B[Create DumpWriter
and GZipCompressor] B --> C[VM_HeapDumper constructor
Pass num_dump_threads] C --> D[VMThread::execute
Trigger safepoint] D --> E[VM_HeapDumper::doit
Execute at safepoint] E --> F[Optional: GC before dump] F --> G[prepare_parallel_dump
Determine actual thread count] G --> H{Parallel dump?} H -->|No| I[Single-threaded dump
work VMDumperId] H -->|Yes| J[Create ParallelObjectIterator
Initialize heap partitioning] J --> K[workers->run_task
Start multiple workers] K --> L1[Worker 0: VM Dumper] K --> L2[Worker 1-N: Parallel Dumpers] L1 --> M1[lock_global_writer
Acquire global write lock] M1 --> N1[Write file header
HPROF_HEADER] N1 --> O1[Write UTF8 records
HPROF_UTF8] O1 --> P1[Write class load records
HPROF_LOAD_CLASS] P1 --> Q1[Write stack traces
HPROF_TRACE] Q1 --> R1[unlock_global_writer
Release global write lock] L2 --> M2[wait_for_start_signal
Wait for VM Dumper to complete non-heap data] M2 --> N2[Create segment files
base_path.p1, .p2, ...] R1 --> S[Parallel phase begins] S --> T1[VM Dumper: Write class dumps
HPROF_GC_CLASS_DUMP] S --> T2[VM Dumper: Write thread objects
HPROF_GC_ROOT_THREAD_OBJ] S --> T3[VM Dumper: Write JNI global refs
HPROF_GC_ROOT_JNI_GLOBAL] T1 --> U[Parallel heap object traversal] T2 --> U T3 --> U U --> V1[Worker 0: ParallelObjectIterator
Traverse heap region 0] U --> V2[Worker 1: ParallelObjectIterator
Traverse heap region 1] U --> V3[Worker N: ParallelObjectIterator
Traverse heap region N] V1 --> W1[Write instance dumps
HPROF_GC_INSTANCE_DUMP
to segment file .p0] V2 --> W2[Write instance dumps
HPROF_GC_INSTANCE_DUMP
to segment file .p1] V3 --> W3[Write instance dumps
HPROF_GC_INSTANCE_DUMP
to segment file .pN] W1 --> X1{Compression enabled?} W2 --> X2{Compression enabled?} W3 --> X3{Compression enabled?} X1 -->|Yes| Y1[GZip compress segment data
Independent compression buffer] X1 -->|No| Z1[Direct write to segment file] X2 -->|Yes| Y2[GZip compress segment data
Independent compression buffer] X2 -->|No| Z2[Direct write to segment file] X3 -->|Yes| Y3[GZip compress segment data
Independent compression buffer] X3 -->|No| Z3[Direct write to segment file] Y1 --> AA1[finish_dump_segment
Complete segment write] Y2 --> AA2[finish_dump_segment
Complete segment write] Y3 --> AA3[finish_dump_segment
Complete segment write] Z1 --> AA1 Z2 --> AA2 Z3 --> AA3 AA1 --> BB1[dumper_complete
Notify completion] AA2 --> BB2[dumper_complete
Notify completion] AA3 --> BB3[dumper_complete
Notify completion] BB1 --> CC[VM Dumper waits
wait_all_dumpers_complete] BB2 --> CC BB3 --> CC CC --> DD[Safepoint ends
Return to calling thread] DD --> EE[DumpMerger::do_merge
Merge segment files] EE --> FF[Read segment files .p0, .p1, ..., .pN] FF --> GG{Linux?} GG -->|Yes| HH[sendfile system call
Zero-copy merge] GG -->|No| II[read + write
Regular merge] HH --> JJ[Write HPROF_HEAP_DUMP_END] II --> JJ JJ --> KK[Delete temporary segment files] KK --> LL[Complete merge
Generate final .hprof file] 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. Thread Count Determination
#

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

// Default thread count: 3/8 of CPU cores
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) {
  // Thread count limit in OOM scenarios: Each thread requires approximately 20MB memory
  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);
    }
  }
  // ... Omit other code ...
}

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;
  
  // Check if parallel dump is possible
  if (num_active_workers <= 1 || num_requested_dump_threads <= 1) {
    _num_dumper_threads = 1;  // Single-threaded dump
  } else {
    // Limit between 2 and active_workers
    _num_dumper_threads = clamp(num_requested_dump_threads, 2U, num_active_workers);
  }
  
  _dumper_controller = new (std::nothrow) DumperController(_num_dumper_threads);
  // ... Log recording ...
}

Thread Count Determination Rules:

  1. Default value: CPU cores * 3 / 8 (e.g., 8-core CPU defaults to 3 threads)
  2. OOM scenario limit: Dynamically adjusted based on available memory, each thread requires approximately 20MB (DumpWriter buffer, DumperClassCacheTable, GZipCompressor buffers)
  3. Actual thread count: clamp(requested threads, 2, active_workers), at least 2 threads to enable parallel, at most not exceeding GC worker thread count
  4. Single-thread fallback: If active_workers <= 1 or requested threads <= 1, fall back to single-threaded dump
2.2.3.7.3. Heap Partitioning Method
#

Parallel dump uses ParallelObjectIterator to partition the heap; different GC implementations have different partitioning strategies:

G1 GC Partitioning Method: jdk-jdk-25-36/src/hotspot/share/gc/g1/g1CollectedHeap.cpp

class G1ParallelObjectIterator : public ParallelObjectIteratorImpl {
  G1HeapRegionClaimer _claimer;  // Partition by 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) {
    // Each worker processes different regions
    _heap->object_iterate_parallel(cl, worker_id, &_claimer);
  }
};

Partitioning Strategies:

  • G1 GC: Partition by Heap Region, each worker thread processes different regions
  • ZGC: Partition by Page
  • Shenandoah: Partition by region, using task queue
  • Parallel GC: Partition by heap area

Key Points:

  • Each worker thread independently traverses assigned heap areas
  • Uses mechanisms like G1HeapRegionClaimer to ensure regions aren’t processed repeatedly
  • Partitioning granularity depends on GC implementation, usually consistent with GC’s parallel strategy
2.2.3.7.4. Parallel Dump Execution Flow
#

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

void VM_HeapDumper::doit() {
  CollectedHeap* ch = Universe::heap();
  
  // Optional: Execute GC before dump
  if (_gc_before_heap_dump) {
    ch->collect_as_vm_thread(GCCause::_heap_dump);
  }
  
  WorkerThreads* workers = ch->safepoint_workers();
  prepare_parallel_dump(workers);  // Determine actual thread count
  
  if (!is_parallel_dump()) {
    // Single-threaded dump
    work(VMDumperId);
  } else {
    // Parallel dump: Create ParallelObjectIterator and start multiple workers
    ParallelObjectIterator poi(_num_dumper_threads);
    _poi = &poi;
    workers->run_task(this, _num_dumper_threads);  // Start worker threads
    _poi = nullptr;
  }
}

void VM_HeapDumper::work(uint worker_id) {
  int dumper_id = get_next_dumper_id();  // Atomically get dumper ID
  
  if (is_vm_dumper(dumper_id)) {
    // VM Dumper (worker_id=0): Responsible for non-heap data
    _dumper_controller->lock_global_writer();
    _dumper_controller->signal_start();
    
    // Write file header, UTF8, class loading, stack traces, etc.
    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();  // Release lock, allow parallel dumpers to start
  } else {
    // Parallel Dumpers (worker_id=1-N): Wait for VM Dumper to complete non-heap data
    _dumper_controller->wait_for_start_signal();
  }
  
  // Create segment file: 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 also responsible for writing class dumps, thread objects, JNI global refs, etc.
    ClassDumper class_dumper(&segment_writer);
    ClassLoaderDataGraph::classes_do(&class_dumper);
    dump_threads(&segment_writer);
    // ... Omit other GC roots ...
  }
  
  // Parallel heap object traversal
  HeapObjectDumper obj_dumper(&segment_writer, this);
  if (!is_parallel_dump()) {
    Universe::heap()->object_iterate(&obj_dumper);
  } else {
    // Parallel dump: Each worker traverses assigned heap areas
    _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 waits for all parallel dumpers to complete
    _dumper_controller->wait_all_dumpers_complete();
    writer()->flush();
    // At this point all segment files are written, need to merge outside safepoint
  }
}

Execution Flow Key Points:

  1. VM Dumper (worker_id=0):

    • Acquires global write lock, writes non-heap data (file header, UTF8, class info, stack traces)
    • After releasing lock, continues writing class dumps, thread objects, etc.
    • Finally waits for all parallel dumpers to complete
  2. Parallel Dumpers (worker_id=1-N):

    • Wait for VM Dumper to complete non-heap data writing
    • Create independent segment files (.p1, .p2, …, .pN)
    • Parallel traverse assigned heap areas, write instance dump records
    • Each segment file independently compressed (if enabled)
  3. Synchronization Mechanism:

    • Uses DumperController to coordinate multiple dumpers
    • lock_global_writer / unlock_global_writer: Protect non-heap data writing
    • wait_for_start_signal / signal_start: Ensure parallel dumpers start after VM Dumper completes
    • wait_all_dumpers_complete: VM Dumper waits for all parallel dumpers to complete
2.2.3.7.5. Parallel Compression Mechanism
#

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

class GZipCompressor : public AbstractCompressor {
  int _level;           // Compression level 0-9
  size_t _block_size;  // Compression block size
  bool _is_first;       // Whether first block
  
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) {
    // Each segment file independently compressed, using independent compression buffer
    if (_is_first) {
      // First block writes block size comment
      jio_snprintf(buf, sizeof(buf), "HPROF BLOCKSIZE=%zu", _block_size);
      *compressed_size = ZipLibrary::compress(..., buf, ...);
      _is_first = false;
    } else {
      *compressed_size = ZipLibrary::compress(..., nullptr, ...);
    }
  }
};

Parallel Compression Features:

  1. Independent compression: Each segment file uses independent GZipCompressor instance and compression buffer
  2. Block-level compression: Data compressed by blocks, each block processed independently
  3. No global synchronization: Each worker thread compresses independently, no synchronization needed
  4. Memory overhead: Each thread requires independent compression buffer (approximately 20MB/thread)
2.2.3.7.6. Segment File Merge Mechanism
#

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

int HeapDumper::dump(const char* path, ...) {
  // Phase 1: Write segment files in parallel within safepoint
  VM_HeapDumper dumper(&writer, _gc_before_heap_dump, _oome, num_dump_threads);
  VMThread::execute(&dumper);
  
  // Phase 2: Merge segment files outside safepoint (doesn't occupy VM Thread)
  DumpMerger merger(path, &writer, dumper.dump_seq());
  merger.do_merge();
}

void DumpMerger::do_merge() {
  // No need to compress again during merge (segment files already compressed)
  AbstractCompressor* saved_compressor = _writer->compressor();
  _writer->set_compressor(nullptr);
  
  // Merge all segment files in order
  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);  // Delete temporary segment file
  }
  
  _writer->set_compressor(saved_compressor);
  // Write HPROF_HEAP_DUMP_END record
  DumperSupport::end_of_dump(_writer);
}

#ifdef LINUX
void DumpMerger::merge_file(const char* path) {
  // Linux: Use sendfile for zero-copy merge (efficient)
  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) {
  // Other platforms: Use read + write merge
  fileStream segment_fs(path, "rb");
  while ((cnt = segment_fs.read(_writer->buffer(), 1, _writer->buffer_size())) != 0) {
    _writer->set_position(cnt);
    _writer->flush();
  }
}
#endif

Merge Mechanism Features:

  1. Two-phase design:

    • Phase 1: Write segment files in parallel within safepoint (.p0, .p1, …, .pN)
    • Phase 2: Merge segment files outside safepoint (doesn’t occupy VM Thread, doesn’t affect GC)
  2. Merge optimization:

    • Linux: Uses sendfile system call for zero-copy merge
    • Other platforms: Uses read + write regular merge
    • No need to compress again during merge (segment files already compressed)
  3. File naming:

    • Segment files: base_path.p0, base_path.p1, …, base_path.pN
    • Final file: base_path (all segment files deleted after merge)
2.2.3.7.7. Design and Limitations
#

Design Considerations:

  1. Parallel traversal: Multiple threads simultaneously traverse different heap areas, fully utilizing multi-core CPUs
  2. Parallel compression: Each thread compresses independently, compression speed scales linearly with thread count (ideal case)
  3. Zero-copy merge: Linux platform uses sendfile for efficient segment file merging

Limitations:

  1. Memory overhead: Each thread requires approximately 20MB memory (DumpWriter buffer, compression buffer, etc.)
  2. OOM scenario limitations: Limited available memory during OOM, may not enable parallel dump
  3. Disk I/O bottleneck: Even with parallel dump, ultimately limited by disk I/O speed
  4. Merge overhead: Merge phase needs to read all segment files and write to final file

2.3. HeapDumpBeforeFullGC / HeapDumpAfterFullGC
#

2.3.1. Parameter Description
#

Description:

  • HeapDumpBeforeFullGC: Dump Java heap before Full GC
  • HeapDumpAfterFullGC: Dump Java heap after Full GC

Default: false

Type: manageable (JDK 11) or product + MANAGEABLE (JDK 17+), can be modified dynamically via JMX

Example:

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

2.3.2. Implementation Mechanism
#

Calls CollectedHeap::full_gc_dump() before and after Full GC; if the corresponding flag is enabled, triggers Java heap dump.

JDK 11-24 Implementation:

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");
  // Check if corresponding Heap Dump parameter is enabled
  if ((HeapDumpBeforeFullGC && before) || (HeapDumpAfterFullGC && !before)) {
    // Record GC log and trigger Java heap dump
    GCTraceTime(Info, gc) tm(before ? "Heap Dump (before full gc)" : "Heap Dump (after full gc)", timer);
    HeapDumper::dump_heap();
  }
  // ... Omit other code ...
}

JDK 25+ Changes: Added FullGCHeapDumpLimit to prevent excessive dump files from frequent Full GCs:

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;  // Static counter, records number of dumps
  // Check if corresponding Heap Dump parameter is enabled
  if ((HeapDumpBeforeFullGC && before) || (HeapDumpAfterFullGC && !before)) {
    // Check dump count limit: 0 means unlimited, otherwise check if limit exceeded
    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++;  // Increment counter after dump
    }
  }
  // ... Omit other code ...
}

Key Points:

  • Full GC Java heap dump executes during Full GC safepoint
  • JDK 25 added dump count limit to prevent excessive dump files from frequent Full GCs

2.4. HeapDumpPath
#

2.4.1. Parameter Description
#

Description: Specifies the path (filename or directory) for Java heap dump files.

Default: NULL (JDK 11-17) or nullptr (JDK 21+)

Type: manageable (JDK 11) or product + MANAGEABLE (JDK 17+), can be modified dynamically via JMX

Example:

  • -XX:HeapDumpPath=/path/to/dump.hprof (specify file)
  • -XX:HeapDumpPath=/path/to/dumps/ (specify directory, will auto-generate filename)

2.4.2. File Naming Rules
#

  • If not specified or empty, default filename is java_pid<pid>.hprof
  • If specified as directory, will create default filename in directory
  • For multiple dumps, subsequent files append sequence number: java_pid<pid>.hprof.1, java_pid<pid>.hprof.2, etc.

JDK 25 Addition: Supports %p placeholder, automatically replaced with process ID.

2.4.3. Implementation Mechanism
#

2.4.3.1. Parameter Definition and Parsing
#

Parameter Definition:

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)")

Parameter Type Description:

  • ccstr: C string type, stores pointer to string
  • MANAGEABLE: Can be modified dynamically via JMX
  • Default value: NULL (JDK 11-17) or nullptr (JDK 21+)

Parameter Parsing:

  • Specified at JVM startup via -XX:HeapDumpPath=<path>
  • Can be modified at runtime via JMX HotSpotDiagnosticMXBean.setVMOption()
  • Parameter value stored in global variable HeapDumpPath

2.4.3.2. Path Processing Logic
#

JDK 11-21 Implementation:

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) {
    // First call: Calculate maximum path length and validate
    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 not specified, use default filename
    } else {
      strcpy(base_path, HeapDumpPath);
      // Check if path is existing directory
      DIR* dir = os::opendir(base_path);
      if (dir == NULL) {
        // Not a directory, treat as filename
        use_default_filename = false;
      } else {
        // Is directory, append file separator (if needed)
        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());
          }
        }
      }
    }
    
    // If HeapDumpPath is not filename, append default filename
    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);
    }
    
    // Use os::malloc to allocate path memory from native heap
    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 {
    // Subsequent dumps: Append sequence number
    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++;
  
  // ... Execute dump ...
  os::free(my_path);
}

Key Processing Logic:

  1. Path length validation: On first call, calculate maximum possible path length; if exceeds JVM_MAXPATHLEN, warn and return
  2. Directory detection: Use os::opendir() to check if path is existing directory
  3. File separator handling: If directory, automatically append file separator (if needed)
  4. Default filename generation: If not specified or is directory, generate java_pid<pid>.hprof format filename
  5. Sequence number appending: For multiple dumps, append .1, .2, etc. sequence numbers after base path
  6. Memory allocation: Use os::malloc() to allocate path memory from native heap, free after dump completes

JDK 25+ Changes: Supports %p placeholder and stack allocation for path variable:

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+: Use stack allocation to avoid allocation failure in OOM scenarios
  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) {
    // Set base path (filename or directory, default or custom, without sequence number), perform %p replacement
    const char *path_src = (HeapDumpPath != nullptr && HeapDumpPath[0] != '\0') ? HeapDumpPath : dump_file_name;
    // Use Arguments::copy_expand_pid to expand %p placeholder
    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;
    }
    
    // Check if path is existing directory
    DIR* dir = os::opendir(base_path);
    if (dir != nullptr) {
      os::closedir(dir);
      // Path is directory, append file separator (if needed)
      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());
        }
      }
      // Then add default filename, perform %p replacement. Use my_path for temporary storage
      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 {
    // Subsequent dumps: Append sequence number
    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++;
  
  // ... Execute dump ...
}

JDK 25+ Key Changes:

  1. Stack allocation: Uses stack array char my_path[JVM_MAXPATHLEN] instead of os::malloc() to avoid memory allocation failure in OOM scenarios
  2. %p placeholder support: Uses Arguments::copy_expand_pid() to expand %p placeholder to process ID
  3. Default filename includes %p: Default filename format changed to java_pid%p.hprof, replaced with actual process ID during expansion

2.4.3.3. %p Placeholder Expansion Mechanism
#

JDK 25+ Introduction: Arguments::copy_expand_pid() function used to expand %p placeholder in paths:

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 '%':         // "%%" ==> "%" (escape)
        *b++ = *p++;
        break;
      case 'p':  {       // "%p" ==> current process ID
        // Calculate available buffer size
        size_t buf_sz = buf_end - b + 1;
        // Use jio_snprintf to format process ID
        int ret = jio_snprintf(b, buf_sz, "%d", os::current_process_id());
        
        // If formatting fails or buffer insufficient, return false
        if (ret < 0 || ret >= (int)buf_sz) {
          return false;
        } else {
          b += ret;  // Move buffer pointer
          assert(*b == '\0', "fail in copy_expand_pid");
          if (p == src_end && b == buf_end + 1) {
            // Reached buffer end
            return true;
          }
        }
        p++;  // Skip 'p'
        break;
      }
      default:
        // Unknown placeholder, copy as-is
        *b++ = '%';
        *b++ = *p++;
        break;
      }
    } else {
      // Regular character, copy directly
      *b++ = *p++;
    }
  }
  
  *b = '\0';  // Ensure string null-terminated
  return true;
}

Placeholder Expansion Rules:

  1. %p: Replaced with current process ID (obtained via os::current_process_id())
  2. %%: Escaped to single % character
  3. Other %X: Unknown placeholders preserved as-is (% + X)
  4. Buffer check: Ensures expanded path doesn’t exceed buffer size, otherwise returns false

Usage Examples:

  • -XX:HeapDumpPath=./dump_%p.hprof./dump_12345.hprof (assuming process ID is 12345)
  • -XX:HeapDumpPath=/var/log/java_pid%p.hprof/var/log/java_pid12345.hprof
  • -XX:HeapDumpPath=/tmp/dumps//tmp/dumps/java_pid12345.hprof (directory + default filename)

Key Points:

  • Path length limit: Expanded path length cannot exceed JVM_MAXPATHLEN - max_digit_chars (reserve space for sequence number)
  • Directory detection timing: %p expansion executes before directory detection, so %p can be used in directory paths
  • Multiple dumps: First dump expands %p, subsequent dumps use already-expanded base path with appended sequence number

2.5. HeapDumpGzipLevel
#

2.5.1. Parameter Description
#

Description: Specifies gzip compression level (0-9) for Java heap dump files.

Default: 0 (compression disabled)

Type: product + MANAGEABLE (JDK 17+), can be modified dynamically via JMX

Range: 0-9

  • 0: Compression disabled
  • 1-9: Compression level, higher numbers mean better compression but longer compression time

Example: -XX:HeapDumpGzipLevel=5

2.5.2. Compression Performance Reference
#

Assuming 4 CPUs, compression time estimate for 8GB Java heap:

  • Level 5 (commonly used for cost-effectiveness):
    • Compression ratio: 2.7×–3.1×
    • Compression time: 4.4–11.2 seconds
    • Compressed file: 2.6–3.2 GB
    • Write to HDD: 13–32 seconds
    • Total time: 17.4–43.2 seconds

Note: This is an ideal estimate; actual performance may be worse.

2.6. FullGCHeapDumpLimit
#

2.6.1. Parameter Description
#

Description: Limits the number of Java heap dumps triggered by Full GC.

Default: 0 (unlimited)

Type: product + MANAGEABLE (JDK 25+), can be modified dynamically via JMX

Example: -XX:FullGCHeapDumpLimit=5

2.6.2. Implementation Mechanism
#

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;  // Static counter, records number of dumps
  
  if ((HeapDumpBeforeFullGC && before) || (HeapDumpAfterFullGC && !before)) {
    // Check dump count limit: 0 means unlimited, otherwise check if limit exceeded
    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++;  // Increment counter after dump
    }
  }
}

Key Points:

  • Uses static counter count to track number of dumps
  • 0 means unlimited, otherwise stops dumping after reaching limit
  • Counter shared by both HeapDumpBeforeFullGC and HeapDumpAfterFullGC

2.7. Best Practices for Heap Dump Parameters
#

2.7.1. Production Environment Recommendations
#

Not Recommended:

  • -XX:+HeapDumpOnOutOfMemoryError - Blocks OOM handling, long dump time
  • -XX:+HeapDumpBeforeFullGC - Affects GC performance, generates excessive files
  • -XX:+HeapDumpAfterFullGC - Same issues as above

Recommended Approach:

  1. Use monitoring tools: Continuously monitor heap usage via JMX/monitoring systems
  2. Manual dump: When issues detected, manually trigger dump via jmap -dump
  3. Quick exit: Use -XX:+ExitOnOutOfMemoryError to quickly exit unhealthy processes
  4. Service deregistration: Use -XX:OnOutOfMemoryError to execute deregistration script

2.7.2. How to Diagnose Java Heap OOM Without HeapDumpOnOutOfMemoryError
#

Step 1: Monitor heap usage trends

  • Use monitoring tools (Prometheus, Grafana, etc.) to track heap usage
  • Set alerts: Trigger when heap usage exceeds threshold (e.g., 80%)

Step 2: Proactive dump before OOM

  • When heap usage approaches limit, manually trigger dump:
    jmap -dump:live,format=b,file=/path/to/dump.hprof <pid>
    
  • Or use JMX remote trigger dump

Step 3: Analyze dump file

  • Use MAT (Memory Analyzer Tool) or similar tools to analyze dump file
  • Identify memory leak sources: Large objects, retained objects, etc.

Step 4: Quick exit on OOM

  • Configure -XX:+ExitOnOutOfMemoryError to quickly exit on OOM
  • Configure -XX:OnOutOfMemoryError=/path/to/deregister.sh to execute deregistration script

Advantages:

  • ✅ No blocking of OOM handling flow
  • ✅ Dump at optimal timing (before OOM, heap state clearer)
  • ✅ No disk I/O performance impact
  • ✅ Service can deregister quickly

2.7.3. Testing/Development Environment Usage
#

Acceptable in testing/development environments:

  • -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/dumps/
  • -XX:HeapDumpGzipLevel=1 (fast compression, saves disk space)

Notes:

  • Ensure sufficient disk space
  • Regularly clean old dump files
  • Don’t use in performance testing (affects test results)

3. OutOfMemoryError Handling Parameters
#

3.1. OnOutOfMemoryError
#

3.1.1. Parameter Description
#

Description: Execute user-defined command or script when Java heap OOM occurs.

Default: "" (empty string)

Type: product (JDK 11, 17, 21, 25)

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

3.1.2. Execution Timing
#

OnOutOfMemoryError executes after HeapDumpOnOutOfMemoryError (if enabled):

  1. OOM detected → report_java_out_of_memory()
  2. If HeapDumpOnOutOfMemoryError enabled → Execute heap dump
  3. Execute OnOutOfMemoryError command
  4. Check CrashOnOutOfMemoryError / ExitOnOutOfMemoryError

3.1.3. Implementation Mechanism
#

jdk-jdk-11-28/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(1, &out_of_memory_reported, 0) == 0) {
    // ... HeapDumpOnOutOfMemoryError handling ...
    
    if (OnOutOfMemoryError && OnOutOfMemoryError[0]) {
      // Execute user-defined command
      VMError::report_java_out_of_memory(message);
    }
    
    // ... CrashOnOutOfMemoryError / ExitOnOutOfMemoryError handling ...
  }
}

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

void VMError::report_java_out_of_memory(const char* message) {
  // Expand %p placeholder in command
  char cmd[O_BUFLEN];
  jio_snprintf(cmd, sizeof(cmd), "%s", OnOutOfMemoryError);
  
  // Execute command
  char* cmd_line = cmd;
  while (*cmd_line != '\0') {
    if (*cmd_line == '%' && *(cmd_line + 1) == 'p') {
      // Replace %p with process ID
      char pid_str[32];
      jio_snprintf(pid_str, sizeof(pid_str), "%d", os::current_process_id());
      // ... String replacement logic ...
    }
    cmd_line++;
  }
  
  // Execute command via os::fork_and_exec or similar
  os::fork_and_exec(cmd);
}

Key Points:

  • Supports %p placeholder, replaced with current process ID
  • Command executed in separate process, doesn’t block JVM exit
  • If command execution fails, doesn’t affect subsequent OOM handling

3.1.4. Usage Examples
#

Example 1: Service deregistration script

-XX:OnOutOfMemoryError="/opt/scripts/deregister.sh %p"

deregister.sh content:

#!/bin/bash
PID=$1
echo "Process $PID encountered OOM, deregistering from service registry..."
# Call service registry API to deregister
curl -X DELETE http://registry:8500/v1/agent/service/deregister/my-service-$PID

Example 2: Send alert notification

-XX:OnOutOfMemoryError="/opt/scripts/alert.sh %p 'Java heap OOM'"

Example 3: Collect diagnostic information

-XX:OnOutOfMemoryError="/opt/scripts/collect-info.sh %p"

collect-info.sh content:

#!/bin/bash
PID=$1
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
OUTPUT_DIR="/var/log/oom-diagnostics/$PID-$TIMESTAMP"
mkdir -p $OUTPUT_DIR

# Collect thread dump
jstack $PID > $OUTPUT_DIR/thread-dump.txt

# Collect GC log (if available)
cp /var/log/gc-$PID.log $OUTPUT_DIR/ 2>/dev/null

# Record system info
top -b -n 1 > $OUTPUT_DIR/top.txt
free -h > $OUTPUT_DIR/memory.txt

3.1.5. Best Practices
#

Recommended:

  • ✅ Use for service deregistration (quickly remove unhealthy instances)
  • ✅ Use for alert notifications (notify operations team)
  • ✅ Keep script simple and fast (avoid long-running operations)
  • ✅ Ensure script has execute permissions

Not Recommended:

  • ❌ Don’t attempt to “fix” OOM in script (process already unhealthy)
  • ❌ Don’t perform complex operations (may fail due to resource constraints)
  • ❌ Don’t rely on script to prevent process exit (use with ExitOnOutOfMemoryError)

3.2. CrashOnOutOfMemoryError
#

3.2.1. Parameter Description
#

Description: Trigger JVM crash (generate core dump and hs_err log) when Java heap OOM occurs.

Default: false

Type: product (JDK 11, 17, 21, 25)

Example: -XX:+CrashOnOutOfMemoryError

3.2.2. Implementation Mechanism
#

JDK 11: jdk-jdk-11-28/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(1, &out_of_memory_reported, 0) == 0) {
    // ... HeapDumpOnOutOfMemoryError handling ...
    // ... OnOutOfMemoryError handling ...
    
    if (CrashOnOutOfMemoryError) {
      // Trigger fatal error, generate hs_err log and core dump
      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;

  if (Atomic::cmpxchg(&out_of_memory_reported, 0, 1) == 0) {
    // ... HeapDumpOnOutOfMemoryError handling ...
    // ... OnOutOfMemoryError handling ...
    
    if (CrashOnOutOfMemoryError) {
      // JDK 17+: Use report_fatal instead of 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+
    }
  }
}

Crash behavior:

  • Generates hs_err_pid<pid>.log file (error log)
  • Generates core dump file (if CreateCoredumpOnCrash enabled)
  • JVM process terminates with signal (e.g., SIGABRT)

3.2.3. Usage Scenarios
#

Suitable for:

  • Development/testing environments: Need complete crash information for debugging
  • Critical systems: Need to preserve complete scene for post-mortem analysis
  • Automated restart systems: Rely on process crash to trigger restart

Not suitable for:

  • Production environments with high availability requirements (crash causes service interruption)
  • Systems without automated restart mechanisms (manual intervention required)
  • Disk space limited environments (core dump files can be very large)

3.2.4. Comparison with ExitOnOutOfMemoryError
#

FeatureCrashOnOutOfMemoryErrorExitOnOutOfMemoryError
Exit methodCrash (signal)Normal exit (exit code 3)
hs_err log✅ Generated❌ Not generated
Core dump✅ Generated (if enabled)❌ Not generated
Exit speedSlower (generates logs)Faster
Diagnostic infoCompleteMinimal
Suitable scenariosDebugging, post-mortem analysisQuick exit, high availability

3.3. ExitOnOutOfMemoryError
#

3.3.1. Parameter Description
#

Description: Quickly exit JVM (exit code 3) when Java heap OOM occurs.

Default: false

Type: product (JDK 11, 17, 21, 25)

Example: -XX:+ExitOnOutOfMemoryError

3.3.2. Implementation Mechanism
#

JDK 11-17:

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) {
    // ... HeapDumpOnOutOfMemoryError handling ...
    // ... OnOutOfMemoryError handling ...
    // ... CrashOnOutOfMemoryError handling ...
    
    if (ExitOnOutOfMemoryError) {
      // Normal exit, exit code 3
      os::exit(3);
    }
  }
}

JDK 21+:

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) {
    // ... HeapDumpOnOutOfMemoryError handling ...
    // ... OnOutOfMemoryError handling ...
    // ... CrashOnOutOfMemoryError handling ...
    
    if (ExitOnOutOfMemoryError) {
      // JDK 21+: Use _exit for fast exit, doesn't run cleanup hooks
      os::_exit(3);
    }
  }
}

Version differences:

  • JDK 11-17: Uses os::exit(3), runs shutdown hooks and finalizers
  • JDK 21+: Uses os::_exit(3), fast exit without running cleanup hooks

3.3.3. Exit Code
#

Exit code is 3, can be used in automated scripts to identify OOM exit:

#!/bin/bash
java -XX:+ExitOnOutOfMemoryError -jar app.jar
EXIT_CODE=$?

if [ $EXIT_CODE -eq 3 ]; then
    echo "Application exited due to OutOfMemoryError"
    # Trigger alert or automated handling
fi

3.3.4. Best Practices
#

Recommended usage:

-XX:+ExitOnOutOfMemoryError \
-XX:OnOutOfMemoryError="/opt/scripts/deregister.sh %p"

Execution flow:

  1. OOM detected
  2. Execute OnOutOfMemoryError script (service deregistration)
  3. Execute ExitOnOutOfMemoryError (process exits)
  4. Container orchestration system (K8s, etc.) detects exit and restarts container

Advantages:

  • ✅ Quickly exits unhealthy process
  • ✅ Avoids continued operation causing more problems
  • ✅ Cooperates with orchestration system for automated recovery
  • ✅ Exit code 3 clearly identifies OOM cause

3.3.5. Combination with Orchestration Systems
#

Kubernetes example:

apiVersion: v1
kind: Pod
metadata:
  name: my-app
spec:
  containers:
  - name: app
    image: my-app:latest
    env:
    - name: JAVA_OPTS
      value: "-XX:+ExitOnOutOfMemoryError -XX:OnOutOfMemoryError='/scripts/deregister.sh %p'"
    livenessProbe:
      httpGet:
        path: /health
        port: 8080
      initialDelaySeconds: 30
      periodSeconds: 10
    restartPolicy: Always  # Automatically restart on exit

Docker Compose example:

version: '3'
services:
  app:
    image: my-app:latest
    environment:
      - JAVA_OPTS=-XX:+ExitOnOutOfMemoryError
    restart: always  # Automatically restart on exit

4. Error Logging and Diagnostic Parameters
#

4.1. ErrorFile
#

4.1.1. Parameter Description
#

Description: Specifies error log file path (hs_err log).

Default: NULL (default is hs_err_pid<pid>.log in current directory)

Type: product (JDK 11, 17, 21, 25)

Example: -XX:ErrorFile=/var/log/java/hs_err_pid%p.log

4.1.2. File Naming
#

  • Supports %p placeholder, replaced with process ID
  • If path is directory, automatically appends default filename
  • If file creation fails, attempts to create in /tmp or current directory

4.1.3. hs_err Log Content
#

hs_err log contains:

  • Error summary: Error type, message, thread info
  • Thread state: All thread stack traces
  • Heap info: Heap usage, GC statistics
  • Native memory: Native memory usage
  • System info: OS version, CPU info, memory info
  • VM arguments: All JVM parameters
  • Dynamic libraries: Loaded dynamic library list
  • Compilation events: Recent JIT compilation events
  • GC events: Recent GC events

4.1.4. Best Practices
#

Production environment recommendations:

-XX:ErrorFile=/var/log/java/hs_err_pid%p_%t.log
  • %p: Process ID
  • %t: Timestamp (JDK 17+)

Ensure:

  • Log directory has write permissions
  • Sufficient disk space
  • Regularly clean old log files

4.2. CreateCoredumpOnCrash
#

4.2.1. Parameter Description
#

Description: Whether to create core dump file on fatal error.

Default: true

Type: product (JDK 11, 17, 21, 25)

Example:

  • -XX:+CreateCoredumpOnCrash (enable, default)
  • -XX:-CreateCoredumpOnCrash (disable)

4.2.2. Core Dump File
#

File location:

  • Linux: Current directory or /cores (depends on system configuration)
  • Check system configuration: cat /proc/sys/kernel/core_pattern

File size:

  • Can be very large (equals process memory usage)
  • Limit core dump size: ulimit -c <size> or ulimit -c unlimited

4.2.3. Usage Scenarios
#

Enable core dump (-XX:+CreateCoredumpOnCrash):

  • Development/testing environments
  • Need to debug native code issues
  • Need complete memory snapshot for post-mortem analysis

Disable core dump (-XX:-CreateCoredumpOnCrash):

  • Production environments (saves disk space)
  • Disk space limited
  • Security concerns (core dump may contain sensitive data)

4.2.4. Core Dump Analysis
#

Use GDB to analyze:

gdb $JAVA_HOME/bin/java core.<pid>

# In GDB
(gdb) bt           # View call stack
(gdb) info threads # View all threads
(gdb) thread <n>   # Switch to thread n
(gdb) bt           # View thread n's call stack

Use jstack to analyze:

jstack $JAVA_HOME/bin/java core.<pid>

4.3. OnError
#

4.3.1. Parameter Description
#

Description: Execute user-defined command on fatal error (not just OOM).

Default: "" (empty string)

Type: product (JDK 11, 17, 21, 25)

Example: -XX:OnError="/opt/scripts/on-error.sh %p"

4.3.2. Difference from OnOutOfMemoryError
#

FeatureOnErrorOnOutOfMemoryError
Trigger conditionAny fatal errorOnly Java heap OOM
Trigger scenariosSegfault, assertion failure, etc.OutOfMemoryError
Execution timingDuring error handlingDuring OOM handling
Use casesGeneral error handlingOOM-specific handling

4.3.3. Usage Examples
#

Example: Collect crash information:

-XX:OnError="/opt/scripts/collect-crash-info.sh %p"

collect-crash-info.sh:

#!/bin/bash
PID=$1
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
OUTPUT_DIR="/var/log/crash-diagnostics/$PID-$TIMESTAMP"
mkdir -p $OUTPUT_DIR

# Copy hs_err log
cp hs_err_pid$PID.log $OUTPUT_DIR/ 2>/dev/null

# Copy core dump (if exists)
cp core.$PID $OUTPUT_DIR/ 2>/dev/null

# Record system info
uname -a > $OUTPUT_DIR/system-info.txt
free -h > $OUTPUT_DIR/memory.txt

4.4. Other Diagnostic Parameters
#

4.4.1. ShowMessageBoxOnError
#

Description: Show message box on fatal error (Windows only, rarely used).

Default: false

Note: Mainly for debugging, not suitable for production environments.

4.4.2. SuppressFatalErrorMessage
#

Description: Suppress fatal error messages.

Default: false

Note: Not recommended, makes debugging difficult.

4.4.3. ErrorLogTimeout
#

Description: Error log write timeout (seconds).

Default: 120 (2 minutes)

Purpose: Prevent error log writing from hanging indefinitely.

5. Other Diagnostic Parameters
#

5.1. MaxJavaStackTraceDepth
#

5.1.1. Parameter Description
#

Description: Maximum depth of Java exception stack traces.

Default: 1024

Type: product (JDK 11, 17, 21, 25)

Example: -XX:MaxJavaStackTraceDepth=2048

5.1.2. Impact
#

  • Stack trace truncation: If stack depth exceeds this value, stack trace is truncated
  • Performance: Larger values may impact exception throwing performance
  • Diagnostic: Smaller values may lose important call chain information

5.1.3. Best Practices
#

Default value (1024) is usually sufficient, only increase if:

  • Deep recursion scenarios
  • Complex framework call chains
  • Need complete call chain for debugging

Not recommended:

  • Setting too small (e.g., < 100) - loses important information
  • Setting too large (e.g., > 10000) - impacts performance

5.2. SelfDestructTimer
#

5.2.1. Parameter Description
#

Description: Self-destruct timer (minutes), for testing purposes.

Default: 0 (disabled)

Type: diagnostic (JDK 11, 17, 21, 25)

Example: -XX:SelfDestructTimer=60 (self-destruct after 60 minutes)

5.2.2. Purpose
#

Testing purposes only:

  • Test automated restart mechanisms
  • Test monitoring and alerting systems
  • Simulate process crashes

Not for production use: May cause unexpected process termination.

6. Summary and Recommendations
#

6.1. Production Environment Best Practices
#

Recommended configuration:

# OOM handling: Quick exit + service deregistration
-XX:+ExitOnOutOfMemoryError \
-XX:OnOutOfMemoryError="/opt/scripts/deregister.sh %p" \

# Error logging: Specify log path
-XX:ErrorFile=/var/log/java/hs_err_pid%p.log \

# Disable core dump (save disk space)
-XX:-CreateCoredumpOnCrash \

# Do NOT enable heap dump
# -XX:-HeapDumpOnOutOfMemoryError

Monitoring and alerting:

  • Monitor heap usage trends via JMX
  • Set alerts: Trigger when heap usage exceeds 80%
  • Proactively dump before OOM: jmap -dump:live,format=b,file=dump.hprof <pid>

6.2. Development/Testing Environment Configuration
#

Recommended configuration:

# OOM handling: Heap dump + crash
-XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=/tmp/dumps/ \
-XX:HeapDumpGzipLevel=1 \
-XX:+CrashOnOutOfMemoryError \

# Error logging
-XX:ErrorFile=/tmp/hs_err_pid%p.log \

# Enable core dump
-XX:+CreateCoredumpOnCrash

6.3. Key Takeaways
#

  1. Don’t enable HeapDumpOnOutOfMemoryError in production - Blocks OOM handling, long dump time
  2. Use ExitOnOutOfMemoryError for quick exit - Cooperate with orchestration systems for automated recovery
  3. Use OnOutOfMemoryError for service deregistration - Quickly remove unhealthy instances
  4. Proactive monitoring and dumping - Dump before OOM, clearer heap state
  5. Understand version differences - JDK 21+ uses _exit for faster exit, JDK 25+ supports parallel dump

6.4. Diagnostic Workflow
#

When OOM occurs:

  1. Monitoring system detects heap usage approaching limit
  2. Proactive dump via jmap or JMX
  3. Analyze dump file using MAT or similar tools
  4. If OOM occursOnOutOfMemoryError script executes (service deregistration)
  5. Process exitsExitOnOutOfMemoryError triggers quick exit
  6. Orchestration system detects exit and restarts container
  7. Post-mortem analysis using dump file and hs_err log

When other fatal errors occur:

  1. OnError script executes (collect crash information)
  2. hs_err log generated (error details)
  3. Core dump generated (if enabled)
  4. Process crashes (if CrashOnOutOfMemoryError enabled)
  5. Post-mortem analysis using hs_err log and core dump

This concludes the comprehensive analysis of JVM error handling and diagnostic parameters. Understanding these mechanisms helps build more robust and diagnosable Java applications.