RadhaPublication
HomeInterviewLearning PathsRole TracksMockBooksPricing
Login / Sign UpLog in

© 2026 Radha Publication. All rights reserved.

Shop on AmazonDonateContactPrivacyTerms
Java questions
MediumJava#67 min readJul 11, 2026

Explain Heap, Stack, Metaspace, PC Register, and Native Method Stack.

practice
learning
Practice modeTest yourself instead of reading straight through

Interviewers love this because it checks whether you can separate where data lives from how code runs inside the JVM.

Question: Explain Heap, Stack, Metaspace, PC Register, and Native Method Stack.

Answer: Java does not use one big memory bucket; the JVM divides runtime memory into areas with different jobs. The heap stores objects and is shared by all threads, while each thread gets its own stack for method calls, local variables, and return information. Metaspace holds class metadata, the PC register tracks the current bytecode instruction for each thread, and the native method stack supports calls into native code through JNI.

Interview-Ready Answer: I’d explain that the JVM splits memory by purpose. The heap stores objects and is shared across threads, the stack is per-thread and stores stack frames for local variables and return info, Metaspace stores class metadata in native memory, the PC register keeps each thread’s current instruction, and the native method stack supports JNI and other native calls. A useful detail is that since Java 8, class metadata moved from PermGen to Metaspace.

🧠 Memory Map
Memory map — visual summary of this topic

What each area does

Big picture: think of the JVM as a busy office. Some things are shared resources, and some are personal workspaces for each thread.

AreaShared?StoresTypical failure
HeapYesObjectsOutOfMemoryError: Java heap space
StackNoFrames, localsStackOverflowError
MetaspaceYesClass metadataOutOfMemoryError: Metaspace
PC RegisterNoNext instructionRarely fails directly
Native Method StackNoNative framesNative stack overflow

How the JVM uses them

  1. When a class is loaded, its metadata, method tables, and related runtime data go into Metaspace. In Java 8+, this is in native memory, not the Java heap.
  2. When a thread starts, the JVM gives it its own stack and PC register. These are private to that thread, so one thread’s method calls do not overwrite another’s.
  3. Each method call creates a stack frame (a small record for one call). It holds local variables, the operand stack, and return bookkeeping. Pushing and popping frames is conceptually O(1).
  4. When code executes new, the object is created on the heap. The reference to that object may live in a stack frame, in another object, or inside a collection.
  5. Garbage collection later reclaims heap objects that are no longer reachable. This is why heap allocation is often fast, but cleanup cost is paid later by GC.
  6. If Java calls a native method, execution switches to the native method stack. On many JVMs this is backed by the operating system thread stack, but conceptually the JVM treats it as a separate area.

Why the split matters

  • Heap is where object lifetime is managed by GC. Tune it with -Xms, -Xmx, and collector choices.
  • Stack is very fast and thread-local, but usually smaller; a deep recursive call chain can crash with StackOverflowError. A common default on HotSpot is around 1 MB per thread, but it varies by platform and is adjustable with -Xss.
  • Metaspace grows as classes are loaded. You can cap it with -XX:MaxMetaspaceSize. This is the modern replacement for the old PermGen area that existed before Java 8.
  • PC register is not something you read from Java code; it is a JVM bookkeeping concept that tells each thread which bytecode instruction to execute next.
  • Native method stack matters when you use JNI, a database driver with native code, or a library that jumps out of the JVM. Bugs there can show up as native crashes, not Java exceptions.

Memory hook: Heap = shared warehouse, Stack = personal desk, Metaspace = blueprint archive, PC register = bookmark, Native stack = translator room.

Real-world story: Imagine a checkout service in an e-commerce system. Each request thread uses the stack for validation calls, the heap for carts and payment objects, and Metaspace for loaded framework classes. A team once added a custom class-loader-based plugin system and created new loaders per request; class metadata kept piling up in Metaspace until the pods started throwing OutOfMemoryError: Metaspace, followed by repeated restarts and 500 errors. In another incident, a deeply nested address validator caused StackOverflowError because the recursion depth was much larger than the thread stack size.

The symptoms were easy to miss at first: rising memory usage, GC logs with frequent full collections, then sudden request failures, timeouts, and log lines that mentioned either metaspace exhaustion or stack overflow. The fix was to reuse class loaders, cap and monitor Metaspace, and rewrite the recursive validator into an iterative loop.

Java
import java.util.ArrayList;
import java.util.List;

public class MemoryAreasDemo {
    // Objects live on the heap. Keeping references here prevents early GC,
    // which makes the heap usage visible in a simple demo.
    private static final List<byte[]> HEAP_OBJECTS = new ArrayList<>();

    private static int depthCounter = 0;

    public static void main(String[] args) throws InterruptedException {
        printHeapStats("Start");

        allocateOnHeap();
        printHeapStats("After allocating 10 MB on the heap");

        // Thread stacks are per-thread. A small stack size hint helps the demo
        // reach StackOverflowError faster, but the JVM is allowed to treat it as a hint.
        Thread stackThread = new Thread(null, () -> {
            try {
                recurse();
            } catch (StackOverflowError e) {
                System.out.println("Stack demo: caught " + e.getClass().getSimpleName()
                        + " after about " + depthCounter + " recursive calls.");
            }
        }, "stack-demo", 64 * 1024);

        stackThread.start();
        stackThread.join();

        // Metaspace is where class metadata lives. Java code can observe class loading,
        // but it does not directly manage the metadata area.
        System.out.println("Metaspace note: class metadata is not stored on the Java heap.");

        // The PC register is conceptual JVM state: Java cannot read it directly.
        System.out.println("PC register note: each thread tracks its next bytecode step internally.");

        // Native method stack is used when the JVM calls native code.
        System.out.println("Native stack note: JNI/native calls use native frames managed outside Java objects.");

        HEAP_OBJECTS.clear();
        System.gc();
        Thread.sleep(200);
        printHeapStats("After clearing references and requesting GC");
    }

    private static void allocateOnHeap() {
        for (int i = 0; i < 10; i++) {
            // Each array is a heap object. The local variable 'i' is on the stack,
            // while the array data itself is allocated on the heap.
            HEAP_OBJECTS.add(new byte[1024 * 1024]);
        }
        System.out.println("Heap demo: stored 10 arrays of 1 MB each in a shared list.");
    }

    private static void recurse() {
        depthCounter++;

        // A few locals make the stack frame a little larger, which helps show that
        // every method call consumes per-thread stack space.
        long a = depthCounter;
        long b = a + 1;
        long c = b + 1;
        long d = c + 1;
        if ((a + b + c + d) == Long.MIN_VALUE) {
            System.out.println("Unreachable");
        }

        recurse();
    }

    private static void printHeapStats(String label) {
        Runtime rt = Runtime.getRuntime();
        long used = rt.totalMemory() - rt.freeMemory();
        System.out.printf("%s - heap used: %.2f MB, total: %.2f MB, max: %.2f MB%n",
                label,
                used / 1024.0 / 1024.0,
                rt.totalMemory() / 1024.0 / 1024.0,
                rt.maxMemory() / 1024.0 / 1024.0);
    }
}

Follow-up & Tricky Questions:

  • What is the difference between heap and stack? The heap stores shared objects that are garbage-collected, while each thread’s stack stores method frames and local variables. Stack memory is faster and automatically removed on return; heap memory lives until no references remain.
  • Why did Java 8 replace PermGen with Metaspace? PermGen had a fixed-size tuning problem and often caused class-loading failures. Metaspace moved class metadata to native memory and made it grow more naturally, while still allowing a cap with -XX:MaxMetaspaceSize.
  • Can an object live on the stack? Conceptually, Java objects are heap objects. JIT escape analysis can sometimes optimize away allocations or keep data in registers/stack-like storage, but that is an implementation optimization, not the normal Java memory model.
  • What happens to the PC register during a native call? The PC concept is defined for Java bytecode execution. For native methods, the JVM does not track a Java bytecode address in the same way, because control has moved to native code.
  • How do you tune stack vs heap? Heap is tuned mainly with -Xms and -Xmx; stack is tuned with -Xss. If you have many threads, a smaller stack can save memory, but too small a stack increases the risk of StackOverflowError.
  • What memory error do you get for too many classes? Usually OutOfMemoryError: Metaspace. This often points to class-loader leaks, excessive dynamic proxy generation, or redeployments that keep old class loaders alive.
  • Is String stored on the stack? No. A String object is on the heap; only the reference to it may be in a stack frame. String literals are interned, but they are still managed as heap objects in modern JVMs.
  • Is the PC register the same as the CPU program counter? It is similar in spirit, but in Java interview terms it is a JVM per-thread data area that records the current instruction location. The exact implementation detail is JVM-specific.
  • Can native code affect the Java heap? Yes. Native code can create, read, and modify Java objects through JNI, but it also consumes native memory and can crash the JVM if it misbehaves.

Common Mistakes:

  • Mistake: Saying the stack stores objects. Correction: The stack stores frames and references; the actual objects are usually on the heap.
  • Mistake: Treating Metaspace as part of the heap. Correction: Metaspace is native memory used for class metadata, separate from the Java heap.
  • Mistake: Forgetting that each thread has its own stack and PC register. Correction: These are per-thread, which is why one thread’s call path does not overwrite another’s.
  • Mistake: Mixing up StackOverflowError and heap OOM. Correction: Deep recursion usually overflows the stack; too many live objects usually exhaust the heap.

Memory Hook: Shared warehouse, personal desk, blueprint archive, bookmark, translator room. If you can recall that line, you can reconstruct the five JVM areas under pressure.

Cheat Sheet:

  • Heap = shared object memory, managed by GC.
  • Stack = per-thread method frames, locals, and returns.
  • Metaspace = class metadata in native memory; Java 8+.
  • PC register = per-thread current instruction pointer.
  • Native method stack = native/JNI call frames.
  • Key tuning flags: -Xmx, -Xms, -Xss, -XX:MaxMetaspaceSize.

Practice Tasks:

  • Run the code, then change the heap allocation count from 10 to 100 and observe how the heap numbers change.
  • Lower the thread stack hint or increase recursion work and see how quickly StackOverflowError appears.
  • Read about Java 7 vs Java 8 class metadata storage, and explain PermGen vs Metaspace in one minute.
Previous
Back to Questions←→to navigate
Next

Recommended Resources

Recommended Book

Cracking the Coding Interview

189 Programming Questions and Solutions

Recommended Book

Cracking the Coding Interview

189 Programming Questions and Solutions

import java.util.ArrayList; import java.util.List; public class MemoryAreasDemo { // Objects live on the heap. Keeping references here prevents early GC, // which makes the heap usage visible in a simple demo. private static final List<byte[]> HEAP_OBJECTS = new ArrayList<>(); private static int depthCounter = 0; public static void main(String[] args) throws InterruptedException { printHeapStats("Start"); allocateOnHeap(); printHeapStats("After allocating 10 MB on the heap"); // Thread stacks are per-thread. A small stack size hint helps the demo // reach StackOverflowError faster, but the JVM is allowed to treat it as a hint. Thread stackThread = new Thread(null, () -> { try { recurse(); } catch (StackOverflowError e) { System.out.println("Stack demo: caught " + e.getClass().getSimpleName() + " after about " + depthCounter + " recursive calls."); } }, "stack-demo", 64 * 1024); stackThread.start(); stackThread.join(); // Metaspace is where class metadata lives. Java code can observe class loading, // but it does not directly manage the metadata area. System.out.println("Metaspace note: class metadata is not stored on the Java heap."); // The PC register is conceptual JVM state: Java cannot read it directly. System.out.println("PC register note: each thread tracks its next bytecode step internally."); // Native method stack is used when the JVM calls native code. System.out.println("Native stack note: JNI/native calls use native frames managed outside Java objects."); HEAP_OBJECTS.clear(); System.gc(); Thread.sleep(200); printHeapStats("After clearing references and requesting GC"); } private static void allocateOnHeap() { for (int i = 0; i < 10; i++) { // Each array is a heap object. The local variable 'i' is on the stack, // while the array data itself is allocated on the heap. HEAP_OBJECTS.add(new byte[1024 * 1024]); } System.out.println("Heap demo: stored 10 arrays of 1 MB each in a shared list."); } private static void recurse() { depthCounter++; // A few locals make the stack frame a little larger, which helps show that // every method call consumes per-thread stack space. long a = depthCounter; long b = a + 1; long c = b + 1; long d = c + 1; if ((a + b + c + d) == Long.MIN_VALUE) { System.out.println("Unreachable"); } recurse(); } private static void printHeapStats(String label) { Runtime rt = Runtime.getRuntime(); long used = rt.totalMemory() - rt.freeMemory(); System.out.printf("%s - heap used: %.2f MB, total: %.2f MB, max: %.2f MB%n", label, used / 1024.0 / 1024.0, rt.totalMemory() / 1024.0 / 1024.0, rt.maxMemory() / 1024.0 / 1024.0); } }