Runtime Data Areas: Heap, Stack, and Method Area Internals

Understanding JVM memory architecture: Heap, JVM Stack, Method Area, PC Registers, and Native Method Stacks - what they store and how they interact.

published: reading time: 27 min read author: GeekWorkBench

Runtime Data Areas: Heap, Stack, and Method Area Internals

The JVM divides its memory into several runtime data areas. Each area has a specific purpose and lifecycle. Getting these areas wrong leads to memory leaks, StackOverflowError, or mysterious OutOfMemoryError messages that reference regions you did not know existed.

This post covers every memory region the JVM uses: which ones are shared across threads, which ones are private to each thread, which ones are garbage collected, and which ones grow independently of the heap.

Introduction

The JVM divides its available memory into distinct runtime data areas, each with a specific purpose, lifecycle, and behavior. The heap stores objects and arrays and is shared across all threads. The metaspace (Java 8+) stores class metadata in native memory outside the heap. Each thread gets its own private JVM stack for method frames, along with a program counter register. Native method stacks handle JNI code. Getting these areas wrong as a developer leads to the three most common JVM memory errors: OutOfMemoryError referencing the heap or metaspace, StackOverflowError from excessive recursion, and subtle performance degradation from improper heap sizing that triggers frequent garbage collection pauses.

Runtime data areas matter to practitioners for two reasons. The first is diagnosing production failures. When an OutOfMemoryError appears in logs, the message identifies which region is exhausted — “Heap Space” versus “Metaspace” versus “Direct Buffer Memory” — and the fix is completely different in each case. Heap exhaustion typically means a memory leak or inappropriate object retention; metaspace exhaustion means a classloader leak or excessive dynamic class generation; direct buffer exhaustion means too many NIO buffers created without release. Knowing which region is affected narrows the investigation immediately. The second reason is performance tuning, where understanding the generational hypothesis (most objects die young) informs why young generation sizing affects overallGC behavior, and why survivor space capacity prevents premature promotion of short-lived objects.

This post covers every runtime data area in detail: the heap and its young/old generation structure, the JVM stack and what each stack frame contains, metaspace versus the old PermGen and why the migration mattered, the PC register and native method stacks, and the production failure scenarios tied to each region. You will learn how to interpret OutOfMemoryError messages, what causes metaspace growth to appear unbounded, why direct ByteBuffers threaten native memory even when the heap has space, and how the generational hypothesis justifies the JVM’s default heap division.

When NOT to Use

Most developers never need to think about runtime data areas in detail. If you are writing business logic and hit an OutOfMemoryError, the error message tells you which region is exhausted. Start there, not with stack frame internals. The JVM defaults work fine for typical applications. Framework authors, library maintainers, and engineers debugging production memory leaks are the actual audience for this depth of detail.

Do not use runtime data area knowledge to optimize before you have measurements. Setting -Xms equal to -Xmx because you read that resizing adds GC overhead is premature if your heap is large enough that resizing almost never triggers. Tuning survivor ratios without GC logs showing actual promotion problems rarely helps. If you have StackOverflowError, the fix is usually finding and fixing infinite recursion, not adjusting -Xss.

Metaspace problems almost always trace to classloader leaks or excessive reflection, not undersized metaspace. Direct buffer memory issues come from NIO usage patterns that need auditing regardless of what you know about memory regions. Only go deep on this stuff when profiling data or production incidents actually demand it. Related topics like Execution Engine and JVM Startup and Shutdown come into play when memory issues overlap with JIT or lifecycle events.

Memory Region Overview

graph TD
    subgraph "Heap (Shared across threads)"
        Young["Young Generation<br/>Eden + Survivor Spaces"]
        Old["Old Generation<br/>Tenured"]
    end

    subgraph "Method Area (Shared, per Class Loader)"
        ClassMeta["Class Metadata"]
        CodeCache["Code Cache<br/>(JIT compiled code)"]
        InternStrings["Interned Strings"]
    end

    subgraph "Per-Thread Areas"
        Stack["JVM Stack<br/>(Frames)"]
        PCR["PC Register"]
        NMS["Native Method Stack"]
    end

    Heap --> GC["Garbage Collector"]
    ClassMeta --> Metaspace["Metaspace<br/>(Java 8+: Replaces PermGen)"]

The Heap

The heap is the runtime memory area where objects and arrays are stored. It is shared across all threads, which means race conditions apply when multiple threads allocate objects concurrently. The garbage collector manages this region, reclaiming memory from objects that are no longer reachable.

The heap is divided into generations. Young generation holds short-lived objects. Old generation holds long-lived objects that survived multiple young collections.

Heap Structure

The heap has three main regions:

Young Generation contains Eden and two Survivor spaces (S0 and S1). New objects start in Eden. After a young GC, surviving objects are copied to a Survivor space. Objects that survive enough collections (determined by the tenuring threshold) are promoted to old generation.

Old Generation holds objects that have lived long enough. It is larger than young generation because most objects die young (the generational hypothesis). The old generation is collected less frequently but takes longer when it is collected.

Metaspace (Java 8+) replaced PermGen. It stores class metadata, method information, constant pools, and JIT compiled code. Unlike the heap, Metaspace uses native memory outside the garbage-collected regions. It can grow until the system runs out of native memory.

Heap Sizing

# Set initial and maximum heap size
-Xms2g -Xmx2g

# Set young generation size (absolute)
-Xmn512m

# Set young generation size (ratio, e.g., 1/3 of heap)
-XX:NewRatio=2

# Set Eden to Survivor ratio
-XX:SurvivorRatio=8

# Metaspace size limit (Java 8+)
-XX:MaxMetaspaceSize=256m

# Compressed class pointers (Java 8+)
-XX:+UseCompressedClassPointers

Setting -Xms equal to -Xmx eliminates heap resizing during runtime. Resizing adds GC overhead because the JVM must recalculate sizes and move objects.

JVM Stack

Each thread gets its own JVM stack. This is not the same as the heap. The stack stores method frames, not objects. Each frame contains local variables, operand stack, and reference to the constant pool of the class being executed.

The stack operates like a standard stack: push a frame when entering a method, pop it when exiting. StackOverflowError occurs when there is no room for a new frame, usually due to excessive recursion.

Stack Frame Contents

Local Variables Array holds method parameters and local variables. Primitive types take one slot. Object references take one slot (the slot holds the reference, not the object itself). Long and double types take two consecutive slots.

Operand Stack is a LIFO stack used during bytecode execution. Most bytecode instructions push values onto the operand stack or pop them off. The JVM uses this stack instead of registers for intermediate values and calculations.

Reference to Constant Pool points to the class constant pool, allowing the method to resolve symbolic references to other classes and methods.

	public class StackFrameDemo {
    // Example: local variable at index 0 is 'this' reference
    // local variable at index 1 is int 'x'
    public int instanceMethod(int x, int y) {
        int result = x + y;  // x at slot 1, y at slot 2, result at slot 3
        return result;
    }
}

Stack Depth and Thread Count

Stack size is set per thread. The -Xss flag controls default stack size (typically 1MB). More threads with larger stacks consume more memory, but deeper recursion becomes possible.

Stack memory is allocated when the thread starts, not when frames are pushed. This means even a thread doing no work consumes stack space.

Method Area and Metaspace

The method area stores class-level data. In Java 8 and earlier, this was called PermGen. Starting with Java 9, PermGen was replaced by Metaspace, which uses native memory instead of heap memory.

What the Method Area Stores

Class Metadata includes the class name, superclass name, modifiers, and package. It stores the list of fields, methods, and constructors along with their attributes.

Constant Pool is a runtime table of numeric and string constants and symbolic references. The constant pool is loaded from the class file constant pool and expanded at runtime to include dynamically generated constants (like String.intern() results).

Field Information stores field name, type, modifiers, and offset in the class instance.

Method Information stores method name, return type, parameter types, modifiers, bytecodes, exception table, and stack frame layout.

Code Cache stores JIT compiled code. The JVM profiles executing bytecode and compiles hot methods to native code. This compiled code is stored in the code cache, which lives in Metaspace.

Metaspace vs. PermGen

AspectPermGen (Java 8 and earlier)Metaspace (Java 9+)
Memory LocationHeapNative memory
Size LimitFixed at JVM startupGrows dynamically
Default SizeSmall (~80MB)Unlimited (system memory)
OOM Namejava.lang.OutOfMemoryError: PermGen spacejava.lang.OutOfMemoryError: Metaspace
Garbage CollectionSubject to heap GCAutomatically reclaimed when classloaders die

PC Register

Each thread has a Program Counter register that holds the address of the currently executing JVM instruction. For native methods (methods written in C or C++ via JNI), the PC register is undefined.

The PC register enables thread scheduling. When a thread yields or is preempted, the JVM saves the PC register value so execution can resume at the correct instruction.

Native Method Stack

Native method stacks are analogous to JVM stacks but for native code. They store native method calls rather than Java bytecode. JNI code uses this stack to call native functions and receive callbacks from native code.

Some JVM implementations (like the HotSpot JVM) use the same stack for both Java bytecode and native code. Others use distinct stacks.

Production Failure Scenarios

FailureRoot CauseSymptomsFix
OutOfMemoryError: Heap SpaceMemory leak or excessive object allocationHeap fills up, GC thrashingHeap dump, -Xmx increase
OutOfMemoryError: MetaspaceClassloader leak or dynamic class generationMetaspace grows unboundedLimit class generation, fix classloader leaks
OutOfMemoryError: Direct Buffer MemoryExcessive NIO buffer allocationNative memory exhaustion-XX:MaxDirectMemorySize, audit ByteBuffer usage
StackOverflowErrorDeep recursion or infinite loopThread dies with stack traceFix recursion, increase -Xss
GC Overhead Limit ExceededExcessive GC with little reclaimable memoryApplication slows dramaticallyHeap analysis, fix memory leaks
Unable to Create New Native ThreadToo many threads or native memory exhaustedOutOfMemoryError: unable to create new native threadReduce thread count, increase native memory

Trade-off Analysis

Trade-offConsiderations
Heap Size vs. GC FrequencyLarger heap means fewer GC cycles but longer pauses when GC runs. Smaller heap triggers more frequent but shorter pauses.
Young vs. Old Generation RatioMore young generation benefits short-lived objects. More old generation helps long-lived objects. The right ratio depends on object lifetime distribution.
Stack Size vs. Thread CountLarger stacks allow deeper recursion but reduce the number of threads that can run simultaneously.
Metaspace vs. Heap MonitoringMetaspace growth is less visible than heap growth. Native memory exhaustion affects the whole system.
Compressed Pointers vs. Address SpaceCompressed class pointers save ~50% Metaspace but limit addressing. Disabling compression allows huge metaspace but wastes memory.

Failure Scenarios Deep Dive

Young Generation Premature Promotion

When the young generation is undersized, objects that should die in Eden survive because the Survivor spaces cannot hold them. They get promoted to old generation during minor GC. Over time, the old generation fills faster than expected, triggering more frequent major GCs or full GCs. The root cause is often a workload with many medium-lived objects that are too long-lived for Eden but should not live long enough for old generation. Use -Xmn to increase young generation size or adjust SurvivorRatio to give Survivor spaces more capacity. GC logs showing high promotion rates confirm this pattern.

Native Memory Leak from Direct ByteBuffers

Direct ByteBuffers allocate native memory outside the heap. Unlike heap objects, this memory is not automatically reclaimed by GC. If your application creates many direct buffers that are then abandoned (not explicitly deallocated), the native memory grows until the system runs out. The java.nio.DirectByteBuffer class itself does not allocate native memory in the constructor; the actual allocation happens during I/O operations. Monitoring native memory with NMT (Native Memory Tracking) helps identify this pattern: jcmd <pid> VM.native_memory summary.

Metaspace Fragmentation

Metaspace can suffer from internal fragmentation when many classloaders load and unload classes. Each classloader has its own chunk of Metaspace, and when it is unloaded, the space is returned to the free list. Over time, the free list becomes fragmented, with small gaps between used chunks. While Metaspace can grow, it may not be able to allocate a large class metadata block even when total free space is sufficient. This manifests as OutOfMemoryError: Metaspace despite seemingly adequate available space. The fix involves reducing classloader churn or restarting the JVM periodically.

Implementation Patterns

// Diagnosing heap usage
public class HeapDiagnostics {
    public static void main(String[] args) {
        Runtime runtime = Runtime.getRuntime();
        long maxMemory = runtime.maxMemory();
        long totalMemory = runtime.totalMemory();
        long freeMemory = runtime.freeMemory();
        long usedMemory = totalMemory - freeMemory;

        System.out.printf("Max Memory: %d MB%n", maxMemory / 1024 / 1024);
        System.out.printf("Total Memory: %d MB%n", totalMemory / 1024 / 1024);
        System.out.printf("Used Memory: %d MB%n", usedMemory / 1024 / 1024);
        System.out.printf("Free Memory: %d MB%n", freeMemory / 1024 / 1024);
    }
}
// Stack frame inspection via bytecode
import java.lang.reflect.*;

public class StackFrameInspection {
    public void inspectMethod(Class<?> clazz, String methodName) throws Exception {
        Method[] methods = clazz.getDeclaredMethods();
        for (Method m : methods) {
            if (m.getName().equals(methodName)) {
                System.out.println("Method: " + m.getName());
                System.out.println("  Slot count: " + m.getParameterCount() + " params");
                // Slot 0 is 'this' for instance methods
            }
        }
    }
}
// Monitoring metaspace via MXBean
import java.lang.management.*;
import javax.management.*;

public class MetaspaceMonitor {
    public static void monitor() {
        try {
            MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
            ObjectName name = new ObjectName("java.lang:type=MemoryPool,name=Metaspace");
            MemoryUsage usage = (MemoryUsage) mbs.getAttribute(name, "Usage");
            System.out.printf("Metaspace Used: %d MB%n",
                usage.getUsed() / 1024 / 1024);
            System.out.printf("Metaspace Committed: %d MB%n",
                usage.getCommitted() / 1024 / 1024);
            System.out.printf("Metaspace Max: %d MB%n",
                usage.getMax() / 1024 / 1024);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

Observability Checklist

  • Track heap usage over time: used vs. committed vs. max
  • Monitor young vs. old generation allocation rates
  • Watch Metaspace committed vs. used for classloader leaks
  • Monitor code cache size for JIT compilation issues
  • Track direct buffer memory usage if using NIO
  • Enable GC logs: -Xlog:gc*:file=/path/to/gc.log
  • Use jmap -heap to capture heap histogram
  • Monitor thread stack memory consumption
  • Watch for native memory exhaustion symptoms

Security Notes

The JVM enforces memory safety through the type system and access controls. Code running in one thread cannot directly read or write another thread’s stack frames. Access to heap objects is mediated through references, which the JVM validates.

However, native code (JNI) operates outside the JVM’s safety guarantees. JNI code can read and write arbitrary memory addresses. Never expose JNI interfaces to untrusted code.

Reflection can bypass normal access controls. The module system (Java 9+) restricts reflective access to internal APIs. Code that worked in Java 8 may fail in Java 9+ unless modules are explicitly opened.

Common Pitfalls / Anti-Patterns

Assuming heap is the only memory region that matters. Many developers focus exclusively on heap and forget Metaspace, code cache, direct buffers, and thread stacks. All of these can cause OutOfMemoryError.

Setting heap too small for the workload. Applications under memory pressure spend more time in GC, causing latency spikes and throughput degradation. Profile your application to determine realistic memory requirements.

Ignoring the young generation size. If young generation is too small, short-lived objects get promoted to old generation prematurely, accelerating old generation fill-up.

Assuming String.intern() is free. String.intern() stores strings in the constant pool (part of Metaspace/PermGen). Excessive interning fills Metaspace. Modern JVMs automatically intern string literals, so explicit intern() is rarely needed.

Not sizing stacks correctly for recursive code. StackOverflowError occurs when the stack cannot accommodate another frame. If your application uses deep recursion (even indirectly through library calls), you may need to increase stack size.

Quick Recap Checklist

  • Heap stores objects, managed by GC
  • Method Area/Metaspace stores class metadata, uses native memory
  • Each thread has its own JVM Stack and PC Register
  • Native Method Stack is for JNI code
  • Heap is divided into young and old generations
  • Young generation has Eden and two Survivor spaces
  • Metaspace replaced PermGen in Java 8+
  • Stack stores method frames, not objects
  • StackOverflowError means recursion depth exceeded
  • Metaspace exhaustion causes different OOM than heap exhaustion
  • Direct buffers use native memory outside the heap

Interview Questions

1. How is the JVM heap organized and why?

The heap is divided into young generation (containing Eden and two Survivor spaces) and old generation. New objects start in Eden. After minor GC, surviving objects move to a Survivor space. Objects that survive enough GC cycles get promoted to old generation. This division exists because of the generational hypothesis: most objects die young. By focusing GC effort on the young generation, the JVM minimizes pause times and maximizes throughput. Old generation collection is less frequent because most objects do not live long enough to need it.

2. What is the difference between the JVM stack and the heap?

The heap stores objects and arrays. It is shared across all threads and garbage collected. The JVM stack stores method frames for each thread. Each frame contains local variables, operand stack, and a reference to the constant pool. Stacks are not garbage collected; they are allocated and deallocated as methods are called and return. Heap objects are accessed via references stored in stack variables. A thread cannot access another thread's stack directly.

3. What happened when PermGen was replaced by Metaspace?

In Java 8, PermGen was replaced by Metaspace. The key differences: PermGen lived in the heap and had a fixed maximum size configured at JVM startup. Metaspace uses native memory outside the heap, allowing it to grow dynamically until the system runs out of memory. This eliminated the common OutOfMemoryError: PermGen space errors that plagued applications using reflection, proxies, or dynamic class generation. However, it also meant Metaspace exhaustion could affect the entire system, not just the JVM process. GC now automatically reclaims Metaspace when classloaders die.

4. What causes StackOverflowError and how do you fix it?

StackOverflowError occurs when the JVM stack cannot allocate a new frame, typically from infinite recursion or excessively deep recursion. The fix is to correct the recursive code: add a base case, convert recursion to iteration, or increase stack size with the -Xss flag. Before increasing stack size, consider whether the recursion is legitimate. If method A calls method B which calls method C which calls A again, you have a cycle. Stack size per thread multiplied by thread count determines total stack memory consumption.

5. What does a stack frame contain?

A stack frame contains three components. The local variables array holds method parameters and local variables, indexed by slot number. Primitive values occupy one slot; object references occupy one slot; long and double occupy two consecutive slots. The operand stack is a LIFO stack used as a workspace during bytecode execution. Most bytecode instructions push values to or pop values from the operand stack. The reference to constant pool allows the method to resolve symbolic references to other classes and methods at runtime. When a method is called, a new frame is pushed. When it returns, the frame is popped.

6. What is the difference between the young generation and old generation in terms of GC behavior?

The young generation is collected by Minor GC (also called young GC), which is stop-the-world but typically very fast (milliseconds). Objects start in Eden, and after surviving a minor GC, they move to a Survivor space. Objects that survive enough minor GCs (after reaching the tenuring threshold) are promoted to the old generation. Major GC or Full GC collects the old generation, which is larger and contains long-lived objects. Major GC is slower and more disruptive because the old generation can be hundreds of megabytes to several gigabytes. In G1, mixed GC collects both young and old regions. The generational hypothesis (most objects die young) justifies focusing GC effort on the young generation.

7. How does compressed class pointers (UseCompressedClassPointers) affect memory usage?

Compressed class pointers allow the JVM to use 32-bit offsets for class metadata references instead of 64-bit pointers, reducing Metaspace usage by approximately 50%. This optimization is enabled by default on JVMs with heap sizes under around 32GB. Each class's metadata (instance size, method table, vtable) is stored in Metaspace, and compressed pointers reference this metadata. When the heap exceeds ~32GB, the JVM disables compressed class pointers because 32-bit offsets can no longer address all possible memory locations. With compressed class pointers disabled, Metaspace usage increases and may require a larger MaxMetaspaceSize setting.

8. What causes metaspace to grow unbounded and how do you diagnose it?

Metaspace grows unbounded when classloaders are retained in memory after their classes should have been unloaded. This typically happens through classloader leaks: static collections holding classloader references, ThreadLocal values pointing to classloaders, orJNI global references. Diagnostic steps: enable -XX:+TraceClassLoading to see class loading/unloading, use JMX to monitor Metaspace usage over time, and take heap dumps when Metaspace usage is elevated. Eclipse MAT or VisualVM can identify classloader instances with large retained sets. The fix is to ensure classloaders are properly released: remove references from ThreadLocals, clear static collections, and auditJNI global references.

9. How does the PC register enable thread preemption and resuming?

Each thread has its own Program Counter register holding the address of the currently executing JVM instruction. When a thread is preempted (by the OS scheduler giving the CPU to another thread) or yields, the JVM saves the PC register value for that thread. When the thread later resumes execution, the JVM restores the saved PC value, allowing the thread to continue from exactly where it left off. For native methods, the PC register is undefined because native code executes outside the JVM's instruction set. The PC register effectively provides each thread with the ability to pause and resume execution without losing its place in the bytecode stream.

10. What is the purpose of the constant pool in the method area?

The constant pool is a runtime table that holds numeric and string constants and symbolic references to classes, methods, and fields. It is loaded from the class file constant pool and expanded at runtime to include dynamically generated constants (like String.intern() results). Bytecode instructions reference the constant pool by index to load literal values, access fields, invoke methods, and create objects. Without the constant pool, bytecode would need to embed raw values inline, making class files larger and updates harder. The constant pool also enables dynamic linking by deferring the resolution of symbolic references until runtime.

11. What is the difference between the young generation Eden space and Survivor spaces?

Eden is where new objects are initially allocated. When Eden fills, a minor GC runs and surviving objects (those still referenced) are copied to a Survivor space. Objects that survive multiple minor GCs get promoted to old generation. The two Survivor spaces (S0 and S1) alternate as the destination for surviving objects—one is always empty. This arrangement allows the GC to reclaim Eden and one Survivor space in each minor GC. Without Survivor spaces, every surviving object would be promoted directly to old generation, overwhelming it with short-lived objects. The Survivor spaces act as a filtering mechanism before tenuring.

12. How does the tenuring threshold affect object promotion from young to old generation?

The tenuring threshold determines how many minor GCs an object must survive before being promoted to old generation. When an object is copied from Eden to a Survivor space, it has an age counter. After each minor GC that the object survives, the age increments. When the age reaches the tenuring threshold (default varies by JVM, often 6-15), the object is eligible for promotion to old generation. Setting the threshold low promotes objects faster, reducing Survivor space pressure but filling old generation sooner. Setting it high keeps objects in young generation longer, which is beneficial if most die young but can cause Survivor overflow if allocation rate is high.

13. What is the difference between TLAB (Thread-Local Allocation Buffer) and regular heap allocation?

TLABs are pre-allocated heap regions for each thread to use for object allocation, reducing contention on the heap's allocation bitmap. Without TLABs, every allocation would require atomic operations on shared heap structures, creating contention in multi-threaded applications. With TLABs, each thread allocates into its private region, writing only the pointer at the end (which requires minimal synchronization). When a thread's TLAB fills, it requests a new one from the Eden space. TLAB size is determined by thread count and heap size, with defaults around 1MB for larger heaps. This allocation path compression makes object allocation nearly as fast as stack allocation.

14. What causes direct buffer memory to grow and how does it differ from heap memory?

Direct ByteBuffers (ByteBuffer.allocateDirect()) allocate native memory outside the JVM heap. They grow when applications create many direct buffers without releasing them—the cleaner does not run immediately when the buffer becomes unreachable, and the native memory may not be freed until a forced full GC or explicit invocation. Unlike heap memory, direct buffer memory is not automatically managed by the GC, meaning it can cause native OOM even when heap has space. Monitoring with Native Memory Tracking (NMT) via jcmd VM.native_memory summary shows direct buffer usage. The -XX:MaxDirectMemorySize flag limits total direct buffer allocation.

15. What is the relationship between the method area and class metadata?

The method area (or Metaspace in Java 8+) is the memory region that stores class metadata. This includes the class name, superclass name, modifiers, and package; the list of fields, methods, and constructors with their attributes; field descriptors and method descriptors; constant pools; and JIT compiled code. Each loaded class has its metadata stored here. The metadata is organized in the native memory of Metaspace as classld (class loader data) structures linked in a hierarchy mirroring the classloader hierarchy. When a classloader is garbage collected, its classes' metadata is reclaimed.

16. How does the JVM handle thread stack size when creating threads?

The JVM allocates stack memory for each thread at thread creation time, not on-demand. The -Xss flag (or -XX:ThreadStackSize) sets the stack size in bytes, with typical defaults of 1MB. If the OS cannot allocate the requested stack size (insufficient virtual memory, especially in containerized environments with memory limits), thread creation fails with an OutOfMemoryError. The actual committed stack memory grows over time as frames are pushed (guarded by page faults), but the virtual memory reservation happens upfront. Larger stacks allow deeper recursion but reduce the number of threads that can run simultaneously under memory constraints.

17. What is the difference between the old generation and the tenured generation?

Old generation and tenured generation are the same thing—different terminology for the region holding long-lived objects that have survived multiple young generation collections. In G1 GC, the equivalent is the "old regions." Objects promoted from Survivor spaces during minor GC are copied to the old generation. The old generation is larger than the young generation (typically 2-3x ratio) because most objects die young, so only long-lived objects need the larger space. Major GC or Full GC reclaims memory from the old generation, which takes longer than minor GC because of the larger size and potential fragmentation.

18. What causes native method stack overflow and how does it differ from JVM stack overflow?

Native method stack overflow occurs when native code (via JNI) pushes too many frames on the native stack, analogous to StackOverflowError for JVM stacks. In HotSpot, the native method stack and JVM stack share the same OS thread stack, so native and Java call depth are both limited by the -Xss setting. When a JNI call chain is too deep, the same StackOverflowError can be thrown even though the Java code using the stack is valid. Some JVMs (like those for embedded systems) use separate native stacks. The fix is to either limit JNI call depth or increase -Xss, though the latter is rarely the right solution for deep JNI chains.

19. How does memory alignment affect object size and heap utilization?

The JVM aligns objects to 8-byte boundaries by default (configurable with -XX:ObjectAlignmentInBytes). An object with 7 bytes of fields actually consumes 8 bytes, wasting 1 byte. A 15-byte object wastes 1 byte; a 16-byte object wastes 0. This padding means smaller objects may consume more memory than their field totals suggest. Alignment enables faster CPU access (aligned loads/stores are cheaper) but creates internal fragmentation. ObjectAlignmentInBytes of 16 reduces fragmentation for larger objects but wastes more for small ones. Understanding alignment helps when profiling memory with tools like JOL (Java Object Layout).

20. What is the code cache region within Metaspace and how does it affect JIT compilation?

The code cache stores JIT-compiled native code—hot methods translated from bytecode to machine instructions. It is part of Metaspace and uses native memory. The code cache has a fixed size limit (controlled by -XX:ReservedCodeCacheSize); when full, JIT compilation stops for methods not yet compiled, causing the JVM to fall back to interpretation for those methods. This manifests as performance degradation after warmup instead of peak performance. Monitoring code cache usage with jstat -printcompilation or JMX helps identify when the cache is filling up. The default size scales with heap and GC algorithm, but applications with many hot methods may need larger code caches.

Further Reading

Conclusion

The JVM divides memory into shared areas (Heap, Metaspace) and per-thread areas (JVM Stack, PC Register, Native Method Stack). The Heap stores objects; Metaspace stores class metadata in native memory; each thread has its own stack for method frames. Understanding these regions helps diagnose OutOfMemoryError in heap vs. Metaspace, StackOverflowError from recursion, and performance issues from improper sizing of young/old generations.

Category

Related Posts

Heap Walking and Allocation Tracking: TLABs and Heap Analysis

Understand how the JVM allocates memory with TLABs, how to track allocations with low overhead, and how heap walking tools analyze object graphs.

#jvm #heap #tlab

JVM Architecture Overview: Understanding the Java Virtual Machine

A deep dive into the JVM architecture covering Class Loader, Runtime Data Areas, and Execution Engine components that power Java applications.

#jvm #java #architecture

Async-Profiler: Low-Overhead CPU and Memory Profiling

Learn async-profiler for low-overhead CPU and memory profiling in production. Generate flame graphs, analyze allocations, and diagnose JVM bottlenecks.

#jvm #async-profiler #cpu-profiling