The JVM’s garbage collector is failing to reclaim memory because an object that should no longer be referenced is still being held onto by an unexpected part of the application.
Common Causes and Fixes
1. Static Collections Holding Unwanted References
- Diagnosis: Run
jmap -histo:live <pid>and look for large collections (e.g.,ArrayList,HashMap) that are consistently growing and hold objects that should have been released. Then, usejhat <hprof_file>or a profiler like YourKit/JProfiler to analyze the heap dump and trace the references to these objects. Look forstaticfields pointing to these collections. - Fix:
// Before: public class Cache { private static List<MyObject> cacheList = new ArrayList<>(); public static void addObject(MyObject obj) { cacheList.add(obj); } // ... other methods } // After: public class Cache { private List<MyObject> cacheList = new ArrayList<>(); // Make it instance-based public void addObject(MyObject obj) { cacheList.add(obj); } public void clearCache() { // Add a method to explicitly clear if needed cacheList.clear(); } // ... other methods } - Why it works: Static collections are shared across all instances of a class and live for the entire duration of the JVM. If objects are added to a static collection and never explicitly removed, they will never be garbage collected, even if the "user" of the object no longer needs it. Making the collection instance-based or adding explicit removal/clearing logic ensures objects can be reclaimed when the instance is no longer needed or when the cache is intentionally cleared.
2. ThreadLocals Not Being Cleaned Up
-
Diagnosis: Use a heap dump analysis tool. Search for
ThreadLocalMapentries that contain references to objects that should have been garbage collected. Look forThreadobjects that are still alive but their associatedThreadLocalMapholds onto large or long-lived objects. -
Fix: Always call
threadLocalVariable.remove()in afinallyblock after you are finished with theThreadLocalvariable.private static final ThreadLocal<MyObject> myObjectHolder = new ThreadLocal<>(); public void processRequest(Request request) { try { MyObject obj = createMyObject(request); myObjectHolder.set(obj); // ... do work with obj ... } finally { myObjectHolder.remove(); // Crucial for preventing leaks } } -
Why it works: When a thread is managed by a thread pool (common in web servers like Tomcat, Jetty), the thread is reused. If a
ThreadLocalvalue is set and not removed, the object referenced by theThreadLocalwill remain attached to that thread’sThreadLocalMapeven after the request processing is complete. This prevents the object from being garbage collected. Explicitly callingremove()clears the reference from theThreadLocalMap.
3. Unclosed Resources (Streams, Connections, etc.)
-
Diagnosis: Heap dump analysis showing many instances of stream objects, database connections, or other resource wrappers that are still open and holding onto underlying system resources or large buffers. Look for objects like
FileInputStream,Socket,PreparedStatement, or custom resource wrappers that are not being closed. -
Fix: Use try-with-resources for all
AutoCloseableresources.// Before (prone to leaks if exceptions occur before close()): Reader reader = null; try { reader = new FileReader("myfile.txt"); // ... read from reader ... } finally { if (reader != null) { try { reader.close(); } catch (IOException e) { // handle close exception } } } // After (safe and concise): try (Reader reader = new FileReader("myfile.txt")) { // ... read from reader ... } catch (IOException e) { // handle read or close exception } -
Why it works: The
try-with-resourcesstatement ensures thatclose()is called on each resource declared in thetrystatement, even if an exception occurs. This guarantees that resources are properly released, preventing leaks of file handles, network sockets, database connections, and associated memory buffers.
4. ClassLoader Leaks
- Diagnosis: This is often seen in application servers where applications are hot-deployed or redeployed. Heap dumps show a large number of classes loaded by a
ClassLoaderthat should have been discarded when the application was undeployed. Common culprits are static fields within classes loaded by thatClassLoader, orThreadLocalvariables still referencing objects from thatClassLoader. - Fix:
- Application Servers: Ensure your application server’s undeployment process correctly tears down application classloaders. Check for custom
ClassLoaderimplementations that might not be properly disposing of resources or references. - General: Review static fields and
ThreadLocalusage. If a class is loaded by an application-specificClassLoader, any static fields in that class will keep the class (and thus theClassLoader) alive. Look for libraries that might cache instances or configurations using static fields tied to application-specific objects.
// Example of a problematic pattern to avoid: public class AppSpecificUtil { private static final Map<String, Object> CACHE = new HashMap<>(); // Static cache public static void put(String key, Object value) { CACHE.put(key, value); // If 'value' is from an app-specific class, it keeps the class alive } // ... } - Application Servers: Ensure your application server’s undeployment process correctly tears down application classloaders. Check for custom
- Why it works: A
ClassLoaderis garbage collected only when no objects are referencing any classes it loaded. If an application is undeployed, itsClassLoadershould also be eligible for garbage collection. Leaks occur when static references,ThreadLocals, or other long-lived objects (often managed by the container itself or other persistent parts of the system) continue to hold references to classes loaded by the application’sClassLoader.
5. Unnecessary Object Caching/Serialization
-
Diagnosis: Heap dump analysis reveals a large number of objects that are instances of configuration objects, parsed XML/JSON data, or other data structures that are created once and then cached indefinitely in memory. Look for patterns where objects are created, then stored in a
MaporListwith a key that might never be removed or invalidated. -
Fix: Implement an eviction policy for your caches (e.g., LRU - Least Recently Used, time-based expiration) or explicitly clear caches when they are no longer needed. Consider using libraries like Guava’s
Cacheor Caffeine for robust caching solutions.// Using Guava Cache: import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import java.util.concurrent.TimeUnit; public class ConfigCache { private final Cache<String, AppConfig> configCache = CacheBuilder.newBuilder() .expireAfterWrite(10, TimeUnit.MINUTES) // Evict after 10 minutes .maximumSize(1000) // Limit to 1000 entries .build(); public AppConfig getConfig(String key) { return configCache.get(key, () -> loadConfigFromSource(key)); } private AppConfig loadConfigFromSource(String key) { // ... logic to load config ... return new AppConfig(key); } } -
Why it works: Caching is essential for performance, but unbounded caches will eventually consume all available memory. Implementing eviction policies ensures that older or less-used items are automatically removed, allowing them to be garbage collected and preventing memory leaks.
6. Finalizers
-
Diagnosis: Objects with
finalize()methods can cause memory leaks because the garbage collector needs to perform a two-step process: first, it identifies objects that are no longer reachable; second, it schedules them for finalization. If thefinalize()method itself creates a new reference to the object (a "resurrection"), the object will not be garbage collected in the next cycle, and this can lead to a persistent leak if it happens repeatedly. Also, objects waiting for finalization are held longer in memory. -
Fix: Avoid using
finalize()methods. If you must clean up resources, usetry-with-resourcesor explicitclose()methods.// Avoid this pattern: public class RiskyResource { private Object resource; public RiskyResource() { this.resource = new Object(); // Imagine this is a large native resource } @Override protected void finalize() throws Throwable { try { // Cleanup logic here - problematic if it resurrects 'this' System.out.println("Finalizing RiskyResource"); // DON'T DO THIS: this.resource = null; // Still not enough if 'this' is referenced elsewhere // DON'T DO THIS EVER: someStaticList.add(this); // Resurrection! } finally { super.finalize(); // Always call super.finalize() } } } -
Why it works: Finalizers introduce unpredictable delays and complexity. Objects are only eligible for garbage collection after their finalizer has run, and if the finalizer resurrects the object, it delays collection indefinitely. Modern Java practices strongly recommend against finalizers, favoring explicit resource management patterns.
The next error you’ll likely encounter after fixing these JVM memory leaks is a java.lang.OutOfMemoryError: Metaspace if you’ve also been loading and unloading many classes without proper cleanup, or a persistent slowdown due to excessive garbage collection activity.