DEV Community

Cover image for Java Memory Leak Detection: Tools and Techniques for Production Applications
Aarav Joshi
Aarav Joshi

Posted on

Java Memory Leak Detection: Tools and Techniques for Production Applications

As a best-selling author, I invite you to explore my books on Amazon. Don't forget to follow me on Medium and show your support. Thank you! Your support means the world!

Memory leaks in Java applications remain a persistent challenge for developers. Despite Java's automatic garbage collection, improper resource handling can lead to objects lingering in memory when they should be released. I've spent years battling these issues in production environments and want to share effective tools and techniques for identifying them.

Memory leaks manifest gradually, causing applications to consume increasing amounts of memory over time. If left unchecked, they eventually trigger OutOfMemoryError exceptions, degrading performance and potentially crashing applications. The challenge lies in identifying which objects aren't being garbage collected and why they're being retained.

Java applications create objects that occupy heap memory. When objects are no longer needed, the garbage collector should reclaim this space. A memory leak occurs when the application maintains references to objects that are no longer needed, preventing the garbage collector from reclaiming that memory.

Eclipse Memory Analyzer Tool (MAT)

MAT is my go-to open-source tool for analyzing heap dumps. It provides comprehensive analysis capabilities that help identify memory wastage patterns.

The tool's Leak Suspects report automatically identifies potential memory issues. I find its object retention graph particularly useful for understanding why objects cannot be garbage collected.

To capture heap dumps for MAT analysis:

// Manual heap dump generation through code
import com.sun.management.HotSpotDiagnosticMXBean;
import javax.management.MBeanServer;
import java.lang.management.ManagementFactory;

public class HeapDumper {
    public static void dumpHeap(String filePath) {
        try {
            MBeanServer server = ManagementFactory.getPlatformMBeanServer();
            HotSpotDiagnosticMXBean mxBean = ManagementFactory.newPlatformMXBeanProxy(
                server, "com.sun.management:type=HotSpotDiagnostic", HotSpotDiagnosticMXBean.class);
            mxBean.dumpHeap(filePath, true);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Once I have the heap dump, I load it into MAT and use the Dominator Tree to find large objects. The Retained Heap column shows the memory that would be freed if an object were garbage collected.

JVisualVM

JVisualVM provides real-time monitoring capabilities that help identify memory growth trends. I appreciate its ability to connect to running applications without any code changes.

It comes bundled with the JDK (through Java 8) or as a separate download for newer Java versions. The memory sampler shows object creation and retention over time, making it easier to correlate memory usage with specific application activities.

For heap dumps analysis in JVisualVM:

// Command line options to enable JMX for remote monitoring
java -Dcom.sun.management.jmxremote
     -Dcom.sun.management.jmxremote.port=9010
     -Dcom.sun.management.jmxremote.authenticate=false
     -Dcom.sun.management.jmxremote.ssl=false
     -jar myApplication.jar
Enter fullscreen mode Exit fullscreen mode

After connecting to an application, I monitor memory usage over time while executing different operations. Memory spikes that don't return to baseline after garbage collection often indicate memory leaks.

LeakCanary

LeakCanary has transformed how I address memory leaks in Android applications. This library automatically detects when activities or fragments are leaked and provides detailed reports about the leak path.

While primarily designed for Android, LeakCanary concepts can be applied to standard Java applications:

// Example LeakCanary setup in Android
dependencies {
    debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.7'
}

// In your application class
public class MyApplication extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        // LeakCanary is automatically initialized
    }
}
Enter fullscreen mode Exit fullscreen mode

For standard Java applications, I've implemented similar concepts by creating weak references to objects and periodically checking if they've been garbage collected when they should have been.

YourKit Java Profiler

YourKit provides comprehensive memory and CPU profiling. I've found its memory allocation recording feature particularly valuable for identifying memory usage patterns.

The tool can be attached to running applications with minimal overhead and provides detailed analytics about object creation and retention:

// Adding YourKit agent to Java application
java -agentpath:/path/to/yourkit/agent/lib/libyjpagent.so=delay=10000
     -jar myApplication.jar
Enter fullscreen mode Exit fullscreen mode

YourKit's object allocation call tree helps me trace where problematic objects are created. Its reference graph visualization shows why objects are being retained, making complex reference chains easier to understand.

JProfiler

JProfiler offers detailed telemetry about memory allocations and object lifetimes. Its heap walker lets me examine the contents of live objects and analyze their reference chains.

I particularly value JProfiler's allocation recording feature, which shows exactly where objects are created:

// Adding JProfiler agent to Java application
java -agentpath:/path/to/jprofiler/bin/linux-x64/libjprofilerti.so=port=8849
     -jar myApplication.jar
Enter fullscreen mode Exit fullscreen mode

The tool's memory leak detection functionality automatically tracks object growth and helps identify potential leaks by highlighting objects that grow consistently in number over multiple garbage collections.

Common Memory Leak Patterns and Solutions

Through my experience with these tools, I've identified several common memory leak patterns:

Static collections that grow unbounded are a frequent cause of memory leaks. I ensure all caches have size limits or expiration policies:

// Using a size-limited cache
Map<String, Data> cache = Collections.synchronizedMap(
    new LinkedHashMap<String, Data>(16, 0.75f, true) {
        @Override
        protected boolean removeEldestEntry(Map.Entry<String, Data> eldest) {
            return size() > MAX_CACHE_SIZE;
        }
    });
Enter fullscreen mode Exit fullscreen mode

Listener registration without corresponding deregistration often causes memory leaks. I always balance registrations with deregistrations:

// Proper listener handling
public class EventSource {
    private List<EventListener> listeners = new ArrayList<>();

    public void addEventListener(EventListener listener) {
        listeners.add(listener);
    }

    public void removeEventListener(EventListener listener) {
        listeners.remove(listener);
    }
}
Enter fullscreen mode Exit fullscreen mode

Thread-local variables that aren't properly removed can prevent class unloading:

// Cleaning up ThreadLocal variables
private static ThreadLocal<ExpensiveObject> threadLocal = new ThreadLocal<>();

public void process() {
    try {
        threadLocal.set(new ExpensiveObject());
        // Use the thread local variable
    } finally {
        threadLocal.remove(); // Clean up
    }
}
Enter fullscreen mode Exit fullscreen mode

I've learned to be particularly careful with resource cleanup in long-running applications:

// Using try-with-resources for automatic cleanup
public void processFile(String path) {
    try (FileInputStream fis = new FileInputStream(path);
         BufferedReader reader = new BufferedReader(new InputStreamReader(fis))) {

        String line;
        while ((line = reader.readLine()) != null) {
            // Process the line
        }
    } catch (IOException e) {
        // Handle exceptions
    }
}
Enter fullscreen mode Exit fullscreen mode

Implementing a Custom Memory Leak Detection System

For critical applications, I've implemented custom monitoring systems that track object creation and retention:

public class MemoryLeakDetector {
    private static final Map<Class<?>, AtomicInteger> instanceCounts = new ConcurrentHashMap<>();

    public static void trackObject(Object obj) {
        Class<?> clazz = obj.getClass();
        instanceCounts.computeIfAbsent(clazz, c -> new AtomicInteger()).incrementAndGet();
    }

    public static void releaseObject(Object obj) {
        Class<?> clazz = obj.getClass();
        AtomicInteger count = instanceCounts.get(clazz);
        if (count != null) count.decrementAndGet();
    }

    public static void printStats() {
        instanceCounts.forEach((clazz, count) -> {
            if (count.get() > 0) {
                System.out.printf("Class %s has %d instances\n", clazz.getName(), count.get());
            }
        });
    }
}
Enter fullscreen mode Exit fullscreen mode

This approach helps track object lifetime throughout the application lifecycle, though it requires adding instrumentation to object creation and destruction points.

Heap Dump Analysis Techniques

When analyzing heap dumps, I focus on several key metrics:

  1. Objects by retained size - identifying which objects hold the most memory
  2. Dominator tree analysis - finding objects that prevent others from being garbage collected
  3. Path to GC roots - understanding why objects are being retained

A systematic approach I follow when investigating memory leaks:

// Example of using MAT's OQL (Object Query Language)
// Finding all ArrayList instances with more than 1000 elements
select * from java.util.ArrayList where size() > 1000
Enter fullscreen mode Exit fullscreen mode

I also look for classloader retention, which prevents classes (and all their static references) from being unloaded:

// Finding classloaders
select * from java.lang.ClassLoader
Enter fullscreen mode Exit fullscreen mode

Memory Leak Prevention

Memory leak prevention starts with good design principles. I follow these practices in my development:

  1. Avoid static collections or ensure they have size constraints
  2. Use weak references for caches where appropriate
  3. Implement proper resource cleanup in finally blocks or try-with-resources
  4. Balance all listener registrations with deregistrations
  5. Be cautious with ThreadLocal usage

For critical sections that manage large objects, I sometimes add explicit memory management checks:

public void processLargeDataSet(List<Data> dataSet) {
    // Process data
    // ...

    // Explicitly clear references to help garbage collection
    dataSet.clear();

    // Suggest garbage collection if needed
    if (Runtime.getRuntime().freeMemory() < MEMORY_THRESHOLD) {
        System.gc();
    }
}
Enter fullscreen mode Exit fullscreen mode

While System.gc() doesn't guarantee garbage collection, it can be helpful in controlled scenarios where memory pressure is understood.

Continuous Monitoring Strategies

For production systems, I implement continuous memory monitoring to detect leaks before they become critical:

public class MemoryMonitor implements Runnable {
    private static final long MEGABYTE = 1024L * 1024L;
    private static final long WARNING_THRESHOLD = 100 * MEGABYTE;

    @Override
    public void run() {
        while (true) {
            Runtime runtime = Runtime.getRuntime();
            long usedMemory = runtime.totalMemory() - runtime.freeMemory();

            if (usedMemory > WARNING_THRESHOLD) {
                System.out.println("WARNING: High memory usage detected: " + 
                                  (usedMemory / MEGABYTE) + " MB");
                // Log detailed statistics or trigger heap dump
            }

            try {
                Thread.sleep(60000); // Check every minute
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                break;
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Combining this with metrics collection systems allows for long-term trending and early detection of memory growth patterns.

Memory leak detection and resolution require a systematic approach. By combining specialized tools with good development practices, most memory leaks can be identified and resolved before they impact users. The investment in proper memory management pays dividends in application stability and performance.

Through my years of Java development, I've found that regular memory profiling isn't just a debugging activity—it's a development practice that leads to more efficient, stable applications. The tools discussed here have helped me build systems that maintain consistent performance even after weeks of continuous operation.


101 Books

101 Books is an AI-driven publishing company co-founded by author Aarav Joshi. By leveraging advanced AI technology, we keep our publishing costs incredibly low—some books are priced as low as $4—making quality knowledge accessible to everyone.

Check out our book Golang Clean Code available on Amazon.

Stay tuned for updates and exciting news. When shopping for books, search for Aarav Joshi to find more of our titles. Use the provided link to enjoy special discounts!

Our Creations

Be sure to check out our creations:

Investor Central | Investor Central Spanish | Investor Central German | Smart Living | Epochs & Echoes | Puzzling Mysteries | Hindutva | Elite Dev | JS Schools


We are on Medium

Tech Koala Insights | Epochs & Echoes World | Investor Central Medium | Puzzling Mysteries Medium | Science & Epochs Medium | Modern Hindutva

Top comments (0)