1. The Hook (The "Byte-Sized" Intro)
- In a Nutshell: Memory leaks in Java = unintentional object retention (objects reachable but not needed).
- Common causes: (1) Static collections (grow forever), (2) Listeners not removed (event handlers), (3) Unclosed resources (streams, connections), (4) ThreadLocal (not cleared), (5) Inner classes (hold outer reference).
- Symptoms: Growing heap, frequent GC, OutOfMemoryError.
- Detection: Heap dumps, profilers (JVisualVM, Eclipse MAT).
- Prevention: Clear collections, remove listeners, use try-with-resources, clean ThreadLocal!
Think of email inbox. Memory leak = never deleting emails (mailbox grows forever, eventually full). Static collection = "save all" folder. Listeners = subscriptions you forgot to cancel. Fix = regular cleanup, unsubscribe, delete old emails!
2. Conceptual Clarity (The "Simple" Tier)
💡 The Analogy
- Memory leak: Hoarder's house (never throw anything away)
- Static collection: Attic (stuff accumulates)
- Listener: Magazine subscription (forgot to cancel)
3. Technical Mastery (The "Deep Dive")
Common Memory Leak Patterns
| Pattern | Cause | Fix |
|---|---|---|
| Static collections | Never cleared | Bound size, WeakHashMap |
| Listeners | Not removed | Explicit removal |
| Unclosed resources | No close() | try-with-resources |
| ThreadLocal | Not removed | remove() in finally |
| Inner classes | Outer reference | Static inner class |
4. Interactive & Applied Code
java
import java.util.*;
public class MemoryLeakDemo {
public static void main(String[] args) {
demonstrateStaticCollectionLeak();
demonstrateListenerLeak();
demonstrateThreadLocalLeak();
demonstrateInnerClassLeak();
}
// ❌ LEAK 1: Static collection
static List<byte[]> CACHE = new ArrayList<>(); // Grows forever!
static void demonstrateStaticCollectionLeak() {
System.out.println("=== STATIC COLLECTION LEAK ===");
// ❌ BAD: Unbounded cache
for (int i = 0; i < 1000; i++) {
CACHE.add(new byte[1024 * 1024]); // 1MB per item
// CACHE nev er cleared → LEAK!
}
// ✅ FIX 1: Bounded cache
// Use LRU cache (e.g., LinkedHashMap with removeEldestEntry)
// ✅ FIX 2: WeakHashMap (entries GC'd when key not strongly referenced)
}
// ❌ LEAK 2: Listeners not removed
static class EventSource {
List<EventListener> listeners = new ArrayList<>();
void addListener(EventListener listener) {
listeners.add(listener);
}
void removeListener(EventListener listener) {
listeners.remove(listener);
}
}
interface EventListener {
void onEvent();
}
static void demonstrateListenerLeak() {
System.out.println("\n=== LISTENER LEAK ===");
EventSource source = new EventSource();
// ❌ BAD: Add listener but never remove
for (int i = 0; i < 1000; i++) {
EventListener listener = () -> System.out.println("Event");
source.addListener(listener);
// Listener never removed → LEAK!
}
// ✅ FIX: Remove listeners
// source.removeListener(listener);
}
// ❌ LEAK 3: ThreadLocal not cleared
static ThreadLocal<byte[]> threadLocal = new ThreadLocal<>();
static void demonstrateThreadLocalLeak() {
System.out.println("\n=== THREADLOCAL LEAK ===");
// ❌ BAD: Set ThreadLocal but never remove
threadLocal.set(new byte[1024 * 1024]); // 1MB
// Thread reused in pool → ThreadLocal persists → LEAK!
// ✅ FIX: Always remove in finally
try {
threadLocal.set(new byte[1024 * 1024]);
// Use threadLocal
} finally {
threadLocal.remove(); // Clean up!
}
}
// ❌ LEAK 4: Inner class holds outer reference
class OuterClass {
byte[] data = new byte[1024 * 1024]; // 1MB
class InnerClass {
// Implicitly holds reference to OuterClass!
void doSomething() {
System.out.println("Inner");
}
}
InnerClass createInner() {
return new InnerClass();
}
}
static void demonstrateInnerClassLeak() {
System.out.println("\n=== INNER CLASS LEAK ===");
OuterClass outer = new OuterClass();
OuterClass.InnerClass inner = outer.createInner();
// ❌ LEAK: 'inner' holds reference to 'outer' (including 1MB data!)
// Even if we don't need 'outer', it's kept alive by 'inner'
// ✅ FIX: Use static inner class (no outer reference)
// static class InnerClass { ... }
}
}
// Real-world example: Connection pool leak
class ConnectionPoolLeak {
static List<Connection> pool = new ArrayList<>();
static Connection getConnection() {
Connection conn = new Connection();
pool.add(conn); // ❌ LEAK: Never removed from pool!
return conn;
}
// ✅ FIX: Return to pool or close
static void releaseConnection(Connection conn) {
pool.remove(conn);
conn.close();
}
}
class Connection {
void close() {
System.out.println("Connection closed");
}
}
// Unclosed resources leak
class ResourceLeak {
static void demonstrateLeak() throws Exception {
// ❌ BAD: Resource not closed
java.io.FileInputStream fis = new java.io.FileInputStream("file.txt");
// If exception thrown, fis never closed → LEAK!
// ✅ FIX: try-with-resources
try (java.io.FileInputStream fis2 = new java.io.FileInputStream("file.txt")) {
// Auto-closed even if exception
}
}
}5. The Comparison & Decision Layer
| Leak Type | Detection | Prevention |
|---|---|---|
| Static collection | Growing heap | Bound size, WeakHashMap |
| Listener | Event source grows | Remove listeners |
| Resource | File/connection leak | try-with-resources |
| ThreadLocal | ThreadPool apps | remove() in finally |
6. The "Interview Corner" (The Edge)
The "Killer" Interview Question: "Can Java have memory leaks?" Answer: YES! Even with GC!
java
// Memory leak example:
static List<Object> LEAK = new ArrayList<>();
void method() {
LEAK.add(new byte[1024 * 1024]); // 1MB
// Objects never removed from LEAK
// Still reachable (static) → GC can't collect
// Heap grows forever → OutOfMemoryError
}
// NOT a leak:
void method2() {
List<Object> local = new ArrayList<>();
local.add(new byte[1024 * 1024]);
// After method returns: 'local' unreachable → GC collects
}Pro-Tips:
- Detecting leaks:
bash
# Heap dump on OOM
java -XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=/tmp/heap.hprof \
-jar app.jar
# Analyze with Eclipse MAT
# Look for:
# - Dominator tree (what's holding memory)
# - Leak suspects (accumulating collections)
# - Histogram (object counts)- WeakHashMap for caches:
java
// ✅ Entries GC'd when key not strongly referenced
Map<Object, Object> cache = new WeakHashMap<>();
Object key = new Object();
cache.put(key, value);
key = null; // No more strong refs
// Next GC → entry removed from WeakHashMap!