A Java application running smoothly after deployment can degrade over hours or days. Response times creep up, garbage collection pauses stretch longer, and eventually the JVM crashes with OutOfMemoryError. Without heap memory monitoring, this cycle repeats until someone manually restarts the process. With heap monitoring, you detect the leak early, identify the object type consuming memory, and fix the root cause before users notice degradation.
According to the 2024 Stack Overflow Developer Survey, 30.3% of professional developers use Java, making memory management issues one of the most common production problems engineering teams face. This guide walks through every step of detecting, diagnosing, and resolving Java memory leaks using built-in JVM tools, open source profilers, and modern observability platforms.
Prerequisites
Before starting heap memory monitoring, ensure you have:
- Java Development Kit (JDK) 11 or later installed
- Application running in a test or staging environment with monitoring access
- JVM flags enabled:
-XX:+HeapDumpOnOutOfMemoryErrorand-XX:HeapDumpPath=/path/to/dumps - SSH or remote access to the application server
- Basic understanding of Java garbage collection concepts
- At least 2GB free disk space for heap dump storage
Step 1: Enable Verbose Garbage Collection Logging
Garbage collection logs are the first signal of memory problems. They show how often GC runs, how long each cycle takes, and how much memory is reclaimed. Enable verbose GC to establish a baseline before investigating leaks.
Add these JVM arguments to your application startup:
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-XX:+PrintGCTimeStamps
-Xloggc:/path/to/gc.log
For Java 9 and later, use the unified logging syntax:
-Xlog:gc*:file=/path/to/gc.log:time,uptime,level,tags
Restart your application and let it run under normal load for at least 30 minutes. Open the GC log and look for these warning signs:
- Old generation (tenured space) usage increasing steadily without dropping after full GC
- Full GC frequency increasing over time
- GC pause times exceeding 1 second
- Memory reclaimed per GC cycle decreasing below 10%
A healthy application shows old generation usage fluctuating within a stable range. A memory leak shows old generation climbing steadily despite frequent full GC cycles.
Step 2: Monitor Heap Memory Usage with JConsole
JConsole ships with the JDK and provides real time visibility into heap memory, thread count, and loaded classes. It connects to any running JVM via JMX without requiring code changes.
Start your application with JMX enabled:
-Dcom.sun.management.jmxremote
-Dcom.sun.management.jmxremote.port=9010
-Dcom.sun.management.jmxremote.authenticate=false
-Dcom.sun.management.jmxremote.ssl=false
Open JConsole from the command line:
jconsole
Select your application process from the list or enter localhost:9010 for remote connection. Navigate to the Memory tab and observe these metrics:
- Heap Memory Usage: Current vs. maximum heap size
- Non-Heap Memory Usage: Metaspace and code cache consumption
- Eden Space: Where new objects are allocated
- Survivor Space: Objects that survived one GC cycle
- Old Gen: Long lived objects after multiple GC cycles
Watch the Old Gen graph for 10 minutes. If the line rises steadily without dropping after GC, you have a memory leak. Record the rate of increase (MB per hour) to estimate how long until OutOfMemoryError occurs.
Step 3: Capture a Heap Dump for Analysis
A heap dump is a snapshot of all objects in memory at a specific moment. It shows which object types consume the most space and which references keep them alive. Capture a heap dump when memory usage is high but before the application crashes.
Generate a heap dump using jmap:
jmap -dump:live,format=b,file=/path/to/heapdump.hprof <pid>
Replace <pid> with your Java process ID, found using jps or ps aux | grep java. The live option triggers a full GC before dumping, removing garbage and showing only reachable objects.
For applications already crashing with OutOfMemoryError, the JVM automatically generates a heap dump if you set -XX:+HeapDumpOnOutOfMemoryError in Step 1. Find the dump file at the path specified by -XX:HeapDumpPath.
Heap dumps can be large (2–10GB for production applications). Compress them before transferring:
gzip heapdump.hprof
Step 4: Analyze the Heap Dump with Eclipse Memory Analyzer (MAT)
Eclipse Memory Analyzer is the standard tool for heap dump analysis. It calculates retained heap size, identifies leak suspects, and shows the path from GC roots to leaked objects.
Download MAT from eclipse.org/mat and install it. Open your heap dump file:
- Click File → Open Heap Dump
- Select the
.hproffile - Choose Leak Suspects Report when prompted
MAT runs automated analysis and highlights the top memory consumers. Look for these patterns:
- Single object type dominating retained heap: One class consuming more than 30% of heap suggests a collection growing unbounded
- Many instances of unexpected types: Thousands of HTTP session objects or cached results indicate poor lifecycle management
- Objects retained by ThreadLocal: Thread pools keeping references after request completion
Click Histogram to see object counts by class. Sort by Retained Heap (not Shallow Heap, which only shows object size without referenced objects). Right-click the top entry and select List objects → with incoming references to see what keeps these objects alive.
The Dominator Tree view shows which objects would be freed if a specific object were garbage collected. This reveals the root cause faster than tracing individual references.
Step 5: Identify the Leak Source Using Thread and Request Context
Most memory leaks in web applications originate from one of three sources: unbounded caches, event listeners never removed, or ThreadLocal variables not cleaned up. Narrow down the source by correlating heap dump data with application logs.
In MAT, open the Thread Overview and inspect threads with high retained heap. Expand each thread to see the call stack and local variables. Look for:
- HTTP request handler threads holding large response buffers
- Background task threads accumulating work queue items
- Database connection pool threads with unclosed result sets
Cross reference the thread names with your application’s thread pool configuration. If a thread named http-nio-8080-exec-42 retains 500MB, inspect the servlet or controller method running on that thread.
Search your codebase for ThreadLocal usage. Every ThreadLocal.set() must have a corresponding remove() in a finally block. Forgetting to clean ThreadLocal in thread pools causes objects to persist across multiple requests.
// Bad: ThreadLocal never cleaned
private static ThreadLocal<UserContext> userContext = new ThreadLocal<>();
public void handleRequest() {
userContext.set(new UserContext(userId));
// process request
// ThreadLocal still holds reference after request completes
}
// Good: Explicit cleanup
private static ThreadLocal<UserContext> userContext = new ThreadLocal<>();
public void handleRequest() {
try {
userContext.set(new UserContext(userId));
// process request
} finally {
userContext.remove(); // prevents memory leak
}
}
Step 6: Use Java Flight Recorder for Continuous Profiling
Java Flight Recorder (JFR) provides low overhead profiling data including allocation rate, GC activity, and exception frequency. Unlike heap dumps, JFR captures data over time, making it easier to see which code paths allocate the most objects.
Start your application with JFR enabled:
-XX:StartFlightRecording=duration=60s,filename=/path/to/recording.jfr
Or start recording on a running JVM:
jcmd <pid> JFR.start duration=60s filename=/path/to/recording.jfr
Open the recording in Java Mission Control (JMC):
jmc
Navigate to Memory → Allocations to see which classes are allocated most frequently. Sort by Allocation in New TLAB (thread local allocation buffer) to find hot allocation sites. Click on a class to see the stack trace showing where objects are created.
High allocation rate does not always indicate a leak. Short lived objects are normal and handled by young generation GC. Focus on allocations that correlate with increasing old generation usage from Step 2.
Step 7: Fix the Memory Leak and Verify with Load Testing
Once you identify the leak source, implement the fix and verify under realistic load before deploying to production. Common fixes include:
- Replace unbounded caches with LRU eviction: Use
LinkedHashMapwithremoveEldestEntry()or GuavaCacheBuilder.maximumSize() - Remove event listeners in cleanup methods: Unregister observers when components are destroyed
- Clear ThreadLocal in finally blocks: Ensure
ThreadLocal.remove()runs after every request - Close database connections and streams: Use try-with-resources for
Connection,Statement, andResultSet
After applying the fix, run a load test matching production traffic patterns. Monitor heap usage with JConsole or infrastructure monitoring platforms that track JVM metrics over time. Old generation should stabilize at a consistent level rather than climbing indefinitely.
Verify the fix eliminates the leak by running the application for at least four hours under load. If old generation usage remains flat and GC pause times stay below 500ms, the leak is resolved.
Step 8: Implement Production Monitoring to Catch Future Leaks
Fixing one memory leak does not prevent others. Set up continuous monitoring to detect new leaks before they cause outages. What is infrastructure monitoring explains how to track JVM metrics alongside application traces.
Configure alerts for these JVM metrics:
- Old generation usage above 80% for more than 10 minutes
- Full GC frequency exceeding 5 per hour
- GC pause time above 1 second
- Metaspace usage increasing above 90% of MaxMetaspaceSize
Production-grade observability platforms like CubeAPM, Datadog, and New Relic provide built-in JVM metric collection. CubeAPM runs on your own infrastructure at $0.15/GB, keeping heap dump data and GC logs inside your environment. It correlates JVM metrics with distributed traces, letting you see which API endpoints trigger excessive allocations.
For teams using Cassandra alongside Java services, Cassandra monitoring tools cover how to track both database and application memory together. JavaScript-based services have different profiling requirements covered in JavaScript application monitoring tools.
Troubleshooting Common Issues
Heap dump file is too large to transfer
Compress the heap dump before moving it:
gzip heapdump.hprof
If the compressed file still exceeds transfer limits, analyze it on the server using command line tools:
jhat -J-Xmx4g heapdump.hprof
This starts a web server on port 7000. Open http://localhost:7000 in a browser to browse the heap interactively.
MAT crashes with OutOfMemoryError while analyzing dump
Increase MAT’s heap size by editing MemoryAnalyzer.ini:
-Xmx8g
For dumps larger than 8GB, use jhat or split analysis across multiple smaller dumps taken at different times.
Application still leaks after fixing obvious ThreadLocal issues
Check for indirect references through static collections. A static Map holding objects that reference ThreadLocal variables keeps those variables alive even after cleanup. Search for static final Map or static final List in your codebase and verify their lifecycle management.
GC logs show high pause times but heap usage is stable
High GC pause time without memory leaks indicates undersized heap or inefficient GC configuration, not a leak. Increase max heap size with -Xmx or switch to a low pause collector like G1GC (-XX:+UseG1GC).
Monitoring Java Heap Memory with CubeAPM
CubeAPM provides unified Java heap memory monitoring alongside distributed tracing and logs. It collects JVM metrics via OpenTelemetry, correlates heap usage spikes with specific API calls, and alerts when old generation usage exceeds thresholds.
Unlike SaaS APM tools that export telemetry outside your cloud, CubeAPM runs inside your VPC or on premises. Heap dumps and GC logs remain on your infrastructure, meeting data residency requirements for healthcare and finance. CubeAPM’s flat $0.15/GB pricing includes unlimited users and retention, eliminating per-seat fees that compound as teams grow.
Setup takes under 15 minutes. Install the OpenTelemetry Java agent:
java -javaagent:/path/to/opentelemetry-javaagent.jar \
-Dotel.exporter.otlp.endpoint=http://cubeapm:4317 \
-Dotel.service.name=my-service \
-jar myapp.jar
CubeAPM automatically tracks heap usage, GC frequency, thread count, and allocation rate. Create alerts on old generation usage or link heap spikes to slow API endpoints in the trace view.
For complete deployment details, see the CubeAPM pricing page.
Disclaimer: The information in this article reflects the latest details available at the time of publication and may change as technologies and products evolve. Features, pricing, and plan limits can change over time. Always verify the latest information directly with the vendor before making purchasing or deployment decisions.
Frequently Asked Questions
What causes Java memory leaks in production?
Java memory leaks occur when objects remain reachable from GC roots but are no longer used by the application. Common causes include unbounded caches without eviction, event listeners never unregistered, ThreadLocal variables not cleaned in thread pools, and static collections holding references indefinitely.
How do I know if my Java application has a memory leak?
Monitor old generation heap usage over time. If it increases steadily without dropping after full GC, you have a leak. Other signs include increasing full GC frequency, longer GC pause times, and eventual OutOfMemoryError crashes despite sufficient max heap size.
What is the difference between shallow heap and retained heap?
Shallow heap is the memory occupied by an object itself, excluding referenced objects. Retained heap is the total memory that would be freed if the object were garbage collected, including all objects reachable only through it. Retained heap reveals the true cost of a memory leak.
Can garbage collection fix memory leaks?
No. Garbage collection only removes objects that are unreachable from GC roots. Memory leaks happen when objects remain reachable through references your code forgot to clear, preventing GC from reclaiming that memory.
How often should I analyze heap dumps in production?
Analyze heap dumps when memory usage is abnormally high or after OutOfMemoryError occurs. Do not schedule regular dumps in production, they cause full GC pauses and generate large files. Use continuous profiling tools like Java Flight Recorder for ongoing visibility instead.
What is the best tool for finding memory leaks in Java?
Eclipse Memory Analyzer (MAT) is the standard tool for heap dump analysis. For continuous monitoring, Java Flight Recorder provides low overhead allocation profiling. Production environments benefit from observability platforms like CubeAPM that correlate JVM metrics with application traces.
How do I prevent memory leaks in new Java code?
Use try-with-resources for all `Closeable` objects. Remove event listeners in cleanup methods. Always call `ThreadLocal.remove()` in finally blocks. Prefer bounded caches with LRU eviction over unbounded maps. Review static collections for proper lifecycle management during code review.





