One process, many memory areas
The problem. A Java service in a pod is killed with exit 137 while every "heap" number the developers show you looks fine. The heap is only part of a Java process's memory, and the kernel counts all of it. To argue with evidence you need to know what else is in there.
What you need to know already: processes, RSS and VSZ (3.1, 5.1), the OOM killer and cgroup limits (5.7, 5.11), the JVM-in-a-container trap (5.13), threads as NLWP (3.1), requests/limits and OOMKilled in Kubernetes (17.1, 17.6).
Java in five sentences (you do not need to write any):
- A Java program is written as classes (blueprints: the code and the fields of a kind of thing); at run time the program creates objects (instances of a class: an order, a request, a string) in memory.
- The compiler turns the source into bytecode - instructions for an imaginary machine - packed with its libraries into a .jar file (a zip of classes).
- The JVM (Java Virtual Machine, the
javaprogram) runs that bytecode: first by interpreting it, then the JIT (just-in-time compiler) compiles the busy parts into real machine code while the program runs. - Objects live in the heap, one big memory area the JVM manages. The program never frees objects; a garbage collector (GC) finds objects nothing uses any more (garbage) and reclaims their memory, sometimes pausing the program to do so.
- Each thread (one line of execution inside the process - a request being served, a background job) has its own stack for its local variables; the
NLWPcolumn ofpscounts them.
The JDK (Java Development Kit) is the JVM plus the developer and diagnostic tools (jcmd, jstat...); the JRE is just the runtime. HotSpot is the name of the standard JVM, and OpenJDK the open-source build of it you install with apt.
A JVM is a C++ program that happens to run your bytecode. The Java heap - the thing -Xmx limits - is one of its allocations. The kernel, ps and the cgroup see the whole process, and so does the OOM killer.
the process (RSS)
├── Java heap -Xmx / MaxRAMPercentage your objects
│ ├── young generation eden + 2 survivor spaces new objects
│ └── old generation objects that survived long-lived data
├── Metaspace unbounded by default class metadata
├── Compressed class space 1 GB reserved, little used class pointers
├── Thread stacks ThreadStackSize per thread one per thread
├── Code cache 240 MB reserved JIT-compiled code
├── GC data structures G1: a few % of the heap remembered sets, card tables
├── Direct / off-heap buffers MaxDirectMemorySize (= -Xmx!) NIO, Netty, gRPC
├── Symbols, strings table a few tens of MB interned names
└── malloc arenas, libs glibc, JNI, libzip outside the JVM's own count
On this box: /opt/app/orders.jar runs with -Xmx512m. Look at what the kernel says it uses:
$ ps -o pid,rss,vsz,nlwp,cmd -p $(pgrep -f orders.jar)
PID RSS VSZ NLWP CMD
1210 610962 3404344 56 /usr/bin/java -Xmx512m -jar /opt/app/orders.jar
RSS is in KB: 597 MB resident for a 512 MB heap that is not even full. VSZ (3.4 GB) is address space the JVM reserved - mostly the 1 GB compressed class space and the code cache reservation - and costs nothing until touched. NLWP is the number of threads: 56.
The heap itself, from inside the JVM:
# jcmd ships with the JDK: installed in lesson 3 of this chapter
sudo jcmd 1210 GC.heap_info
1210:
garbage-first heap total 370688K, used 252111K [0x00000000e0000000, 0x0000000100000000)
region size 1024K, 47 young (48128K), 3 survivors (3072K)
Metaspace used 98304K, committed 99602K, reserved 1213714K
class space used 13312K, committed 13568K, reserved 1048576K
Read it line by line:
garbage-first heap- the collector is G1.total 370688K- committed heap: 362 MB the JVM has asked the OS for. It grows up to-Xmxas needed and G1 rarely gives it back.used 252111K- bytes in live objects plus garbage not yet collected. Used is not "live": a heap that is 90% used just before a young GC is normal.- the two hex numbers are the reserved address range:
0xe0000000to0x100000000is exactly 512 MB - that is-Xmx. region size 1024K- G1 splits the heap into equal regions (1-32 MB, chosen from the heap size).47 youngregions are eden plus survivors.Metaspace used 98304K- 96 MB of class metadata. Not in the heap, not limited by-Xmx, and a Spring Boot app with 16 000 classes needs it.
So the arithmetic for this process is roughly:
Java heap committed 362 MB
Metaspace + class space 110 MB
Thread stacks (56 threads) 17 MB (touched pages, not the 2 MB reserved each)
Code cache 45 MB
GC structures 22 MB
Symbols, internal, other 35 MB
-------
~590 MB = the RSS above
The number that surprises people is how much is not heap: ~230 MB here. A bigger Spring app with more threads, Netty buffers and 25 000 classes easily carries 400 MB of non-heap.
Heap regions and object lifetime
The generational hypothesis: most objects die young. A request allocates strings, DTOs and buffers, and nearly all of them are garbage by the time the response is written. So the heap is split:
young generation
eden every new object is born here
survivor 0/1 objects that survived one or more young GCs
old generation objects that survived enough young GCs ("tenuring threshold")
or were too big for young (G1 "humongous": > half a region)
A young GC (minor GC) copies the few live objects out of eden into a survivor space and throws eden away wholesale. It costs time proportional to what survives, not to what was allocated - which is why it is cheap and frequent. Objects that keep surviving are promoted to old.
The old generation is collected less often and at higher cost: G1 marks it concurrently and cleans it with mixed collections; if it cannot keep up it falls back to a Full GC that stops everything. The next lessons read those events in logs.
Why this matters for operations
- The container limit must hold the whole process, not the heap.
-Xmx1gwithMemoryMax=1G(chapter 5) orlimits.memory: 1Giis an OOM kill waiting for the first busy hour. - Rule of thumb: limit = heap x 1.3 to 1.5, then measure (NMT, later in this chapter) and adjust.
- Direct buffers default to the heap size.
MaxDirectMemorySizedefaults to roughly-Xmx, so a Netty-heavy service can take another heap's worth outside the heap before the JVM complains. - Threads cost memory outside the heap. A thread leak kills a pod by RSS while the heap graph stays flat - one of this chapter's incidents.
Where each number lives
ps -o rss,vsz,nlwp -p PID the kernel's view of the process
cat /proc/PID/status | grep -E 'VmRSS|Threads' same, from /proc
cat /sys/fs/cgroup/system.slice/UNIT/memory.current what the cgroup charges
jcmd PID GC.heap_info the heap, from inside
jcmd PID VM.native_memory summary every category (needs NMT on)
The cgroup number (memory.current) is usually a bit higher than RSS: it also counts page cache the process caused, such as log files it wrote. The OOM killer acts on the cgroup number.
Common misreadings
- "Used is 90%, we are about to OOM." No - look at used after a GC.
- "RSS is bigger than -Xmx, so there is a leak." No - that is normal, by 100-400 MB. A leak is RSS or after-GC heap that keeps climbing.
- "VSZ is 3.4 GB, the process is huge." Reserved address space, not memory.
- "Setting -Xms = -Xmx makes it use less." It makes the heap fully committed from the start - more RSS early, fewer resizes later. A choice, not a fix.