1. Overview of a Java process’s memory
When you run a Java program, the JVM (Java Virtual Machine) asks the operating system for a chunk of memory. Sometimes it’s modest, sometimes it’s quite large (especially if you launch something like Minecraft with tons of mods). This memory is divided into several key areas, each playing its own role:
- Stack — for local variables and method calls.
- Heap — for all objects you create with new.
- Service areas (PermGen/Metaspace) — for class metadata, static fields, and other “magical” things.
It looks roughly like this:
┌───────────────────────────────┐
│ JVM process │
│ ┌─────────────┐ │
│ │ Stack │ ← Each thread has its own stack!
│ └─────────────┘ │
│ ┌─────────────┐ │
│ │ Heap │ ← Shared by all threads
│ └─────────────┘ │
│ ┌───────────────┐ │
│ │ PermGen/ │ ← Class metadata
│ │ Metaspace │
│ └───────────────┘ │
└───────────────────────────────┘
Why is this important?
- Understanding the memory layout helps you write more efficient and safer code.
- It’s easier to diagnose errors like StackOverflowError or OutOfMemoryError.
- “Garbage collector” and “memory leak” stop being scary words — you understand where and what to look for.
2. Stack: fast, local, but not forever
The stack is a special memory area allocated per thread. The stack is like a pile of plates: the last one you put on is the first you can take off, but only after you remove those above it. In other words, the stack works on a LIFO (Last In, First Out) principle.
What is the stack for?
The stack stores:
- Local variables of methods (for example, int x = 5; inside a method).
- Return address after a method call (to know where to return after the method finishes).
Every time you call a method, a new frame (stack frame) is pushed onto the stack — essentially a box that contains all the local variables of that method and service information. When the method completes, its frame is popped — its local variables disappear.
Example
public static void main(String[] args) {
int a = 10; // a is on main's stack frame
int b = sum(a, 5); // call sum
}
public static int sum(int x, int y) {
int result = x + y; // x, y, result are on sum's stack frame
return result;
}
- When sum is called, a separate frame is created on the stack for it.
- After sum finishes, its variables disappear.
Variable lifecycle
Local variables live only as long as the method in which they’re declared is running. As soon as the method returns, they’re gone and the memory is freed immediately.
Stack overflow
If you accidentally (or intentionally) write infinite recursion, each method call will add a new frame to the stack. At some point the stack will run out, and you’ll get:
Exception in thread "main" java.lang.StackOverflowError
Example:
public static void main(String[] args) {
recurse();
}
public static void recurse() {
recurse(); // Infinite recursion!
}
Stack size
The stack size is limited — typically a few megabytes per thread (it can be set with -Xss). If the stack is exhausted, the program crashes with an error.
3. Heap: a place for your objects
The heap is a shared memory area for all threads where all objects you create with new, as well as arrays, live. This is where all the magic of object-oriented programming happens.
How do objects end up in the heap?
String s = new String("Hello");
int[] arr = new int[10];
- The variable s is a reference; it lives on the stack.
- The String object itself and the array arr live in the heap.
Object lifecycle
An object lives in the heap as long as there is at least one strong reference to it. As soon as nothing references the object, it becomes “garbage” and may be removed by the garbage collector (GC).
Memory management
Unlike C/C++, where you must free memory yourself (free, delete), in Java this is handled by the GC. You cannot explicitly free an object, but you can null out all references to it — then it becomes a candidate for collection.
Diagram: where does what live?
Stack (main)
└─ s ─┬────────────┐
│ │
▼ │
Heap │
┌─────────────┐ │
│ String "Hello"◄──┘
└─────────────┘
Heap specifics
- There is a single heap for the entire JVM process.
- You can set the heap size at startup (-Xmx, -Xms).
- If there is no free space left in the heap and the GC cannot free memory, the program crashes with OutOfMemoryError.
4. PermGen and Metaspace: where do classes live?
When you write class MyClass { ... } and then run the program, the JVM has to store everything related to this class somewhere — methods, fields, bytecode, static variables, constants, and even string literals. For this, the JVM has a special memory area where classes “live.”
In the past, before Java 8, this area was called PermGen (Permanent Generation). But it had plenty of issues — for example, it had a fixed size, and if it ran out, the application simply crashed with OutOfMemoryError: PermGen space.
With Java 8, a new, more flexible area appeared — Metaspace. It replaced the old PermGen and can now grow automatically, taking as much memory as the system needs (within available physical memory).
PermGen (pre–Java 8)
- PermGen stored class metadata, static fields, and string literals.
- The size of PermGen was limited (small by default); you could increase it with -XX:MaxPermSize=256m.
- If an application loaded lots of classes dynamically (for example, in web servers), PermGen could “run out,” and you’d see an error:
java.lang.OutOfMemoryError: PermGen space
- The problem: PermGen cleanup didn’t always happen correctly when classes were unloaded dynamically (for example, during web application reloads).
Metaspace (Java 8+)
- Starting with Java 8, PermGen disappeared and Metaspace was introduced.
- Metaspace stores class metadata, but now in native memory (outside the Java heap).
- By default, Metaspace is not limited (it’s only limited by system memory), but you can set a cap with -XX:MaxMetaspaceSize=512m.
- The error when memory runs out now looks like this:
java.lang.OutOfMemoryError: Metaspace
- Metaspace also holds static fields, methods, and class information.
Diagram: how it’s all arranged
┌───────────────────────────────┐
│ JVM process │
│ ┌─────────────┐ │
│ │ Stack │ ← Local variables, method calls
│ └─────────────┘ │
│ ┌─────────────┐ │
│ │ Heap │ ← Objects, arrays, everything created via new
│ └─────────────┘ │
│ ┌───────────────┐ │
│ │ Metaspace │ ← Class metadata, static fields
│ └───────────────┘ │
└───────────────────────────────┘
Why does this matter?
If you write typical desktop or server applications, you’ll most likely never hit PermGen or Metaspace errors. But if you work with dynamic class loading (for example, plugins, web applications, frameworks like Spring that can load and unload lots of classes), then knowing about Metaspace is a must-have!
5. Illustration: JVM memory diagram
flowchart TD
subgraph JVM
direction TB
Stack1["Stack (Thread 1)"]
Stack2["Stack (Thread 2)"]
Heap[Heap]
Metaspace[Metaspace]
end
Stack1 --references--> Heap
Stack2 --references--> Heap
Heap --uses classes from--> Metaspace
- Each thread has its own stack.
- All stacks can reference objects in the heap.
- Objects in the heap “know” their class, whose information resides in Metaspace.
6. Example: what this looks like in real code
public class MemoryDemo {
public static void main(String[] args) {
int x = 42; // x lives on main's stack frame
String s = "Hello!"; // s is a reference on the stack, the String object is on the heap, the literal "Hello!" is in Metaspace
Person p = new Person("Alice"); // p is a reference on the stack, the Person object is on the heap
// Call a method to create a new stack frame
printPerson(p);
}
public static void printPerson(Person person) {
// person is a reference on printPerson's stack frame
System.out.println(person.getName());
}
}
class Person {
private String name;
public Person(String name) {
this.name = name;
}
public String getName() { return name; }
}
Walkthrough:
- x is a local variable that lives on the main method’s stack frame.
- s is a reference on the stack, the String object is on the heap, and the string literal "Hello!" is in Metaspace.
- p is a reference on the stack, the Person object is on the heap.
- The Person class and all its methods/fields are in Metaspace (class metadata).
- The call printPerson(p) creates a new stack frame; inside it, the local reference person points to the same object in the heap.
7. How the JVM manages memory: a brief FAQ
Can I manage the stack?
No, the stack is fully controlled by the JVM. You can only set its size at startup (-Xss).
Can I manage the heap?
Partially: the heap size is set at startup (-Xmx, -Xms). Cleanup is handled by the garbage collector (GC).
Can I manage Metaspace?
You can limit its size (-XX:MaxMetaspaceSize), but this is usually unnecessary.
What happens when memory runs out?
— If the stack runs out — StackOverflowError.
— If the heap runs out — OutOfMemoryError: Java heap space.
— If Metaspace runs out — OutOfMemoryError: Metaspace.
8. Common memory-related mistakes
Error #1: StackOverflowError due to infinite recursion. The most common cause is forgetting to provide a recursion exit condition. For example, a method calls itself endlessly. The JVM cannot grow the stack indefinitely, and the program will crash.
Error #2: OutOfMemoryError due to heap exhaustion. If you create too many objects that remain referenced by variables/collections (for example, you keep adding items to a list and never remove them), the heap can be exhausted.
Error #3: OutOfMemoryError: PermGen space / Metaspace. If you use plugins or dynamically load lots of classes and Metaspace is not cleaned up (for example, due to improper class unloading), Metaspace can run out of space.
Error #4: Confusing a reference with an object. Many beginners confuse this: a variable of type Person on the stack is only a reference, while the object itself lives in the heap.
Error #5: Expecting the garbage collector to delete everything instantly. The GC works “when it wants to” (in reality — based on internal algorithms and memory pressure), not immediately after you null a reference. Don’t count on instant memory release.
GO TO FULL VERSION