ποΈ 29042026 1500
The JVM splits a running program's data across five regions. Where an object lives determines who sees it, when it gets collected, and which OutOfMemoryError fires when something leaks.
| Area | Location | Shared? | Bounded By |
|---|---|---|---|
| Heap | JVM-managed | All threads | -Xmx |
| Stack | Per thread | No | -Xss |
| Metaspace | Native memory | All threads | -XX:MaxMetaspaceSize |
| Code Cache | Native memory | All threads | -XX:ReservedCodeCacheSize |
| Direct Memory | Native memory | All threads | -XX:MaxDirectMemorySize |
Heapβ
Every new MyObject() allocates here. Subdivided by generational GC into young and old β see basics for how.
ββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Heap β
β ββββββββββββββββββββ ββββββββββββββββββββββ β
β β Young Gen β β Old Gen β β
β β ββββββ¬ββββββ¬ββββββ β β β
β β βEdenβ S0 β S1 ββ β β β
β β ββββββ΄ββββββ΄ββββββ β β β
β ββββββββββββββββββββ ββββββββββββββββββββββ β
ββββββββββββββββββββββββββββββββββββββββββββββββββββ
Tuningβ
-Xms/-Xmxβ initial / max heap-Xmnβ young-gen size-XX:NewRatio=2β old/young ratio-XX:SurvivorRatio=8β eden/survivor ratio
Heap fills past max β java.lang.OutOfMemoryError: Java heap space.
Stackβ
One per thread. Each method call pushes a frame:
- Local variables (primitives live here; references point into heap)
- Operand stack (intermediate expression values)
- Return address
Frames pop on method return β no GC needed, lifetime is method-scoped.
Size: -Xss (default ~512 KBβ1 MB). Deep recursion β StackOverflowError.
Thread.start() allocates a fresh stack. 1000 threads Γ 1 MB = 1 GB in stacks alone. Use -Xss256k for high-thread-count servers.
Metaspaceβ
Holds class metadata: Class objects, method bytecode, runtime constant pool, interned strings (since JDK 7+).
Pre-JDK 8: lived in PermGen β a fixed-size heap region. Too many classes β OutOfMemoryError: PermGen space.
JDK 8+: PermGen removed. Metadata moved to native memory. Grows unbounded by default. Bound it: -XX:MaxMetaspaceSize.
Failure: dynamic class generation (CGLIB proxies, classloader leaks) without bound β OutOfMemoryError: Metaspace.
Compressed Class Spaceβ
Sub-region of metaspace for compressed class pointers (-XX:+UseCompressedClassPointers, on by default when heap < 32 GB). Stores 32-bit pointers instead of 64-bit β saves ~4 bytes per object header.
-XX:CompressedClassSpaceSize(default 1 GB)- Grows with loaded class count β same root causes as metaspace
- Included within the
-XX:MaxMetaspaceSizelimit - Failure:
OutOfMemoryError: Compressed class space
Code Cacheβ
JIT-compiled native code from HotSpot's C1/C2 compilers lives here.
Default: ~240 MB (JDK 11+). When full, JIT stops compiling β performance drops as new hot methods stay interpreted.
Tuning: -XX:ReservedCodeCacheSize=512m for large apps.
Failure (rare): CodeCache is full. Compiler has been disabled.
Direct (Off-heap) Memoryβ
Allocated outside the JVM heap:
ByteBuffer.allocateDirect(int)β NIO direct buffersUnsafe.allocateMemory(long)β manual native memory- Netty's pooled allocator β see netty_memory_model
- Memory-mapped files (
FileChannel.map)
Why: avoids copying between JVM heap and OS buffers for I/O. NIO socket reads write directly into a direct buffer.
Not bounded by -Xmx. Bounded by -XX:MaxDirectMemorySize (defaults to ~-Xmx). Unset + abused β process eats all host memory β kernel OOM-kill.
Failure: OutOfMemoryError: Direct buffer memory.
Mapped Buffersβ
MappedByteBuffer via FileChannel.map() β OS memory-maps a file region into the process's virtual address space. Reads/writes go through the page cache.
- Not bounded by
-XX:MaxDirectMemorySizeβ OS manages this memory - Common sources: Kafka log segments, RocksDB SST files, Lucene indexes
- High virtual memory (VIRT) from mapped buffers is usually benign β only RSS matters for pressure
- Can't be explicitly unmapped in Java. Persists until the buffer object is GC'd.
Native Memoryβ
Everything else the JVM allocates outside the heap:
- Thread stacks
- JNI allocations
- GC bookkeeping (card tables, remembered sets)
- JIT compiler workspace
Native Memory Tracking (NMT) shows the full breakdown:
java -XX:NativeMemoryTracking=summary ...
jcmd <pid> VM.native_memory summary
Use when RSS exceeds -Xmx + metaspace + direct memory and you need to find where.
Where Data Livesβ
| Data | Area |
|---|---|
Local primitive (int x = 5) | Stack frame |
| Local object reference | Stack frame (reference); heap (object) |
new object | Heap |
String literals | String pool (heap, since JDK 7) |
Class<?> objects | Metaspace |
| JIT-compiled native code | Code Cache |
ByteBuffer.allocateDirect | Direct memory |
FileChannel.map | Mapped buffers (OS-managed) |
Common Pitfallsβ
OOM: heap vs metaspace confusionβ
Different leaks, different fixes. Heap = object retention. Metaspace = class loading.
RSS exceeds -Xmxβ
Metaspace, code cache, direct memory, thread stacks all live outside the heap. Container limits should be -Xmx + ~1β2 GB. Use -XX:MaxRAMPercentage=75 instead of hardcoding -Xmx.
Unbounded thread poolsβ
Executors.newCachedThreadPool β unbounded threads Γ stack size = surprise OOM-kill. See thread_pool_executor_internals.
Direct buffer leakβ
Netty / NIO buffers held by reference too long. Shows up as OOM: Direct buffer memory weeks into production.
Classloader leakβ
JEE/Tomcat redeploy without proper teardown. Old classloaders pinned β old classes stuck in metaspace. Classic "metaspace OOM after the 50th deploy."
Referencesβ
- JVM Specification ch. 2
- "Java Performance" 2nd ed. (Oaks) ch. 5β6
- NMT β JEP 247