Back to Blog
Java memorygarbage collectionheap optimizationmemory leaksOutOfMemoryErrorperformance tuning

Java Memory Management: Best Practices to Prevent OutOfMemoryError

Master Java memory management. Learn heap optimization, garbage collection tuning, and patterns to eliminate memory leaks that crash production.

J

JOptimize Team

September 30, 2026· 15 min read

The Memory Crisis: Why Java Apps Crash in Production

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:

  • 2-hour outage during peak business hours
  • $500k in lost revenue
  • Emergency page-out at 3 AM
  • Weekend spent debugging production logs

This happens to thousands of Java teams every month. And most could have been prevented with proper memory management.


How Java Memory Works (The Short Version)

Heap: The Danger Zone

The Java heap is where objects live:

  • Young Generation (Eden, S0, S1): New objects, fast collection
  • Old Generation: Objects that survived multiple collections
  • Metaspace: Class definitions and metadata

Garbage Collection: The Cleanup Crew

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.

Try free →

The 7 Most Common Memory Leaks

1. Static Collections That Grow Unbounded

// ❌ 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; } };

2. Event Listeners Never Unregistered

// ❌ 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(); }

3. Streams and Resources Not Closed

// ❌ 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); } }

4. Thread-Local Variables

// ❌ 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! } }

5. Circular References with Weak Pointers

// ❌ 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); } }

6. Large Objects in Method Scope

// ❌ 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 }

7. String.intern() Abuse

// ❌ 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; }); }

Detecting Memory Leaks: Tools & Techniques

1. JVM Flags for Memory Dumps

# Enable heap dump on OOM java -XX:+HeapDumpOnOutOfMemoryError \ -XX:HeapDumpPath=/var/dumps \ -Xmx4g MyApp

2. Heap Analysis with JProfiler

  1. Run app with profiler attached
  2. Wait for symptoms (high memory usage)
  3. Take heap dump
  4. Analyze: "What objects are keeping memory alive?"
  5. Trace back to the leak source

3. GC Logs

# Enable GC logging java -Xlog:gc*:file=gc.log:time,level,tags \ -Xmx4g MyApp

Look for:

  • Full GC every few minutes → Memory filling up
  • GC pauses > 1 second → App unresponsive
  • Young gen size growing → Old gen leak

4. Automated Detection (JOptimize)

JOptimize scans for:

  • Static collections without bounds
  • Listeners never unregistered
  • Try-with-resources violations
  • ThreadLocal without cleanup

Memory Tuning: From Bloated to Lean

Heap Size Configuration

# 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 Algorithm Selection

GCThroughputLatencyUse Case
Serial95%HighSmall apps (<1GB)
Parallel99%HighBatch processing
G199%Medium4-40GB heaps
ZGC99%Ultra-lowReal-time apps

Young Gen Sizing

# 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.


Real Numbers: Memory Optimization Impact

Before Memory Tuning

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

After Memory Tuning + Leak Fixes

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.


Memory Best Practices Checklist

✅ Design Phase

  • Bound all caches (LRU, TTL, or max size)
  • Plan object lifecycle (when created, when destroyed)
  • Identify long-lived objects (singletons, caches, pools)

✅ Implementation

  • Use try-with-resources for all streams
  • Unregister listeners/callbacks
  • Clean up ThreadLocal in finally blocks
  • Avoid String.intern() except for known pools

✅ Testing

  • Load tests: 1 million operations → check memory growth
  • Profile with JProfiler: find heap dominators
  • Monitor GC: set up alerts for frequent full GCs

✅ Production

  • Enable heap dumps: -XX:+HeapDumpOnOutOfMemoryError
  • Log GC times: track pause durations
  • Alert on high memory: > 80% heap usage

Why JOptimize Catches Memory Leaks

JOptimize analyzes for:

  • Static collections without size limits
  • Listeners registered without cleanup
  • ThreadLocal without removal
  • Files/streams without try-with-resources
  • Circular references with strong pointers

Detection accuracy: 94% of common patterns caught pre-deployment.


The Bottom Line

Memory leaks aren't magic—they're patterns. Once you know the patterns, you can:

  1. Avoid them during development
  2. Detect them during testing
  3. Fix them before production

Result: Stable, reliable applications that don't crash at 3 AM.

Start detecting memory leaks today: joptimize.io/memory

Detect issues in your project

JOptimize finds N+1 queries, EAGER collections, and 70+ other issues in your Java codebase — in under 30 seconds.