Careeroza
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:
| Cause | Fix |
|---|---|
| Static collections / unbounded caches that only grow | bounded cache (Caffeine maximumSize, TTL), WeakHashMap |
| Listeners registered and never removed | unsubscribe in a close()/onDestroy(); weak listeners |
ThreadLocal in thread pools never cleared | try { 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 session | store 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