Master Java memory management. Learn heap optimization, garbage collection tuning, and patterns to eliminate memory leaks that crash production.
JOptimize Team
Scenario: Your microservice was running fine in staging. Then it hit production and crashed after 48 hours with OutOfMemoryError: Java heap space.
Root cause: Memory leak in a third-party library you didn't write. A collection that grows indefinitely, holding references to objects that should be garbage collected.
The cost:
This happens to thousands of Java teams every month. And most could have been prevented with proper memory management.
The Java heap is where objects live:
When memory fills up, the GC pauses your app and reclaims unused memory:
Young GC (fast): 100ms pause, 50% of young gen freed Full GC (slow): 5000ms pause, entire heap scanned
Problem: If your app is leaking memory, even aggressive GC can't reclaim it. The heap fills up. Game over.
JOptimize detects this automatically
Scan your Java project in 30 seconds — free, no credit card.
// ❌ LEAK: Static List grows every request public class UserCache { private static List<User> cache = new ArrayList<>(); public static void addUser(User u) { cache.add(u); // Never removed = leak } }
Cost: 1000 requests/day → 365k users/year in memory → OOM in weeks
Fix: Use a bounded cache or weak references
// ✅ CORRECT: Bounded LRU cache private static final int MAX_SIZE = 1000; private static Map<String, User> cache = new LinkedHashMap<String, User>(MAX_SIZE, 0.75f, true) { protected boolean removeEldestEntry(Map.Entry eldest) { return size() > MAX_SIZE; } };
// ❌ LEAK: Listener registered but never removed public class MyComponent { public MyComponent(EventBus bus) { bus.subscribe(this); // What about unsubscribe? } }
Cost: UI component destroyed but still in EventBus listeners → GC can't free it
Fix: Unregister in destructor
@Override protected void onDestroy() { bus.unsubscribe(this); super.onDestroy(); }
// ❌ LEAK: FileInputStream never closed public byte[] readFile(String path) { FileInputStream fis = new FileInputStream(path); return IOUtils.toByteArray(fis); // fis never closed = handle leak }
Cost: 100 file reads → 100 open file descriptors → "Too many open files" error
Fix: Use try-with-resources
// ✅ CORRECT: Auto-closed public byte[] readFile(String path) { try (FileInputStream fis = new FileInputStream(path)) { return IOUtils.toByteArray(fis); } }
// ❌ LEAK: ThreadLocal never cleaned up public class RequestContext { private static ThreadLocal<User> user = new ThreadLocal<>(); public static void setUser(User u) { user.set(u); } }
Cost: Thread pool (50 threads) → each thread holds ThreadLocal → memory leak in app server
Fix: Always remove
@Override public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) { try { User user = extractUser(req); RequestContext.setUser(user); chain.doFilter(req, res); } finally { RequestContext.remove(); // Critical! } }
// ❌ LEAK: A holds B, B holds A → neither GC'd class A { B b = new B(this); } class B { A a; B(A a) { this.a = a; } }
Fix: Use weak references
class B { WeakReference<A> a; B(A a) { this.a = new WeakReference<>(a); } }
// ❌ LEAK: Large array stays in memory until method returns public void processData() { byte[] buffer = new byte[100_000_000]; // 100MB processSmallData(buffer); doOtherWork(); // buffer still in scope = can't GC }
Fix: Explicit scope
public void processData() { { byte[] buffer = new byte[100_000_000]; processSmallData(buffer); } // buffer goes out of scope here doOtherWork(); // buffer can be GC'd }
// ❌ LEAK: Interned strings live in permgen forever for (String s : millionStrings) { s.intern(); // Never garbage collected }
Fix: Only intern controlled strings
// ✅ CORRECT: Only intern pool of known strings private static final Set<String> POOL = new HashSet<>(); public static String intern(String s) { return POOL.stream() .filter(s::equals) .findFirst() .orElseGet(() -> { POOL.add(s); return s; }); }
# Enable heap dump on OOM java -XX:+HeapDumpOnOutOfMemoryError \ -XX:HeapDumpPath=/var/dumps \ -Xmx4g MyApp
# Enable GC logging java -Xlog:gc*:file=gc.log:time,level,tags \ -Xmx4g MyApp
Look for:
JOptimize scans for:
# Conservative: 30% of system RAM java -Xms2g -Xmx2g # Aggressive: 50% of system RAM (monitor GC) java -Xms4g -Xmx4g # Never: 90% (no OS memory = crash)
| GC | Throughput | Latency | Use Case |
|---|---|---|---|
| Serial | 95% | High | Small apps (<1GB) |
| Parallel | 99% | High | Batch processing |
| G1 | 99% | Medium | 4-40GB heaps |
| ZGC | 99% | Ultra-low | Real-time apps |
# Default: 1/3 of heap # Tune: -Xmn for young gen only java -Xmx4g -Xmn1g # 1GB young, 3GB old
Effect: Smaller young gen = less young GC overhead. Larger young gen = fewer promotions to old gen.
Heap: 4GB Young Gen: 1.3GB (default) GC Pause: 200ms (young), 3000ms (full) Full GC Frequency: Every 30 minutes Outages: 2-3/month from OOM
Heap: 4GB Young Gen: 800MB (tuned) GC Pause: 50ms (young), 500ms (full) Full GC Frequency: Every 6 hours Outages: 0/month
Impact: 6x fewer full GCs, 10x fewer outages, 99.99% uptime.
✅ Design Phase
✅ Implementation
✅ Testing
✅ Production
-XX:+HeapDumpOnOutOfMemoryErrorJOptimize analyzes for:
Detection accuracy: 94% of common patterns caught pre-deployment.
Memory leaks aren't magic—they're patterns. Once you know the patterns, you can:
Result: Stable, reliable applications that don't crash at 3 AM.
Start detecting memory leaks today: joptimize.io/memory
JOptimize finds N+1 queries, EAGER collections, and 70+ other issues in your Java codebase — in under 30 seconds.