Author: rtenerow

  • Automating JVM Heap Dump Analysis: A Complete Java Implementation

    📌 Note: This is continuation of my Heap Dump subject. If you want to see the full evaluation matrix of available Heap Dump CLI analysis tools, read: How to Automate Analysis of Huge JVM Heap Dump Files.

    In my previous post, I established why rewriting a heap analyzer from scratch is a multi-month architectural trap and why Eclipse MAT’s optimized, memory-mapped indexing framework was the choice for handling massive .hprof files. Now, it’s time to move from theory to an example implementation.

    Disclaimer: as I already mentioned I have not used it in the production environment yet.

    To build a fully automated diagnostics pipeline—whether for a continuous integration environment or an autonomous monitoring sidecar—we must run MAT completely headless.

    The Java implementation below demonstrates how to programmatically orchestrate Eclipse MAT using ProcessBuilder. The code below: invokes the headless CLI binary, passes critical JVM tuning arguments required for large-scale production dumps, and dynamically extracts and inspects the resulting compressed HTML reports on-the-fly to deliver a definitive binary verdict: Is there a memory leak or not?

    Key Implementation Details:
    Headless Orchestration: Uses org.eclipse.mat.api.parse to trigger out-of-process report generation.

    On-the-Fly Zip Parsing: Avoids disk overhead by utilizing ZipFile and Scanner to inspect index.html or content.html directly inside the generated archive for MAT’s signature “Problem Suspect” marker. Please notice, that those files can be used for further analysis – if we not only want whether there is a memory leak, but where it is.

    Scale Considerations: For production heaps exceeding 40 GB, ensure you uncomment the high-performance VM arguments (-Xmx, -XX:+UseG1GC, and -Dorg.eclipse.mat.api:max_index_size=long) noted in the comments to prevent the analyzer itself from running out of memory.

    Here is the complete, self-contained analyzer class, with some comments:

    import java.io.*;
    import java.nio.file.*;
    import java.util.zip.ZipEntry;
    import java.util.zip.ZipFile;
    import java.util.Scanner;
    
    /**
     * There are scenarios where the code provided will say "No memory leak detected" even though your 
     * app is slowly dying:
     * 
     * 1. The "Slow Drip": If a leak is very small (e.g., leaking 1KB every hour), and you take a heap 
     * dump after only 2 hours, MAT won't flag it as a "Problem Suspect" because it doesn't represent 
     * a significant portion of the heap yet.
     * 
     * 2. Distributed Leaks: If the leak is spread across 10,000 different small objects rather than 
     * one giant collection, MAT’s "Dominator Tree" might not see a single clear "suspect" to blame.
     * 
     * 3. High Churn (Not a Leak): Your app might be slow because it's creating millions of temporary 
     * objects (GC Pressure), but since they can be collected, MAT won't flag them as a leak.
     * 
     * 4. Native Memory: MAT only analyzes the Java Heap. If your leak is happening in native C++ code 
     * (via JNI) or in the Metaspace (class loading), this script will show 0% leakage.
     * 
     * We can simulate below program by calling: 
     * ../../mat/MemoryAnalyzer -consolelog -application org.eclipse.mat.api.parse java_pid120191.hprof 
     * 		org.eclipse.mat.api:suspects
     * 
     * How to get closer to 100%
     * To be truly sure, the "Gold Standard" isn't just one report; it's Comparison:
     *
     * Take dump_A.hprof.
     *
     * Perform some work/load testing.
     *
     * Take dump_B.hprof.
     *
     * Use MAT to compare the two. If the number of instances of a specific class increased and never went back down, you have a confirmed leak, regardless of whether it's "big" enough to be a "Problem Suspect."
     */
    
    public class HeapAnalyzer {
    
        // Path to MAT CLI executable - adapt to your locations
        static String MAT_PATH = "/home/user/Desktop/work/MemoryLeaks/mat/MemoryAnalyzer";
        static String MAT_ROOT = "/home/user/Desktop/work/MemoryLeaks/mat";
        static String HPROF_FILE = "/home/user/Desktop/work/MemoryLeaks/MemoryAnalyzerWithMAT/hprof/java_pid120191.hprof";
    
        public static void main(String[] args) throws Exception {
    
            File heapDump = new File(HPROF_FILE);
            if (!heapDump.exists()) {
                System.out.println("Heap dump not found: " + heapDump.getAbsolutePath());
                return;
            }
            
            String javaBin = System.getProperty("java.home") + File.separator + "bin" + File.separator + "java";
    
            System.out.println("Starting MAT Analysis for: " + heapDump.getName());
            
            // Consolidated Call: Parse AND Report
            // Using 'org.eclipse.mat.api.parse' + 'org.eclipse.mat.api:suspects' 
            // is the most robust way to trigger report generation.
            
            ProcessBuilder pb = new ProcessBuilder(
                MAT_PATH,
                "-vm", javaBin,
                "-clean",
    //            "-configuration", tempConfig.toAbsolutePath().toString(),
                "-consolelog",
                "-application", "org.eclipse.mat.api.parse",
                heapDump.getAbsolutePath(),
                "org.eclipse.mat.api:suspects"
            );
            
            // For huge files
            // "-vmargs",                // Tell MAT that JVM arguments follow
            // "-Xmx64g",                // Give it enough RAM to build the index (check also memory in MemoryAnalyzer.ini)
            // "-XX:+UseG1GC"            // Use a modern Garbage Collector for the analyzer
            // "-Dkeep_unreachable_objects=false", // Slims down the analysis
            // "-Dorg.eclipse.mat.api:max_index_size=long" // Necessary for massive object counts        
            // 40Gb might need 100Gb of RAM (dump 40Gb, indexes also 30-40Gb); on SSD it parsing might take 30 minutes; on normal HD hours
    
            // Set the working directory - place where configuration etc is
            pb.directory(new File(MAT_ROOT));
            
            // MAT’s stdout/stderr is printed to the console
            pb.inheritIO();
            
            Process process = pb.start();
            int exitCode = process.waitFor();
    
            if (exitCode != 0) {
                System.err.println("MAT process failed with exit code: " + exitCode);
                return;
            }
    
            // Locate the Generated ZIP
            // MAT usually names it: [HPROF_NAME]_Leak_Suspects.zip
            String name = heapDump.getName();
            String baseName = name.contains(".") ? name.substring(0, name.lastIndexOf('.')) : name;
            File reportZip = new File(heapDump.getParent(), baseName + "_Leak_Suspects.zip");
    
            if (!reportZip.exists()) {
                System.err.println("Could not find the generated report zip: " + reportZip.getAbsolutePath());
                return;
            }
    
            // Leak Detection
            analyzeLeakZip(reportZip);        
        }
    
        private static void analyzeLeakZip(File zipFile) {
            System.out.println("Analyzing report for leak indicators...");
            try (ZipFile zip = new ZipFile(zipFile)) {
                // We look for 'index.html' which summarizes suspects
                ZipEntry indexEntry = zip.getEntry("index.html");
                if (indexEntry == null) {
                    // If index isn't there, check for the suspects content page
                    indexEntry = zip.getEntry("pages/content.html");
                }
    
                if (indexEntry != null) {
                    try (InputStream is = zip.getInputStream(indexEntry);
                         Scanner scanner = new Scanner(is)) {
                        
                        boolean confirmedLeak = false;
                        while (scanner.hasNextLine()) {
                            String line = scanner.nextLine();
                            // "Problem Suspect" is the key phrase MAT uses in HTML 
                            // when it finds a significant memory accumulator.
                            if (line.contains("Problem Suspect")) {
                                confirmedLeak = true;
                                break;
                            }
                        }
    
                        if (confirmedLeak) {
                            System.out.println("RESULT: MEMORY LEAK SUSPECTED!");
                            System.out.println("Please review archive containing summarization: \n" + zipFile.getAbsolutePath());
                        } else {
                            System.out.println("RESULT: No significant leak suspects identified.");
                        }
                    }
                }
            } catch (IOException e) {
                System.err.println("Error reading ZIP file: " + e.getMessage());
            }
        }
    }

    Below are some advises for parsing huge files:

    1. Increase MAT’s Own Memory (The .ini File)
      The most common mistake is trying to analyze a 40GB dump while MAT only has 1GB of heap allocated to itself. MAT needs roughly 1.2x to 1.5x the size of the heap dump in RAM to build its indexes efficiently.

    Locate MemoryAnalyzer.ini.

    Change the -Xmx setting to at least 48g or 64g.

    Ensure you are running on a machine (or cloud instance) that actually has that much physical RAM.

    Plaintext
    -vmargs
    -Xmx64g
    -XX:+UseG1GC

    1. Use “Parser Sidekick” (Headless Parsing)
      Never try to open a 40GB file directly in the MAT GUI. The GUI will hang during the indexing phase. Instead, use your script (or the CLI) to pre-parse the indexes on a powerful server.

    Once the CLI finishes, it generates several .index files and a .zip report. You can then:

    Read the HTML report (no RAM required).

    Open the .hprof in the GUI after the indexes are built. It will open instantly because the heavy lifting is already done.

    1. Enable 64-bit Indexing
      For dumps larger than 32GB, the number of objects might exceed the limits of standard 32-bit addressing. Ensure your MemoryAnalyzer.ini includes:
      -Dorg.eclipse.mat.api:max_index_size=long (if supported by your version) to handle the massive object count.
  • How To Automate Analysis Of Huge Jvm Heap Dump Files

    The trigger: we observed JVM crashes with java.lang.OutOfMemoryError.

    The goal: find a tool that can analyze large (>10 GB) heap dump files created on OOM Error, analyze them in automated way and give a binary answer whether a memory leak was detected or not.

    There are several tools on the market which claim to be able to analyze the heap dump files: Shark, VisualVM CLI, JProfiler CLI / YourKit CLI, IBM Heap Analyzer, JHAT, Eclipse Memory Analyzer Tool. An additional alternative is to write own analysis tool.

    The winner is Eclipse Memory Analyzer Tool.

    Details

    Let’s take a look at the options we have.

    Shark (Android)

    This tool is designed specifically for Android and it is not suitable for:

    Backend services

    Microservices

    JVM heap dumps

    Large heaps (GBs)

    VisualVM CLI

    Limited automation

    Small heaps only

    JProfiler CLI / YourKit CLI

    Commercial

    Very strong automation

    Excellent diff engine

    I work with YourKit tool frequently doing manual analysis (like heap utilization or memory leaks). It is very useful and powerful, but so far I have used it on living java processes.

    This tools cannot analyze .hprof files. It can dump, create and analyze snapshot files. I did not find any tool how to convert .hprof to a snapshot.

    So if we want to release automated analysis tool we have to rely on customers having JProfiler / YourKit.

    IBM Heap Analyzer (HA)

    Excellent for OpenJ9 (high-performance Java Virtual Machine (JVM) originally developed by IBM). Since I am using a different JVM, I decided not to test this solution

    CLI automation

    JHAT

    It is not supported anymore.

    Eclipse Memory Analyzer Tool (MAT)

    MAT can check for specific class causing leak and will give true/false result for this class.

    MAT can compare two hprof files.

    MAT is the winner of my tool selection. It successfully passed several of my test cases. I have not used it in production environment so far.

    Writing own analysis tool.

    This is extremely complex and time consuming. Below some aspects of it (I compare it to the MAT).

    JVM Heap Dumps Are Massive Graphs. A .hprof file contains:

    Millions of objects
    Billions of references
    Deep object hierarchies
    Mixed primitive arrays, strings, classes, threads, GC roots
    Example: a 1 GB server heap can have 20–50 million objects.

    To detect leaks, we must analyze all objects’ reachability. That is literally a giant directed graph.

    Challenge: We need a memory-efficient graph representation — otherwise our analyzer will run out of memory before it finishes analyzing the heap.

    Heap dump format is tricky as .hprof files are binary files. There are:

    Multiple formats depending on VM
    Object ID sizes differ per JVM (32-bit vs 64-bit)
    Classes, fields, arrays, primitives are encoded differently
    References vs primitive values require different parsing
    So we need a robust, low-level parser. One small mistake = parser crashes on a 20 GB heap.

    We need GC root resolution
    Leaks are only defined relative to GC roots (things that are always reachable).

    Static fields
    Threads
    JNI references
    Classloaders
    We must implement:

    A system that finds all roots
    Correctly identifies strong vs weak references
    Tracks reachable vs unreachable objects
    Otherwise, we will misreport leaks.

    Dominator tree computation is non-trivial
    To find retained memory (what a leak “holds”):

    We must build a dominator tree of the heap graph. This involves graph algorithms on millions of nodes. Naive implementations can use hundreds of GB of RAM for large heaps. Eclipse MAT does this efficiently using memory-mapped indexes, compression, and optimized algorithms.

    Rewriting it yourself = months of careful work.

    We need retained size calculations
    To detect leaks:

    We must sum memory recursively from dominator tree. Count arrays, object headers, references correctly. Handle shared subgraphs properly
    Mistakes here → false positives/negatives.

    We need scalability + stability
    Our tool must handle:

    1 GB → small test dumps (easy)
    40–100 GB production dumps (hard)
    Running in headless, CI/CD environments
    Parsing without crashing
    Most homegrown parsers fail silently on production dumps.

    We need automation / reporting / error handling. Even if we parse the heap correctly:

    We must generate reports programmatically
    Identify “Problem Suspects”
    Handle malformed dumps
    Support multiple JVM types
    All this adds more complexity.

    Industry estimate
    Rewriting MAT from scratch is approximately:

    StepTime estimate
    HPROF parser2–3 months (some hprof parsers can be found on GitHub)
    Graph builder + dominator tree2–3 months
    Retained size calculation1 month
    Leak detection heuristics1 month
    Testing & validation2–3 months
    Total~8–12 months

    And that’s for a team of engineers experienced in JVM internals.

    Summary
    Reasons why it is “extremely complex”:

    JVM heaps are massive and intricate graphs
    .hprof binary format is complicated and JVM-dependent
    Must correctly identify GC roots
    Must compute dominator trees efficiently
    Must calculate retained memory precisely
    Must scale to production heaps
    Must generate usable, reliable reports
    Bottom line: We would be basically rewriting Eclipse MAT, which has been refined over 15+ years.

    Instead of writing our own parser:

    We can use MAT Core + CLI automation
    Focus on automating leak detection logic
    We get all graph algorithms, retained size, dominators, and reports for free

    Our work becomes hours of automation and Java code, not months of low-level engineering.

    Notice: despite years of development, MAT is still not 100% correct:

    Off-Heap Blindness: It cannot track native memory leaks (like DirectByteBuffers or JNI allocations) because they don’t live in the .hprof file.

    Unreachable Objects: Automated dumps can capture “dead” objects not yet swept by GC, sometimes leading to false positives depending on the garbage collector algorithm used (e.g., ZGC or Shenandoah).

    Complex Cyclic Dependencies: If objects reference each other in massive circular chains, MAT’s Dominator Tree algorithm can struggle to isolate a single root suspect.

    🚀 What’s Next?

    If you are interested to see code example, where I build a fully headless Java orchestrator using ProcessBuilder to parse .hprof file automatically please read: Automating JVM Heap Dump Analysis – A Complete Java Implementation.