# Memory and GC Optimization for Java

This guide covers JVM memory layout, garbage collector selection and tuning, escape analysis, allocation patterns, memory leak detection, and off-heap memory management. For allocation categorization and profiling workflows, see the memory agent prompt.

## JVM Memory Model

### Heap layout

The JVM heap is divided into generations based on object lifetime:

| Region | What lives there | Collected by | Typical fraction |
|--------|-----------------|-------------|-----------------|
| **Eden** | Newly allocated objects | Minor GC (young gen collection) | ~60% of young gen |
| **Survivor spaces (S0, S1)** | Objects that survived 1+ minor GCs | Minor GC | ~40% of young gen (split into two equal-sized spaces) |
| **Old generation (tenured)** | Long-lived objects promoted from survivor | Major/mixed/full GC | 2/3 of total heap |

Objects start in Eden. If they survive a minor GC, they move to a survivor space. After surviving `-XX:MaxTenuringThreshold` (default 15) minor GCs, they are promoted to old gen.

### Non-heap memory regions

| Region | Purpose | Default size | Key flags |
|--------|---------|-------------|-----------|
| **Metaspace** | Class metadata, method bytecode, constant pools | Unbounded (grows as needed) | `-XX:MaxMetaspaceSize`, `-XX:MetaspaceSize` (initial high-water mark) |
| **Code cache** | JIT-compiled native code | 48 MB (tiered) / 240 MB (segmented, JDK 9+) | `-XX:ReservedCodeCacheSize`, `-XX:InitialCodeCacheSize` |
| **Thread stacks** | Per-thread call stack frames | 512 KB - 1 MB per thread | `-Xss` (stack size per thread) |
| **Direct memory** | `DirectByteBuffer`, `MappedByteBuffer` | Same as `-Xmx` | `-XX:MaxDirectMemorySize` |
| **Native memory** | JNI allocations, NIO buffers, JVM internals | Varies | Track with NMT: `-XX:NativeMemoryTracking=summary` |

### Total process memory

Process RSS = Heap + Metaspace + Code Cache + (Thread Count * Stack Size) + Direct Memory + Native Memory + GC overhead structures.

A JVM with `-Xmx4g` may consume 5-6 GB of RSS due to non-heap regions. Always account for this when sizing containers.

## GC Algorithms

### G1GC (Garbage-First) -- Default since JDK 9

**How it works**: Divides the heap into equal-sized regions (~1-32 MB each). Tracks which regions have the most garbage ("garbage first") and collects those first during mixed collections. Young gen collections are stop-the-world but fast. Mixed collections reclaim old gen regions incrementally.

**When to use**: General-purpose default. Good balance of throughput and latency for heaps from 256 MB to 64 GB. Best when you need predictable pause times.

**Key characteristics**:
- Region-based (no contiguous young/old boundary)
- Concurrent marking phase runs alongside application threads
- Mixed collections reclaim old gen incrementally (avoids full GC)
- Humongous objects (>50% of region size) get special treatment -- allocated in contiguous regions, collected eagerly

**Key tuning parameters**:

| Flag | Default | What it does |
|------|---------|-------------|
| `-XX:MaxGCPauseMillis` | 200 | Target max pause time. G1 adjusts young gen size to meet this. Lower = smaller young gen = more frequent but shorter pauses. |
| `-XX:G1HeapRegionSize` | Auto (1-32 MB) | Region size. Set explicitly to avoid humongous allocation for objects >50% of region. Power of 2 between 1 MB and 32 MB. |
| `-XX:InitiatingHeapOccupancyPercent` (IHOP) | 45 | Old gen occupancy that triggers concurrent marking. Lower = starts marking earlier = less risk of full GC, but more CPU for marking. |
| `-XX:G1MixedGCLiveThresholdPercent` | 85 | Regions with more than this % live data are not collected (not worth copying). Lower = more regions collected = longer pauses but more reclaimed. |
| `-XX:G1NewSizePercent` | 5 | Min young gen as % of heap. |
| `-XX:G1MaxNewSizePercent` | 60 | Max young gen as % of heap. |
| `-XX:G1ReservePercent` | 10 | Reserve heap % to reduce evacuation failures. |
| `-XX:ConcGCThreads` | Auto | Concurrent marking threads. Default is ~1/4 of ParallelGCThreads. |

**Humongous objects**: Objects larger than 50% of the G1 region size are allocated as "humongous" -- they span multiple contiguous regions, bypass young gen, and are harder to collect. If JFR shows frequent humongous allocations:
- Increase `-XX:G1HeapRegionSize` so the object fits within one region
- Or reduce the object size (e.g., chunk large arrays)

### ZGC (Z Garbage Collector) -- Production since JDK 15

**How it works**: Uses colored pointers (metadata in pointer bits) and load barriers to perform all GC work concurrently. Application threads are paused only for very short root scanning (typically <1 ms regardless of heap size).

**When to use**: When sub-millisecond pause times are critical (real-time systems, financial trading, latency-sensitive services). Handles heaps from 8 MB to 16 TB.

**Key characteristics**:
- Sub-millisecond pauses (typically <1 ms, rarely >2 ms)
- Concurrent relocation (objects move while application runs)
- No generational distinction (JDK 21+ has generational ZGC as default: `-XX:+UseZGC`)
- Higher memory overhead (~3-5% more than G1 due to colored pointers)
- Higher CPU overhead (~5-15% more than G1 for concurrent work)

**Key tuning parameters**:

| Flag | Default | What it does |
|------|---------|-------------|
| `-XX:+UseZGC` | Off | Enable ZGC |
| `-XX:+ZGenerational` | On (JDK 21+) | Enable generational mode. Significantly better throughput. |
| `-XX:SoftMaxHeapSize` | Same as -Xmx | Soft limit -- ZGC tries to stay below this but can exceed it. Useful for container-friendly behavior. |
| `-XX:ZUncommitDelay` | 300 (seconds) | Time before uncommitting unused heap back to OS. Lower = faster memory return. |
| `-XX:ZAllocationSpikeTolerance` | 2.0 | How proactive GC is about allocation spikes. Higher = more eager concurrent collection. |

### Shenandoah GC -- Available in OpenJDK

**How it works**: Uses Brooks forwarding pointers (an extra pointer per object) to relocate objects concurrently. Similar goals to ZGC (low pause times) but with a different implementation.

**When to use**: Similar use case to ZGC. Some workloads perform better on Shenandoah, others on ZGC. Benchmark both if sub-ms pauses are required.

**Key characteristics**:
- Sub-millisecond pauses (comparable to ZGC)
- Brooks pointer adds 8 bytes per object (memory overhead)
- Available in Red Hat builds and OpenJDK (not in Oracle JDK)
- Concurrent evacuation without colored pointers

**Key tuning parameters**:

| Flag | Default | What it does |
|------|---------|-------------|
| `-XX:+UseShenandoahGC` | Off | Enable Shenandoah |
| `-XX:ShenandoahGCHeuristics` | adaptive | Heuristic mode: adaptive, static, compact, aggressive |
| `-XX:ShenandoahMinFreeThreshold` | 10 | Start collection when free heap drops below this %. |
| `-XX:ShenandoahUncommitDelay` | 5000 (ms) | Time before uncommitting unused heap. |

### Parallel GC (throughput collector)

**When to use**: Batch processing, offline analytics, any workload where total throughput matters more than individual pause times. Uses all CPUs for GC work but stops all application threads during collection.

| Flag | What it does |
|------|-------------|
| `-XX:+UseParallelGC` | Enable (default in JDK 8) |
| `-XX:ParallelGCThreads` | Number of GC threads (default: number of CPUs) |
| `-XX:MaxGCPauseMillis` | Soft target for max pause |
| `-XX:GCTimeRatio` | Target throughput: app_time / gc_time. Default 99 means 1% GC overhead target. |

### Serial GC

**When to use**: Single-core machines, tiny heaps (<100 MB), or containers with 1 CPU. Minimal overhead but stops everything during collection.

| Flag | What it does |
|------|-------------|
| `-XX:+UseSerialGC` | Enable |

### GC selection decision tree

```
Is sub-millisecond pause time required?
├── YES: Use ZGC (or Shenandoah on OpenJDK)
└── NO: Is maximum throughput the priority (batch/offline)?
    ├── YES: Use Parallel GC
    └── NO: Is heap < 100 MB or single CPU?
        ├── YES: Use Serial GC
        └── NO: Use G1GC (default)
```

## Escape Analysis

Escape analysis (EA) is a JIT optimization that determines whether an object can be allocated on the stack instead of the heap. Stack-allocated objects are freed instantly when the method returns -- no GC needed.

### What EA does

1. **Scalar replacement**: Object fields are promoted to local variables (no object allocated at all)
2. **Stack allocation**: Object allocated on the stack frame (freed on method return)
3. **Lock elision**: If the object doesn't escape, synchronized blocks on it are removed

### When EA succeeds

```java
// EA can scalar-replace this -- Point never escapes the method
public double distance(double x1, double y1, double x2, double y2) {
    Point p = new Point(x2 - x1, y2 - y1);  // scalar-replaced: dx and dy become local variables
    return Math.sqrt(p.x * p.x + p.y * p.y);
}
```

### When EA fails

| Failure mode | Example | Fix |
|-------------|---------|-----|
| **Stored to field** | `this.lastResult = new Result(...)` | Reuse existing object or accept heap allocation |
| **Passed to non-inlined method** | `process(new Wrapper(data))` where `process` is too large to inline | Break `process` into smaller methods the JIT can inline |
| **Stored in array** | `results[i] = new Item(...)` | Use parallel primitive arrays instead of object array |
| **Returned from method** | `return new Pair(a, b)` | Caller must be inlined too for EA to work on the chain |
| **Used in synchronized block** | `synchronized(new Object()) { ... }` | Use a dedicated lock field |
| **Object too large** | Large arrays or objects with many fields | EA has size limits; split into smaller objects |

### Diagnosing EA failures

```bash
# Print escape analysis results
java -XX:+PrintEscapeAnalysis -XX:+PrintEliminateAllocations ...

# In JFR: look for high allocation rates in methods that should be EA-friendly
# jdk.ObjectAllocationInNewTLAB events in methods with small, short-lived objects
```

### Writing EA-friendly code

1. Keep hot methods small (under inlining threshold)
2. Avoid storing temporary objects in fields or arrays
3. Prefer returning primitives or using out-parameters over returning new objects
4. Avoid `synchronized` on temporary objects
5. Prefer final fields -- they help the JIT prove more properties

## Object Allocation

### TLAB allocation (fast path)

Each thread has a Thread-Local Allocation Buffer (TLAB) -- a private chunk of Eden. Allocating in a TLAB is just a pointer bump (a few nanoseconds). No synchronization needed.

- **TLAB allocation** (~5 ns): Object fits in thread's TLAB. Pointer bump, zero synchronization.
- **TLAB refill** (~50-100 ns): TLAB is full. Thread gets a new TLAB from Eden. Atomic CAS on Eden pointer.
- **Outside-TLAB allocation** (~100-500 ns): Object doesn't fit in any TLAB (large object). Allocated directly in Eden or old gen with synchronization.

### Object header overhead

Every Java object has a header:
- **Mark word**: 8 bytes (hash code, GC age, lock state)
- **Class pointer**: 4 bytes (compressed oops) or 8 bytes (no compressed oops)
- **Array length**: 4 bytes (only for arrays)
- **Padding**: Aligned to 8 bytes

**Minimum object size**: 16 bytes (even an empty `new Object()`).

For an `Integer`: 16 bytes (12-byte header + 4-byte int field). For an `int`: 4 bytes. That is 4x overhead for boxing.

### Right-sizing collections

Avoid default initial capacities when you know the expected size:

```java
// HashMap: capacity = expectedSize / 0.75 + 1
Map<K, V> map = new HashMap<>(expectedSize * 4 / 3 + 1);

// ArrayList: exact expected size
List<Item> list = new ArrayList<>(expectedSize);

// StringBuilder: estimated character count
StringBuilder sb = new StringBuilder(estimatedLength);
```

## Memory Leak Patterns

### ClassLoader leaks

**Symptom**: Metaspace grows without bound; `OutOfMemoryError: Metaspace`.

**Cause**: A classloader is retained by a reference chain (often a static field in a loaded class, a ThreadLocal, or a JDBC driver registration). All classes loaded by that classloader -- and all their static fields -- are retained.

**Common sources**: Webapp hot redeployment (old webapp classloader retained), JDBC drivers not deregistered, logging frameworks caching per-classloader state.

**Detection**: Heap dump -> find instances of `ClassLoader` subclasses -> check GC roots and retainer chains.

### ThreadLocal leaks

**Symptom**: Heap grows over time in thread-pooled applications.

**Cause**: Thread pool reuses threads. If a `ThreadLocal` value is set but never removed, the value lives as long as the thread (which may be forever in a pool).

```java
// LEAK: ThreadLocal value retained for thread's lifetime
private static final ThreadLocal<ExpensiveObject> context = new ThreadLocal<>();

public void handleRequest() {
    context.set(new ExpensiveObject());
    process();
    // BUG: no context.remove() -- ExpensiveObject retained until thread dies
}

// FIX: always remove in finally
public void handleRequest() {
    context.set(new ExpensiveObject());
    try {
        process();
    } finally {
        context.remove();
    }
}
```

### Listener / callback leaks

**Symptom**: Objects that should be GC'd are retained because they are registered as listeners or callbacks.

**Cause**: Event source holds a strong reference to the listener. If the listener's owner is discarded but the listener isn't unregistered, the entire object graph reachable from the listener is retained.

**Fix**: Unregister listeners explicitly, or use weak references for listener registration.

### Static collection growth

**Symptom**: A static `Map` or `List` grows without bound.

```java
// LEAK: entries never removed
private static final Map<String, CachedResult> cache = new ConcurrentHashMap<>();

public CachedResult lookup(String key) {
    return cache.computeIfAbsent(key, k -> computeExpensive(k));
    // Cache grows forever -- no eviction
}

// FIX: use a bounded cache
private static final Cache<String, CachedResult> cache = Caffeine.newBuilder()
    .maximumSize(10_000)
    .expireAfterWrite(Duration.ofMinutes(10))
    .build();
```

### Soft/weak reference misuse

**Symptom**: Frequent full GCs. Soft references are cleared only under memory pressure, causing the JVM to oscillate between filling the heap and doing full GC to clear soft references.

**Fix**: Do not use `SoftReference` as a general caching mechanism. Use a bounded cache (Caffeine, Guava Cache) with explicit size limits and TTL. Use `WeakReference` only when the reference should not prevent GC (e.g., canonicalization maps).

## Heap Dump Analysis

### Capturing heap dumps

```bash
# Live heap dump (triggers full GC first)
jmap -dump:live,format=b,file=/tmp/heap.hprof <pid>

# Heap dump on OOM (add to JVM flags)
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/

# JFR allocation profiling (lightweight, continuous)
jcmd <pid> JFR.start name=alloc settings=profile duration=60s filename=/tmp/alloc.jfr

# Histogram (quick -- no full dump needed)
jmap -histo <pid> | head -30
jmap -histo:live <pid> | head -30  # forces full GC, shows only reachable objects
```

### Analysis with Eclipse MAT (Memory Analyzer Tool)

1. **Dominator tree**: Shows which objects retain the most memory. The dominator of an object is the last object on all paths from GC root to that object.
2. **Leak suspects report**: Automated analysis that identifies objects retaining disproportionate memory.
3. **Histogram**: Object counts and sizes by class. Compare live vs total to identify objects awaiting GC.
4. **Path to GC roots**: For a suspect object, shows the reference chain keeping it alive.
5. **OQL (Object Query Language)**: SQL-like queries on the heap: `SELECT * FROM java.lang.String s WHERE s.value.length > 10000`

### JFR allocation profiling

More production-friendly than heap dumps -- low overhead, continuous monitoring:

```bash
# In-flight allocation profiling
jcmd <pid> JFR.start name=alloc settings=profile duration=60s filename=/tmp/alloc.jfr
```

Key JFR events:
- `jdk.ObjectAllocationInNewTLAB`: Object allocated when TLAB was refilled. Shows allocation hotspots.
- `jdk.ObjectAllocationOutsideTLAB`: Large object allocated outside TLAB. These are expensive.
- `jdk.OldObjectSample`: Objects that survived to old gen. Shows what's being promoted.

## Off-Heap Memory

### DirectByteBuffer

Used by NIO channels and many high-performance libraries (Netty, LMDB, RocksDB). Allocated outside the Java heap, not subject to GC pauses.

```java
// Allocate 64 MB direct buffer
ByteBuffer buf = ByteBuffer.allocateDirect(64 * 1024 * 1024);

// Limit total direct memory
// JVM flag: -XX:MaxDirectMemorySize=256m
```

**Lifecycle issue**: DirectByteBuffer is freed when its Java wrapper object is GC'd. If GC is infrequent (large heap, low allocation rate), direct buffers can accumulate and cause `OutOfMemoryError: Direct buffer memory`.

**Fix**: Pool direct buffers and reuse them, or call `((sun.misc.Cleaner) ((DirectBuffer) buf).cleaner()).clean()` to free immediately (internal API, use with care).

### MappedByteBuffer

Memory-mapped files. The OS maps file pages into process address space. Reads/writes go through the page cache -- no system call overhead after the initial mapping.

```java
FileChannel channel = FileChannel.open(path, StandardOpenOption.READ);
MappedByteBuffer mapped = channel.map(FileChannel.MapMode.READ_ONLY, 0, channel.size());
```

**Caution**: `MappedByteBuffer` cannot be explicitly unmapped in standard Java (no `unmap()` method). The mapping persists until GC collects the buffer. On Windows, this can prevent file deletion.

### Native Memory Tracking (NMT)

```bash
# Enable NMT (add to JVM flags -- ~5% overhead)
-XX:NativeMemoryTracking=summary    # or =detail for full tracking

# Query NMT
jcmd <pid> VM.native_memory summary

# Compare snapshots (track growth over time)
jcmd <pid> VM.native_memory baseline
# ... wait ...
jcmd <pid> VM.native_memory summary.diff
```

NMT output shows memory by category: Java Heap, Class, Thread, Code, GC, Internal, Symbol, Native Memory Tracking overhead. Useful for diagnosing non-heap memory growth.

## Object Pooling

### When object pooling helps

- Objects are expensive to create (network connections, compiled regex, large buffers)
- Objects are used briefly then discarded (thread-per-request patterns)
- Allocation rate causes GC pressure that is measurable in profiling

### When object pooling hurts

- Objects are cheap to create (small POJOs, wrappers)
- Pool management adds complexity and potential bugs (returning corrupted objects, forgetting to return)
- Modern GCs handle short-lived objects extremely well -- Eden collection is essentially free for objects that die young
- Pool contention under high concurrency can negate the benefit

### Rule of thumb

Profile first. If JFR shows a specific object type as a dominant allocator AND the allocation rate is causing measurable GC pauses, consider pooling. Otherwise, let the GC do its job. Modern GC (G1, ZGC) is specifically designed to handle high allocation rates efficiently.

**Connection pools** (HikariCP, DBCP) are the classic justified case -- connection creation involves network round-trips, TLS handshakes, and authentication. Always pool connections.

**Thread pools** (ThreadPoolExecutor, ForkJoinPool) are another justified case -- thread creation involves OS kernel calls and stack allocation.

**Byte buffer pools** (Netty's PooledByteBufAllocator) are justified for high-throughput I/O -- avoiding repeated large allocation and zeroing.

**General object pools** for small business objects are almost never justified on modern JVMs.
