Heap, Stack and Metaspace in Java
Where each piece of data actually lives, and what runs out when each area is exhausted.
-
Java Basics
- Introduction to Java
- Setting Up Java and Writing Your First Program
- Variables, Data Types and Literals in Java
- Type Casting and Type Conversion in Java
- Operators and Expressions in Java
- Input and Output in Java
- Comments, Keywords and Naming Conventions in Java
- Control Flow in Java: if, else and switch
- Loops in Java: for, while and do-while
- Methods
- Arrays and Strings
-
OOP
- Classes and Objects in Java
- Constructors in Java
- The this Keyword in Java
- The static Keyword in Java
- Encapsulation in Java
- Access Modifiers in Java
- Inheritance in Java
- Method Overriding and super in Java
- Polymorphism in Java
- Abstraction, Abstract Classes and Interfaces in Java
- Composition, Aggregation and Association in Java
- The Object Lifecycle in Java
- Core Java
- Exception Handling
-
Collections
- The Java Collections Framework
- List in Java: ArrayList, LinkedList, Vector and Stack
- Set in Java: HashSet, LinkedHashSet and TreeSet
- Map in Java: HashMap, LinkedHashMap and TreeMap
- How HashMap Works Internally in Java
- Queue and Deque in Java: ArrayDeque and PriorityQueue
- Iterators in Java
- Comparable and Comparator in Java
- Collections Utilities and Choosing the Right Collection
- Generics
- Java 8+
- Stream API
- Date and Time
- File and I/O
-
Multithreading
- Threads in Java: Processes, Runnable and Thread
- Thread Lifecycle in Java
- Synchronization in Java: synchronized and volatile
- Locks and Atomic Classes in Java
- Race Conditions and Deadlocks in Java
- The Executor Framework and Thread Pools in Java
- Future and CompletableFuture in Java
- Concurrent Collections in Java
- The Java Memory Model
- JVM and Memory
- Advanced Java
- Networking
- JDBC
- Testing
The three areas that matter
| Stack | Heap | Metaspace | |
|---|---|---|---|
| Holds | Frames, locals, references | Objects and arrays | Class metadata, static fields |
| Shared | One per thread | All threads | All threads |
| Managed by | Automatic push and pop | The garbage collector | Freed when a loader is collected |
| Sized with | -Xss | -Xms and -Xmx | -XX:MaxMetaspaceSize |
| Exhaustion | StackOverflowError | OutOfMemoryError: Java heap space | OutOfMemoryError: Metaspace |
Following one method
public class Demo {
private static final String APP = "notes"; // metaspace, object on heap
public void run() {
int count = 5; // stack
String label = "total"; // reference stack, object heap
int[] values = new int[3]; // reference stack, array heap
Note note = new Note(label, count); // reference stack, object heap
}
}Thread stack Heap
+-------------------+ +--------------------------+
| run() frame | | "total" (string pool) |
| count = 5 | | int[3] {0,0,0} |
| label --------------------> | Note{title, views} |
| values --------------------> +--------------------------+
| note -------------------->
+-------------------+
Metaspace: Demo class structure, method bytecode, the APP field slotThe stack
public static int depth(int n) {
return depth(n + 1); // no base case
}
// StackOverflowError after tens of thousands of frames- One stack per thread, typically half a megabyte to a megabyte.
- A frame is pushed on every call and popped on return.
- Nothing is garbage collected; the memory is reclaimed by the pop.
- Thread confined, so stack data needs no synchronisation.
Note that thousands of threads consume real memory in stacks alone, which is one of the reasons virtual threads exist.
The heap
+-------------------------------------------------------+
| Young generation | Old generation |
| Eden | S0 | S1 | tenured objects |
+-------------------------------------------------------+- Every object and array is allocated here.
- New objects go into Eden.
- A minor collection copies survivors between the survivor spaces.
- Objects surviving enough collections are promoted to the old generation.
This split exists because of a strong empirical observation: most objects die very young. Collecting a small young generation frequently is far cheaper than scanning the whole heap.
java -Xms1g -Xmx4g -XX:+UseG1GC -jar app.jarMetaspace
Before Java 8 class metadata lived in a fixed size area called PermGen, which overflowed easily in applications that loaded many classes. Metaspace replaced it and is allocated from native memory, growing as needed unless capped.
java -XX:MaxMetaspaceSize=256m -jar app.jarMetaspace is reclaimed only when an entire class loader becomes unreachable. Repeated redeployment in a container is the classic cause of a metaspace leak.
Where a String lives
String a = "java"; // pooled, on the heap
String b = "java"; // the same pooled object
String c = new String("java"); // a separate heap object
String d = c.intern(); // the pooled instance
System.out.println(a == b); // true
System.out.println(a == c); // false
System.out.println(a == d); // trueThe string pool has lived on the normal heap since Java 7, so pooled strings are collected like anything else.
Escape analysis
public int total(int a, int b) {
Point point = new Point(a, b); // may never be allocated at all
return point.x() + point.y();
}If the JIT can prove an object never escapes the method, it may replace it with plain local values. This is a real optimisation, but it depends on inlining and cannot be relied upon; write clear code and let the JVM decide.
Diagnosing memory
jcmd <pid> GC.heap_info
jcmd <pid> GC.class_histogram
jmap -dump:live,format=b,file=heap.hprof <pid>
jstat -gc <pid> 1sjava -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/dumps -jar app.jarEnable the heap dump flag in production. An out of memory error without a dump is very difficult to explain after the fact.
Reading the errors
| Message | Usual cause |
|---|---|
Java heap space | A genuine leak, or a heap too small for the workload |
GC overhead limit exceeded | Collection is running constantly and reclaiming almost nothing |
Metaspace | Too many classes, or a class loader leak |
unable to create native thread | Too many threads, or stacks too large |
Requested array size exceeds VM limit | An array larger than the implementation allows |
Common misconceptions
- Primitives always live on the stack. A primitive field lives inside its object on the heap.
- Setting a reference to null frees memory. It removes one reference; collection happens later, if at all.
- A bigger heap is always better. More heap can mean longer pauses.
- The stack is garbage collected. It is not; frames are popped.
Practice
- For a method with a local
int, aStringand an array, state where each part lives. - Why does creating thousands of platform threads consume so much memory?
- Explain why the heap is split into generations.
- Trigger a
StackOverflowErrorand anOutOfMemoryErrordeliberately and compare the messages. - What condition would cause metaspace to grow without bound?
Conclusion
Frames and references live on per thread stacks, objects live on the shared heap, and class metadata lives in metaspace. Knowing which area an error names is the first step towards diagnosing it.