Avoiding Memory Leaks

Yes, Java Can Leak

GC frees unreachable objects. A Java "leak" is an object that is still reachable but no longer needed. Common causes:

CauseFix
Static collections / unbounded caches that only growbounded cache (Caffeine maximumSize, TTL), WeakHashMap
Listeners registered and never removedunsubscribe in a close()/onDestroy(); weak listeners
ThreadLocal in thread pools never clearedtry { tl.set(x); ... } finally { tl.remove(); }
Unclosed resources (streams, connections, ResultSets)try-with-resources
Inner (non-static) classes holding the outer object (Android Activities)static nested classes, weak references
Huge objects kept in HTTP sessionstore ids, not whole objects

ThreadLocal Cleanup

Example

Pool threads are reused. If a request leaves data in a ThreadLocal, the next request on the same thread sees it (a leak and a security bug).

import java.util.concurrent.*;

public class Main {
    static final ThreadLocal<String> CURRENT_USER = new ThreadLocal<>();

    static String handle(String user, boolean cleanUp) {
        String before = CURRENT_USER.get();
        if (user != null) CURRENT_USER.set(user);
        try {
            return "saw leftover user: " + before;
        } finally {
            if (cleanUp) CURRENT_USER.remove();
        }
    }

    public static void main(String[] args) throws Exception {
        try (ExecutorService pool = Executors.newSingleThreadExecutor()) {
            System.out.println("Request 1 (no cleanup):   " + pool.submit(() -> handle("ravi", false)).get());
            System.out.println("Request 2 (anonymous):    " + pool.submit(() -> handle(null, true)).get());
            System.out.println("Request 3 (after remove): " + pool.submit(() -> handle(null, true)).get());
        }
    }
}

Output

Request 1 (no cleanup):   saw leftover user: null
Request 2 (anonymous):    saw leftover user: ravi
Request 3 (after remove): saw leftover user: null

Always Close Resources

Example

try-with-resources closes in reverse order, even when an exception is thrown.

public class Main {
    public static void main(String[] args) {
        try (Res db = new Res("db-connection"); Res file = new Res("report-file")) {
            System.out.println("Working with " + db.name + " and " + file.name);
            throw new IllegalStateException("query failed");
        } catch (IllegalStateException e) {
            System.out.println("Handled: " + e.getMessage());
        }
    }
}

class Res implements AutoCloseable {
    final String name;
    Res(String name) { this.name = name; System.out.println("open  " + name); }
    @Override public void close() { System.out.println("close " + name); }
}

Output

open  db-connection
open  report-file
Working with db-connection and report-file
close report-file
close db-connection
Handled: query failed