Master Java memory leaks: learn why they happen, common misconceptions, detection strategies, and how to fix them before they crash your production system
JOptimize Team
"Java has garbage collection, so we don't have memory leaks."
If you've said this, you're not alone. But you're also completely wrong.
Java's garbage collector is powerful, but it's not magic. Memory leaks in Java are just as real as in C++—they're just harder to see until your production system crashes at 3 AM.
The difference? In Java, memory leaks are invisible until they explode.
Here's the truth: Garbage collection only frees memory when objects become unreachable.
If an object is still referenced—even if you never use it again—the GC can't touch it.
This is where Java memory leaks come from.
@Service public class UserCache { // This Map holds references forever private static Map<Long, User> cache = new HashMap<>(); public User getUser(Long id) { if (!cache.containsKey(id)) { User user = userRepository.findById(id).orElse(null); cache.put(id, user); // ← Object is cached indefinitely } return cache.get(id); } }
The problem:
cachejava.lang.OutOfMemoryError: Java heap space
Result: Your application grinds to a halt. Not because of bad code—because of a reference that should have been garbage-collected.// WRONG: Static Map that grows forever public class ConnectionManager { private static Map<String, Connection> pool = new HashMap<>(); public static void addConnection(String key, Connection conn) { pool.put(key, conn); // ← Reference never released } }
Why it leaks:
// CORRECT: Use Caffeine (bounded cache with expiration) private static LoadingCache<String, Connection> pool = Caffeine.newBuilder() .expireAfterWrite(10, TimeUnit.MINUTES) .maximumSize(1000) .build(key -> openConnection(key));
@Component public class UserEventListener { @PostConstruct public void init() { // Register listener eventBus.subscribe(this); // ← Reference held by eventBus } // NEVER unsubscribe! @EventListener public void onUserCreated(UserCreatedEvent event) { // Handle event } }
Why it leaks:
@Component public class UserEventListener { @PostConstruct public void init() { eventBus.subscribe(this); } @PreDestroy public void cleanup() { eventBus.unsubscribe(this); // ← Release reference } }
public class Parent { private Child child; public Parent(Child child) { this.child = child; child.setParent(this); // ← Circular reference } } public class Child { private Parent parent; public void setParent(Parent parent) { this.parent = parent; } }
Why it leaks:
public class Child { private WeakReference<Parent> parent; public void setParent(Parent parent) { this.parent = new WeakReference<>(parent); } public Parent getParent() { return parent != null ? parent.get() : null; } }
public class RequestContext { private static ThreadLocal<User> currentUser = new ThreadLocal<>(); public static void setUser(User user) { currentUser.set(user); // ← Reference in thread } public static User getUser() { return currentUser.get(); } // Missing cleanup! }
Why it leaks:
@Component public class RequestContextFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws Exception { try { RequestContext.setUser(user); chain.doFilter(request, response); } finally { RequestContext.clear(); // ← CRITICAL } } } public class RequestContext { private static ThreadLocal<User> currentUser = new ThreadLocal<>(); public static void clear() { currentUser.remove(); // ← Release reference } }
public class FileProcessor { public void processFile(String filename) throws IOException { FileInputStream fis = new FileInputStream(filename); BufferedReader reader = new BufferedReader(new InputStreamReader(fis)); String line; while ((line = reader.readLine()) != null) { processLine(line); } // Missing close() - file handle leaked! } }
Why it leaks:
java.io.IOException: Too many open files
Fix: Use try-with-resources:public void processFile(String filename) throws IOException { try (FileInputStream fis = new FileInputStream(filename); BufferedReader reader = new BufferedReader(new InputStreamReader(fis))) { String line; while ((line = reader.readLine()) != null) { processLine(line); } } // ← Automatically closes resources }
Day 1: Application deployed - Heap usage: 500MB / 2GB available - No issues Day 7: Heap usage growing slowly - Heap usage: 900MB - Response time: Still normal - No alarms Day 14: Heap usage accelerating - Heap usage: 1.5GB - Response time: Slight increase (100ms → 150ms) - Garbage collector running constantly Day 21: CRISIS - Heap usage: 1.9GB / 2GB - GC pauses: 2-3 seconds every few requests - Response time: 1000ms+ - Users complain: "Application is slow" - Ops team pages you Day 22: System outages - Heap full: 2GB - OutOfMemoryError thrown - Application crashes - 1,000+ users affected - Database connections pile up - Cascading failure Day 23: Emergency redeployment - Restart application - Heap resets to 500MB - System works again - But nobody knows WHY it happened - Happens again in 3 weeks
This cycle repeats until you fix the leak.
# Force heap dump jmap -dump:live,format=b,file=heap.bin <PID> # Analyze with JProfiler or Eclipse MAT # Look for: # - Growing collections # - Duplicate objects # - Unreachable references # - Static field references # Time: 2-4 hours # Accuracy: 80% (requires expertise)
# Monitor heap over time jconsole # Built-in Java monitoring visualvm # Better visualization
Watch for:
# Run with verbose GC logging java -Xmx2g \ -Xms512m \ -XX:+PrintGCDetails \ -XX:+PrintGCDateStamps \ -Xloggc:gc.log \ -jar app.jar # Analyze gc.log for: # - Heap not shrinking after Full GC # - Full GC happening more frequently # - Young generation collection time increasing
1. Deploy JOptimize JVM agent 2. Live profiling monitors: - Heap growth rate - Object allocation patterns - GC frequency & pause times 3. Detects memory leak signatures 4. Alerts: "Potential leak detected in UserCache" 5. Get actionable recommendations Time: Real-time monitoring Accuracy: 95%+
2024-12-10 14:32:15 WARN [GC] Full GC took 2341ms 2024-12-10 14:32:20 ERROR OutOfMemoryError: Java heap space 2024-12-10 14:32:21 FATAL Application crash
jmap -dump:live,format=b,file=prod-heap.bin 12345 # Transfer to local machine
1. Open prod-heap.bin in Eclipse MAT 2. Generate "Leak Suspects" report 3. See: "UserCache holds 1.2GB in 500k User objects" 4. Find code: UserCache.cache.put() in UserService
@Service public class UserService { private static Map<Long, User> cache = new HashMap<>(); public User getUser(Long id) { if (!cache.containsKey(id)) { User user = userRepository.findById(id).orElse(null); cache.put(id, user); // ← NEVER EVICTS } return cache.get(id); } }
The leak: After 500k user queries, cache holds 500k User objects. Database deletes users, but cache keeps references.
@Service public class UserService { // Use bounded cache with TTL private final LoadingCache<Long, User> cache = Caffeine.newBuilder() .maximumSize(10_000) // Max 10k users in cache .expireAfterWrite(5, TimeUnit.MINUTES) // Auto-expire .build(id -> userRepository.findById(id).orElse(null)); public User getUser(Long id) { return cache.get(id); } // When user is deleted in database public void deleteUser(Long id) { userRepository.deleteById(id); cache.invalidate(id); // ← Explicit eviction } }
# Deploy fix # Monitor heap for 7 days # Heap stays stable at 500MB instead of climbing to 2GB # Problem solved!
npm install -g @joptimize/cli joptimize auth YOUR_API_KEY joptimize analyze .
That's it. You get:
Have you experienced memory leaks in Java? Share your story in the comments—we read every one.
Master Spring Boot, security, and Java performance with hands-on courses.
JOptimize finds N+1 queries, EAGER collections, and 70+ other issues in your Java codebase — in under 30 seconds.