Java Memory Model: Heap, Stack, Metaspace and Code Cache

The Java memory model on JDK 25 explained with heap generations, thread stacks, metaspace and code cache, plus their flags, defaults and errors.

Diagram of JVM memory areas in JDK 25 with the shared heap split into young and old generation, metaspace and code cache in native memory, and a JVM stack, native method stack and PC register for each platform thread

The Java memory model, in the sense most developers search for, is the way a running JVM divides its memory into the heap for objects, one stack per thread for method calls, metaspace for class data and the code cache for compiled code. Each area has its own size limit, its own JVM flag and its own error when it runs full.

We need this model when we size a container, tune garbage collection, or read an error such as OutOfMemoryError: Metaspace or StackOverflowError, because the fix depends on which area ran out.

The following example creates a few values and shows where the JVM keeps each one on JDK 25.

int count = 3;                                            // 3, a local variable in the stack frame
String city = "Pune";                                     // "Pune", reference on the stack, String object in the heap
List<String> cities = new ArrayList<>(List.of(city));     // [Pune], the ArrayList object is on the heap
long maxHeap = Runtime.getRuntime().maxMemory();          // 1568669696 bytes here, a quarter of the RAM the JVM sees

Notice that only the primitive and the references live in the stack frame of the method, whereas every object goes to the heap. We first look at all memory areas together and continue with the heap, thread stacks, metaspace and code cache with their flags and errors. A short section explains the other meaning of “Java memory model”, the thread visibility rules from the Java Language Specification.

1. JVM Memory Areas at a Glance

The Java Virtual Machine Specification defines the run-time data areas that every JVM has, and HotSpot, the JVM inside OpenJDK and Oracle JDK, implements them as concrete memory regions. Some areas are created when the JVM starts and are shared by all threads. Others are created for each thread and removed when the thread ends.

Diagram of JVM memory areas in JDK 25 with the shared heap split into young and old generation, metaspace and code cache in native memory, and a JVM stack, native method stack and PC register for each platform thread
Objects share one heap, class data and compiled code sit in native memory outside the heap, and every platform thread gets its own stack

The table gives the size flag and the error for each area, which is what we look for when an app fails. The defaults are for a 64-bit Linux JDK 25.

AreaHoldsShared or per threadSize flag (default)Error when full
HeapObjects, arrays, String pool, static field valuesShared-Xmx (25% of RAM)OutOfMemoryError: Java heap space
JVM stackFrames with local variables and partial resultsPer platform thread-Xss (1 MB)StackOverflowError
MetaspaceClass metadata, method bytecode, constant poolsShared, native memory-XX:MaxMetaspaceSize (no limit)OutOfMemoryError: Metaspace
Code cacheMachine code from the JIT compilerShared, native memory-XX:ReservedCodeCacheSize (240 MB)Warning, the JIT stops compiling
Native method stackFrames of native (JNI) methodsPer threadSame as the thread stackStackOverflowError
PC registerAddress of the current bytecode instructionPer threadFixedNone

2. Heap Memory

The heap holds every object and array the app creates, and the garbage collector frees the objects that are no longer reachable. Since Java 7, the heap also holds the String pool and the values of static fields. The JVM reserves the heap at startup and grows it between the initial size (-Xms) and the maximum size (-Xmx).

Without -Xmx, JDK 25 sets the maximum heap to 25% of the memory it sees, which is the physical RAM or the memory limit of the container. This default is set by -XX:MaxRAMPercentage=25, so a Spring Boot application in a 2 GB container gets a 512 MB heap and can fail with OutOfMemoryError while 1.5 GB of the container stays unused.

# fixed heap size
java -Xms1g -Xmx1g -jar app.jar

# heap as a share of the container memory limit
java -XX:MaxRAMPercentage=75 -jar app.jar

In containers, we set the heap with -XX:MaxRAMPercentage and leave about a quarter of the memory for metaspace, the code cache, thread stacks and other native memory. A heap equal to the container limit gets the process killed by the operating system, because the other areas need memory too.

2.1. Young and Old Generation

Most objects in a typical app live for a short time, such as the request and response objects of one HTTP call. The heap of the default G1 collector and of the Serial and Parallel collectors is therefore split into generations, so the collector can clean the short-lived objects often and cheaply.

  • The young generation holds new objects. It has one eden space, where the JVM allocates objects, and two survivor spaces, where objects that survived a collection wait.
  • The old generation holds objects that survived several young collections. An object moves there after at most 15 collections (-XX:MaxTenuringThreshold=15), or earlier when the survivor space is full.
  • Large objects, at least half a G1 region in size, go into humongous regions in the old generation, because copying them between survivor spaces would cost too much.

We can list the heap areas of the running JVM with the MemoryPoolMXBean API. With the default G1 collector on JDK 25, the heap has three pools, and the other pools belong to native memory.

List<String> heapPools = ManagementFactory.getMemoryPoolMXBeans().stream().filter(pool -> pool.getType() == MemoryType.HEAP).map(MemoryPoolMXBean::getName).toList();          // [G1 Eden Space, G1 Old Gen, G1 Survivor Space]
List<String> nonHeapPools = ManagementFactory.getMemoryPoolMXBeans().stream().filter(pool -> pool.getType() == MemoryType.NON_HEAP).map(MemoryPoolMXBean::getName).toList();   // [CodeHeap 'non-nmethods', Metaspace, CodeHeap 'profiled nmethods', Compressed Class Space, CodeHeap 'non-profiled nmethods']

The GC log shows the generations at work. In the trimmed G1 log of a small program with a 128 MB heap, each young collection empties the eden regions, a few objects stay in the survivor regions, and the old regions grow as long-lived objects get promoted.

$ java -Xmx128m -Xlog:gc,gc+heap=info SessionCache
[0.036s][info][gc,heap] GC(0) Eden regions: 14->0(42)
[0.036s][info][gc,heap] GC(0) Survivor regions: 0->1(2)
[0.036s][info][gc,heap] GC(0) Old regions: 2->2
[0.036s][info][gc     ] GC(0) Pause Young (Normal) (G1 Evacuation Pause) 15M->1M(96M) 1.086ms
[0.081s][info][gc,heap] GC(3) Eden regions: 50->0(50)
[0.081s][info][gc,heap] GC(3) Survivor regions: 6->6(7)
[0.081s][info][gc,heap] GC(3) Old regions: 2->5
[0.081s][info][gc     ] GC(3) Pause Young (Normal) (G1 Evacuation Pause) 57M->10M(96M) 2.577ms

The collectors and their algorithms are covered in our guide to Java garbage collection algorithms. JDK 25 also added compact object headers (-XX:+UseCompactObjectHeaders), which shrink the header of every object from 12 or 16 bytes to 8 bytes on 64-bit platforms and so fit more objects into the same heap. The option is off by default.

3. Thread Stacks

Each platform thread gets its own JVM stack when it starts. Every method call pushes a frame onto the stack, and the frame holds the local variables, the method parameters and the intermediate results of the bytecode. When the method returns, the JVM removes the frame, so stack memory needs no garbage collection.

A primitive local variable lives in the frame itself. For an object, the frame holds only the reference, and the object lives in the heap. That is why two threads can share an object but never share a local variable.

public class PriceCalculator {

    public double total(double price, int quantity) {
        double net = price * quantity;
        List<String> lines = new ArrayList<>();
        lines.add("net=" + net);
        return net * 1.18;
    }
}
double total = new PriceCalculator().total(100.0, 2);   // 236.0

While total() runs, its frame holds price, quantity, net and the reference lines. The ArrayList object and its String elements are on the heap and become unreachable when the frame is removed.

3.1. Stack Size and StackOverflowError

The stack has a fixed maximum size, 1 MB by default on 64-bit Linux, which we change with -Xss. A recursion that goes too deep fills the stack and throws a StackOverflowError. The number of frames that fit depends on the frame size, so a small recursive method gets far more frames than one with many local variables.

Thread stack sizeFrames before StackOverflowError (small recursive method, JDK 25)
-Xss256k3,644
Default (1 MB)23,306
-Xss4m101,948

For example, a service that parses JSON documents with a recursive tree walker works with every test file, but a customer uploads a document nested 50,000 levels deep and the parser thread dies with a StackOverflowError. Raising -Xss for the whole JVM makes every thread bigger. We can give a larger stack only to the parser thread, or rewrite the walker with an explicit ArrayDeque so the depth no longer depends on the stack.

Thread parser = Thread.ofPlatform().name("parser").stackSize(4L * 1024 * 1024).unstarted(() -> {});   // unstarted thread "parser" that requests a 4 MB stack

The stackSize() value is a request that the JVM may ignore on some platforms, so we test it on the target system.

3.2. Virtual Thread Stacks

Virtual threads, final since Java 21, do not get a fixed native stack. The JVM stores their frames in the heap as stack chunk objects, as described in JEP 444, and copies them to the stack of a carrier thread while the virtual thread runs. So a million virtual threads cost heap memory that grows and shrinks with their call depth, not a million times 1 MB.

4. Metaspace and Class Metadata

Metaspace holds the data the JVM keeps about each loaded class, such as the field and method layout, the bytecode of the methods and the runtime constant pool. It is the HotSpot implementation of the method area from the JVM specification. Since JDK 8 (JEP 122), metaspace lives in native memory outside the heap and replaced the Permanent Generation (PermGen).

Metaspace has no upper limit by default and grows as the app loads classes. A class’s metadata is freed only when the garbage collector unloads its class loader, which happens for application servers that redeploy apps and for code that creates classes at runtime, such as dynamic proxies or scripting engines.

  • -XX:MaxMetaspaceSize sets an upper limit, which turns a class loader leak into a clear error instead of a slowly growing process.
  • -XX:MetaspaceSize sets the size at which the first metaspace-triggered garbage collection runs (about 21 MB on JDK 25).
  • -XX:CompressedClassSpaceSize limits the part of metaspace that holds class structures addressed by compressed pointers (1 GB by default).

When the limit is reached, the JVM throws OutOfMemoryError: Metaspace. The trimmed output comes from a program that keeps defining hidden classes with -XX:MaxMetaspaceSize=32m.

Exception in thread "main" java.lang.OutOfMemoryError: Metaspace
	at java.base/java.lang.ClassLoader.defineClass0(Native Method)
	at java.base/java.lang.System$1.defineClass(System.java:2051)

Old startup scripts often still pass the PermGen flags. JDK 25 no longer knows them and refuses to start.

$ java -XX:MaxPermSize=256m -jar app.jar
Unrecognized VM option 'MaxPermSize=256m'
Error: Could not create the Java Virtual Machine.
PermGen (JDK 7 and earlier)Metaspace (JDK 8 and later)
LocationPart of the Java heapNative memory
Default maximumFixed, set by -XX:MaxPermSizeNo limit
Flags-XX:PermSize, -XX:MaxPermSize-XX:MetaspaceSize, -XX:MaxMetaspaceSize
Typical errorOutOfMemoryError: PermGen spaceOutOfMemoryError: Metaspace

5. Code Cache

The JVM starts by interpreting bytecode and compiles frequently called methods to machine code with its JIT compilers, which compile code while the app runs. The code cache is the native memory area that stores this compiled code. Since JDK 9 (JEP 197), it is split into three segments, one for JVM-internal code, one for lightly optimized (profiled) methods and one for fully optimized methods, which are the three CodeHeap pools in the earlier pool list.

The default reserved size with tiered compilation is 240 MB (-XX:ReservedCodeCacheSize). When the code cache runs full, the JVM prints a warning and stops compiling new methods, so the app keeps running but slows down in the interpreter. Large apps with many classes, such as a monolith with hundreds of Spring beans, sometimes need a bigger code cache, and the NMT output in section 6 shows how much is in use.

6. Other Native Memory and Container Sizing

Each thread also has a native method stack for code written in C and called through JNI, and a PC (program counter) register that holds the address of the bytecode instruction the thread runs. Both are small. The larger native consumers are the garbage collector’s own data structures, direct NIO buffers and the stacks of many platform threads.

Native Memory Tracking (NMT) reports how much each area uses. We start the JVM with -XX:NativeMemoryTracking=summary and read the report with jcmd <pid> VM.native_memory summary. The trimmed output comes from a small program on JDK 25 with -Xmx512m, where reserved is the address space and committed is the memory in use.

Total: reserved=2012764059, committed=166471579
-                 Java Heap (reserved=536870912, committed=100663296)
-                     Class (reserved=1073819832, committed=209080)
-                    Thread (reserved=13687880, committed=691272)
-                      Code (reserved=253725160, committed=7817704)
-                        GC (reserved=48512946, committed=39993266)
-                 Metaspace (reserved=67121432, committed=602392)

For a container, the memory limit must cover the sum of all committed areas, not only the heap. A web app with a 1.5 GB heap, 150 MB of metaspace, 50 MB of code cache, 200 threads with 1 MB stacks and 100 MB of GC data needs about 2 GB before counting direct buffers. So -XX:MaxRAMPercentage=75 is a common starting point, which we adjust after reading NMT in a load test.

7. The Other Java Memory Model (JLS Chapter 17)

The name “Java memory model” has a second, formal meaning. Section 17.4 of the Java Language Specification defines when a write by one thread becomes visible to a read in another thread, through rules called happens-before relations. It says nothing about heap or stack layout.

The happens-before rules matter for concurrent code. For example, a background job checks a stop flag that another thread sets. Without volatile, the JIT compiler may keep the flag in a register or move the read out of the loop, and the job never sees the change. A write to a volatile field happens before every later read of it, so the job stops.

public class JobControl {

    private volatile boolean stopped;

    public void stop() {
        stopped = true;
    }

    public boolean isStopped() {
        return stopped;
    }
}
JobControl control = new JobControl();
control.stop();
boolean stopped = control.isStopped();     // true

Starting a thread, joining it, entering and leaving a synchronized block and the classes of java.util.concurrent create the same visibility guarantees. Our Java concurrency tutorial covers them, and volatile and thread safety explains the keyword in detail.

8. Java Memory Model FAQs

Memory questions often start with an error message in production or with an interview question about where a particular value is stored.

8.1. Is Metaspace Part of the Heap?

No. Metaspace is native memory that the JVM allocates outside the Java heap, so -Xmx does not limit it, and a heap dump does not contain it. Only the Class objects that represent classes in Java code live on the heap.

8.2. Where Are Static Variables Stored in Java?

The values of static fields are stored on the heap, inside the java.lang.Class object of their class, since JDK 7. The field definitions, such as the name and the type, are part of the class metadata in metaspace. A static field that refers to an object keeps that object reachable as long as the class is loaded, which is a common cause of memory leaks in caches.

8.3. What Is the Default Heap Size in Java 25?

The default maximum heap is 25% of the memory the JVM sees, and the default initial heap is about 1.6% of it (-XX:InitialRAMPercentage=1.5625). In a container, the JVM uses the container memory limit instead of the host memory. We can print the values with java -XX:+PrintFlagsFinal -version and look for MaxHeapSize and InitialHeapSize.

$ java -XX:+PrintFlagsFinal -version | grep -E " (MaxHeapSize|InitialHeapSize) "
   size_t InitialHeapSize    = 98566144      {product} {ergonomic}
   size_t MaxHeapSize        = 1568669696    {product} {ergonomic}

8.4. What Is the Difference Between Stack and Heap Memory in Java?

The stack belongs to one thread and holds method frames with local variables, and its memory is freed when a method returns. The heap is shared by all threads and holds objects, and the garbage collector frees it. A stack that runs full throws StackOverflowError, whereas a full heap throws OutOfMemoryError.

8.5. Where Is the String Pool Stored?

The String pool has been part of the heap since JDK 7, and before that it was in PermGen. String literals and strings returned by intern() are pool entries, and the garbage collector can remove entries that nothing refers to. More details are in our article on the String constant pool.

9. Conclusion

The JVM splits its memory into a shared heap for objects, one stack per platform thread, metaspace for class metadata and a code cache for JIT-compiled code. Each area has its own flag (-Xmx, -Xss, -XX:MaxMetaspaceSize, -XX:ReservedCodeCacheSize) and its own error, so the error message tells us which area to look at first.

On JDK 25, the heap defaults to 25% of the available memory and uses young and old generations with the default G1 collector, metaspace is unlimited native memory, and virtual threads keep their frames on the heap. For containers, we size the whole process with NMT, not only the heap. The visibility rules of JLS chapter 17 share the name but belong to concurrent programming.

10. References

Happy Learning !!

Source Code on Github

Leave a Comment

  1. Hi ,
    I want to know
    if two tomcat running on same machine then will they use same heap area or different ?

  2. HI..
    i want to know about how application developed by one program can be viewed by different kind of client like(tab,mobile,pc…)

  3. Hi Lokesh,

    Could you explain why Native method stacks are needed and what is the main purpose of it in memory model of java.

  4. Heap memory contains three components. Young, Old and Perm generation.

    Logically, Method Area and runtime constant pool is also part of Heap memory’s Perm Gen – which Java people has planned to remove in JAVA 8. They are planning to have new structure ( Metaspace) in Java 8 instead of Perm Gen.

    Correct me if I am wrong.

    Regards, Uzair

      • Hi Lokesh,

        Thanks for your posts on Java. These are very clear and easy to understand. Will you pls explain more on Metaspace ? Is there any change in behavior for Metaspace from Perm Gen (like GC collection or any other in particular). As per my understanding, there will not be any major, in behavior.

        Regards, Uzair

    • Hi Ram, I don’t think there is any difference in terms of memory structuring inside JVM. It’s still same in java 8 as it was in java 7. Only major difference is removal of PermGen and introduction of Metaspace, but thats how JVM manages objects inside heap and method area. Please let me know if I am missing anything.

  5. Hi Lokesh,

    your blog awesome ! recently i got some requirements like i need to retrieve information from windows registry such as software/ hardware information So we will achieve this using java. can please guide me. do i need to use jRejestry jars ?

Comments are closed.

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.