Stack vs heap memory

Your laptop has gigabytes of RAM, yet a Java program can crash because it ran out of memory. That happens because the JVM splits memory into areas with different jobs: a small stack per thread for method calls, one big shared heap for objects, and metaspace for class data. You'll see exactly what goes where, how the two famous memory errors happen, which flags control the sizes, and then practise with 9 problems.

Module 8 · JVM Memory⏱ 24 minIntermediate9 practice problems
1 The story

The crash on an 8 GB laptop

Riya builds a referral tracker for the college fest: each student was referred by another, and her method follows the chain back to the first student by calling itself. One chain loops back on itself by mistake. The program dies in a second with java.lang.StackOverflowError, while Task Manager shows 6 GB of RAM still free.

Stack: small, one per thread main()findRoot(17)findRoot(42)findRoot(17)findRoot(42) StackOverflowError references Heap: big, shared by all threads Student #1roll 17Student #2roll 42Student #3roll 58ArrayList #43 students plenty of room left myphatshala.in
Riya

Sir, it says I ran out of stack. But my laptop has 8 GB and most of it is free!

Arjun Sir

The JVM doesn't hand your program one big bag of memory. Every thread gets its own stack, about 1 MB by default, just for method calls. Your loop in the referral chain made calls that never returned, so that small stack filled up. The RAM was never the problem.

Riya

Then where do my 3,000 Student objects go? Not into that 1 MB?

Arjun Sir

No, objects go to the heap, a much bigger area shared by all threads. Your local variables on the stack only hold references, like addresses, to those objects. Learn which is which and you'll read every memory error like a sentence.

2 Definition

What are the stack and the heap?

Formal definition (write this in exams)

At run time the JVM divides memory into run-time data areas. Each thread has its own JVM stack, made of frames: a frame is pushed for every method call and holds that call's local variables, its operand stack and the data needed to return to the caller; it is popped when the method returns. The heap is one area shared by all threads in which every object and array is allocated; it is managed by the garbage collector. Class metadata (bytecode, field and method descriptions) is kept in the method area, which HotSpot implements as Metaspace in native memory since Java 8.

In simple words

The stack is each worker's notepad: one page per job in progress, torn off when the job is done. The heap is the shared warehouse where all the actual goods (objects) are kept. A notepad page only says where in the warehouse a thing is.

Words you'll meet in this lesson

FrameOne method call's private space on the stack. Pushed on call, popped on return.
Local variableA parameter or variable declared inside a method. Lives in that call's frame.
Operand stackA small scratch area in each frame where bytecode does its arithmetic.
ReferenceA value that points to an object on the heap. Copying it does not copy the object.
HeapShared memory for all objects and arrays. Cleaned up by the garbage collector.
MetaspaceNative memory for class data (Java 8+). Replaced the old PermGen area.

Memory areas at a glance

StackOne per thread. Frames for method calls. Size set by -Xss (1 MB by default on 64-bit Linux). Full → StackOverflowError
HeapOne per JVM, shared. All objects and arrays (and the String pool since Java 7). -Xms start size, -Xmx maximum (default about ¼ of RAM). Full → OutOfMemoryError: Java heap space
MetaspaceClass metadata in native memory (Java 8+). Unlimited by default; cap with -XX:MaxMetaspaceSize. Full → OutOfMemoryError: Metaspace
Local primitiveint count = 2; → the value 2 sits in the frame
Local object variableOrder o = new Order(); → the reference sits in the frame, the object on the heap
Field of an objectInside the object, so on the heap, even if it is an int

Exam tip: “primitives on the stack, objects on the heap” is only half true. It holds for local variables. A primitive field such as int balance is part of its object, so it lives on the heap. Examiners love this trap.

3 Types and ways to use it

Three areas, three jobs

Stack (per thread)

Frames for calls in progress: parameters, locals, partial results. Last in, first out. Tiny and fast; cleaned up automatically on return.

-Xss

Heap (shared)

Every object and array, from all threads. Objects stay as long as something references them; then the garbage collector reclaims them.

-Xms / -Xmx

Metaspace (class data)

For each loaded class: its bytecode, method and field descriptions, constant pool. Lives outside the heap, in native memory.

-XX:MaxMetaspaceSize

Copying a value vs copying a reference

public class Main {
    static class Product {
        String name;
        int stock;          // a field: lives inside the object, on the heap
    }

    public static void main(String[] args) {
        int a = 10;                   // local primitive: the value is in main's frame
        int b = a;                    // copies the value
        b = 99;

        Product p = new Product();    // object on the heap; p holds a reference
        Product q = p;                // copies the reference, not the object
        q.stock = 99;

        System.out.println("a = " + a + ", b = " + b);
        System.out.println("p.stock = " + p.stock + ", same object? " + (p == q));
        System.out.println("default name = " + p.name);
    }
}
a = 10, b = 99 p.stock = 99, same object? true default name = null

Stack vs heap, side by side

StackHeap
Belongs toOne threadThe whole JVM, all threads
HoldsFrames: locals (primitive values and references), operand stack, return infoObjects and arrays, including their fields
Freed whenThe method returns (frame popped)No references remain and the GC runs
Allocation costVery cheap: move the stack pointerAlso cheap (a pointer bump in a per-thread buffer), but objects must be collected later
Typical sizeAbout 1 MB per thread (-Xss)Hundreds of MB to many GB (-Xmx)
Error when fullStackOverflowErrorOutOfMemoryError: Java heap space
Thread safetyPrivate to its thread: nothing to shareShared: objects used by many threads need care (Module 12)

Not a fix: catching StackOverflowError or OutOfMemoryError and carrying on hides the real bug. Both extend VirtualMachineError, which signals the JVM itself is in trouble. Find the runaway recursion or the objects that should not be kept, or size the memory properly.

4 Step by step

From java Main to the first object

  1. The JVM reserves its areas

    java -Xms256m -Xmx1g -Xss1m Main
    Before any of your code runs, the JVM sets up the heap (start 256 MB, may grow to 1 GB) and creates the main thread with its own stack (1 MB here). Without flags it picks defaults from the machine's memory.
  2. The class is loaded

    public class Main { … }
    The class loader reads Main.class. Its bytecode, constant pool and method descriptions go into metaspace. Static field values are kept with the class's Class object on the heap (Lesson 8.4 covers loading in detail).
  3. main is called: the first frame is pushed

    static int addGst(int price) {
        int tax = price * 18 / 100;
        return price + tax;
    }
    The frame size is decided at compile time. javap -v on this method prints stack=2, locals=2: two slots for locals (price, tax) and room for two values on the operand stack, where imul and idiv do the maths.
  4. new creates an object on the heap

    Order o = new Order(101, 500.0);
    The bytecode is new (allocate and zero-fill), dup, then invokespecial Order.<init> (run the constructor in its own frame), then astore_1: the reference is stored in local slot 1 of main's frame.
  5. The method returns: the frame is popped

    Locals vanish instantly. Objects stay on the heap as long as some reference to them remains.

    Returning just moves the stack pointer back and jumps to the saved return point in the caller. No cleanup work is needed, which is why the stack is so fast. The heap needs the garbage collector instead (Lesson 8.3).
5 See it happen

Frames, objects and two crashes: both views

Left: the line being run and what the console shows. Right: main thread's stack, the heap, and metaspace.

Where everything lives 6 steps
What you see int count = 2; Order first = new Order(101, 500.0); double g = withGst(first); System.out.println("g = " + g); return n * factorial(n - 1); chunks.add(new byte[1024 * 1024]); // main's frame: count = 2 // the heap has no Order yet first → Order {id 101, amount 500.0} // withGst is running // tax = 90.0 in its frame g = 590.0 Exception in thread "main" java.lang.StackOverflowError at Main.factorial(Main.java:3) at Main.factorial(Main.java:3) … (many more) java.lang.OutOfMemoryError: Java heap space at Main.main(Main.java:7) A primitive local is just a value. new puts the object on the heap. A method call gets its own frame. The call is over; the object is still there. No base case: the calls never end. Every chunk is kept: the heap fills up. myphatshala.in
Behind the screen Stack (main thread) Heap (shared) main() count = 2 count = 2 g = 590.0 first → #1 (no objects yet) #1 Order id=101 amount=500.0 withGst() order → #1 tax = 90.0 factorial(5)factorial(4)factorial(3)factorial(2) … no room for a frame byte[]byte[]byte[]byte[]byte[]byte[]byte[]byte[]byte[]byte[]byte[]byte[] full: all still referenced Metaspace class Main class Main, class Order main's frame holds the value 2 itself. The frame holds an arrow; the object is on the heap. New frame on top; both frames point to #1. withGst's frame is popped; #1 stays. Frames pile up until the 1 MB stack is full. The heap reaches -Xmx; the GC can't free anything. myphatshala.in
6 Real-world use

Stack and heap in real systems

Backend developers set memory flags, read memory errors and size containers every week. These are the situations where this lesson matters most.

Watch it with an example: two customers, two threads, one heap

A shopping server handles Asha and Ravi at the same time. Each request runs on its own thread with its own stack, but both share the one heap.

Separate stacks, shared heap 5 steps
Animated example Thread 1: Asha (stack) handle() addToCart() qty = 2 cart → (empty) Thread 2: Ravi (stack) handle() addToCart() qty = 1 cart → (empty) HEAP (shared by all threads) CartAsha Catalog1 object CartRavi each thread gets its own stack local variables live in each frame objects live on the heap both threads read the same Catalog frames gone; carts stay while referenced stack: small, private, auto-cleaned · heap: big, shared, GC myphatshala.in
heapother

1. Sizing a service in a container

A service gets a 2 GB container. The heap (-Xmx) must leave room for metaspace, thread stacks and other native memory, or the container is killed from outside. Teams often use -XX:MaxRAMPercentage so the heap follows the container limit.

Container limit→-XX:MaxRAMPercentage=75→Heap + headroom
one stack each

2. Thread-heavy servers

Every platform thread reserves its own stack. A server with 800 request threads reserves up to 800 × 1 MB of stack address space in addition to the heap. That is one reason servers use bounded thread pools (Module 12).

800 threads→-Xss1m each→Plan the memory
{"a":{"b":{"c":{"d":{"e":{… 50,000 levelsStackOverflowError

3. Deeply nested input

Recursive parsers for JSON, XML or expression trees use one frame per nesting level. A maliciously deep document can overflow the stack, so production parsers limit nesting depth or use loops with an explicit stack.

Nested input→depth limit / loop→No crash
OutOfMemoryErrorDumping heap tojava_pid4321.hprof

4. Investigating an OutOfMemoryError

With -XX:+HeapDumpOnOutOfMemoryError, the JVM writes a snapshot of the heap when it runs out. Tools like Eclipse MAT then show which objects filled it and which references kept them alive.

OOM in production→heap dump→Find the culprit
class Proxy$1class Proxy$2class Proxy$3 …

5. Frameworks that generate classes

Frameworks create classes at run time (proxies, generated serializers). A bug that keeps generating new classes, or an app server that redeploys without releasing old classes, slowly fills metaspace until OutOfMemoryError: Metaspace.

Generated classes→metaspace grows→Cap and monitor it

Examples are simplified. Exact default sizes depend on the JVM version, operating system and container settings; check yours with java -XX:+PrintFlagsFinal -version.

7 Run the code

One object, two references, one method call

public class Main {
    static class Order {
        int id;
        double amount;
        Order(int id, double amount) {
            this.id = id;
            this.amount = amount;
        }
    }

    static double withGst(Order order) {
        double tax = order.amount * 0.18;
        return order.amount + tax;
    }

    public static void main(String[] args) {
        int count = 2;
        Order first = new Order(101, 500.0);
        Order same = first;
        same.amount = 1000.0;
        System.out.println("Orders: " + count);
        System.out.println("first.amount = " + first.amount);
        System.out.println("Same object? " + (first == same));
        System.out.println("With GST: " + withGst(first));
    }
}
Orders: 2 first.amount = 1000.0 Same object? true With GST: 1180.0
  • int count = 2;A primitive local: the value lives in main's frame.
  • Order same = first;Copies the reference. Still one Order object on the heap.
  • same.amount = 1000.0;Writes into that one object, so first.amount changes too.
  • withGst(first)Pushes a frame with order and tax; popped when it returns 1180.0.
8 Line by line

A wallet on the heap, a payment on the stack

Recorded from the real JVM. Watch pay's frame appear and disappear while the Wallet object (#2) stays on the heap and changes.

Worked examples: real problems, solved and traced in memory

Three business programs, traced on the real JVM. Watch each method call push a frame on the stack, watch it pop on return, and notice that objects and arrays always live on the heap while frames only hold values and references.

Example 1E-commerce · checkout

Frames pile up and unwind during a bill

Problem statement

A checkout page computes a bill: for each cart line, lineTotal multiplies price by quantity and calls addGst to add 18% GST (in whole rupees). Print the bill and see how deep the stack gets.

Input
prices = {200, 50}
qtys   = {2, 3}
Expected output
Bill: Rs 649

Approach

  1. Keep prices and quantities in two arrays: they are objects, so they live on the heap.
  2. main calls lineTotal, which calls addGst: three frames at the deepest point.
  3. Each frame has its own locals (base, tax). They vanish when the method returns; only the returned value travels back.

Solution: run it live

What happens in memory, step by step

Recorded from the real JVM: press Next to execute one line at a time. Watch method calls push and pop frames on the stack, and objects (numbered #1, #2 …) appear on the heap.

Example 2Banking · deposits

A method changes the account, not the caller's variable

Problem statement

A bank's deposit method adds money to an Account object. A buggy helper replace tries to swap the caller's account for a new one. Predict what main prints, then check the memory trace.

Input
account: Riya, Rs 1000
deposit: Rs 500
Expected output
Riya 1500.0, cash 500.0

Approach

  1. The Account object lives on the heap; riya in main's frame holds a reference to it.
  2. Java passes a copy of every argument. For an object, the copy is the reference, so deposit reaches the same object.
  3. Assigning a new object to the parameter inside replace changes only the copy in replace's frame.

Solution: run it live

What happens in memory, step by step

Recorded from the real JVM: press Next to execute one line at a time. Watch method calls push and pop frames on the stack, and objects (numbered #1, #2 …) appear on the heap.

Example 3Logistics · route planner

Recursion: one frame per delivery leg

Problem statement

A delivery route has legs of 12, 30 and 8 km. A recursive method totalKm(legs, i) returns the distance from leg i to the end. Trace it and count the frames.

Input
legs = {12, 30, 8}
Expected output
Route: 50 km

Approach

  1. Base case: when i reaches legs.length there are no legs left, so return 0.
  2. Otherwise ask the next call for the rest of the route, then add this leg.
  3. Every call is a new frame with its own i and rest. With n legs there are n + 1 frames on top of main, which is why a very long route (or a missing base case) ends in StackOverflowError.

Solution: run it live

What happens in memory, step by step

Recorded from the real JVM: press Next to execute one line at a time. Watch method calls push and pop frames on the stack, and objects (numbered #1, #2 …) appear on the heap.

9 Quiz and revision

Check yourself

Quick revision

Lesson 8.1 in one screen

  • Stack: one per thread, made of frames; a frame per call holds locals, the operand stack and return info.
  • Heap: one per JVM, shared; all objects and arrays (with their fields) live here.
  • Local primitive → value in the frame. Local object variable → reference in the frame, object on the heap.
  • Metaspace (Java 8+, native memory) holds class metadata; it replaced PermGen.
  • Flags: -Xss stack per thread, -Xms/-Xmx heap start/max, -XX:MaxMetaspaceSize.
  • Stack full → StackOverflowError; heap full → OutOfMemoryError: Java heap space.
  • Java passes copies: of values for primitives, of references for objects.

Interview and viva questions

What is the difference between stack and heap memory in Java?

The stack is per thread and stores frames for active method calls: local variables, partial results and return information. It is freed automatically when a method returns. The heap is shared by all threads and stores every object and array; objects are removed by the garbage collector once nothing references them. Stack overflow gives StackOverflowError; a full heap gives OutOfMemoryError.

What exactly is inside a stack frame?

A local variable array (parameters, including this for instance methods, and locals), an operand stack used by bytecode instructions for intermediate values, and frame data such as a link to the class's run-time constant pool and where to continue in the caller after returning. The sizes are fixed at compile time; javap -v shows them as stack= and locals=.

What replaced PermGen and why does it matter?

Java 8 replaced PermGen with Metaspace. Class metadata moved out of the Java heap into native memory, which is unlimited by default, so the old OutOfMemoryError: PermGen space disappeared. Metaspace can still fill up if classes keep being created or class loaders leak, giving OutOfMemoryError: Metaspace; it can be capped with -XX:MaxMetaspaceSize.

Is Java pass-by-value or pass-by-reference?

Always pass-by-value. For a primitive the value is the number itself; for an object the value is the reference. The called method can change the object through its copy of the reference, but reassigning the parameter never affects the caller's variable.

How would you fix a StackOverflowError in production?

Read the stack trace: the repeating lines show which method recurses. Usually there is a missing or wrong base case, or a cycle in the data (for example a category that is its own ancestor). Fix the logic, add cycle detection, or convert deep recursion to a loop. Increasing -Xss only helps for legitimately deep but bounded recursion.

Practice problems

These problems train you to see where every value lives: in a frame on the stack, or in an object on the heap. Predict the output on paper first, then check. The last three are business tasks.

P1Easy

Copy a value, copy a reference

Show the difference between copying a primitive and copying a reference. Change the copy in both cases and print the originals.

Input
int a = 10; int[] scores = {50, 60}
Expected output
a = 10, b = 20
scores = [99, 60]
Show solution and explanation
import java.util.Arrays;

public class Main {
    public static void main(String[] args) {
        int a = 10;
        int b = a;          // copies the value 10
        b = 20;

        int[] scores = {50, 60};
        int[] alias = scores;   // copies the reference
        alias[0] = 99;

        System.out.println("a = " + a + ", b = " + b);
        System.out.println("scores = " + Arrays.toString(scores));
    }
}
a = 10, b = 20 scores = [99, 60]

How it works: a and b are two separate boxes in main's frame, so changing b can't touch a. scores and alias are two boxes holding the same address; there is only one array on the heap, so the change is visible through both names.

P2Easy

Fields get defaults, locals don't

Create a Student object without setting its fields and print them. Explain why the same trick with a local variable would not compile.

Input
class Student { String name; int marks; boolean passed; double fee; }
Expected output
null 0 false 0.0
Show solution and explanation
public class Main {
    static class Student {
        String name;
        int marks;
        boolean passed;
        double fee;
    }

    public static void main(String[] args) {
        Student s = new Student();
        System.out.println(s.name + " " + s.marks + " " + s.passed + " " + s.fee);
        // int local; System.out.println(local);  -> compile error
    }
}
null 0 false 0.0

How it works: when new creates an object on the heap, the JVM zero-fills its memory first, so every field starts at null, 0, false or 0.0. Local variables live in a frame and are not zero-filled; javac refuses to let you read one before assigning it ("variable might not have been initialized").

P3Easy

Swap that doesn't swap

Write swap(int x, int y) and swap(int[] pair). Show that only the array version changes the caller's data.

Input
p = 5, q = 9
pair = {5, 9}
Expected output
p = 5, q = 9
pair = [9, 5]
Show solution and explanation
import java.util.Arrays;

public class Main {
    static void swap(int x, int y) {
        int t = x;
        x = y;
        y = t;
    }

    static void swap(int[] pair) {
        int t = pair[0];
        pair[0] = pair[1];
        pair[1] = t;
    }

    public static void main(String[] args) {
        int p = 5, q = 9;
        swap(p, q);
        System.out.println("p = " + p + ", q = " + q);

        int[] pair = {5, 9};
        swap(pair);
        System.out.println("pair = " + Arrays.toString(pair));
    }
}
p = 5, q = 9 pair = [9, 5]

How it works: the first swap receives copies of 5 and 9 in its own frame and swaps the copies; the frame is popped and the swap is lost. The second receives a copy of the reference, so it swaps the elements of the one array on the heap. Java is always pass-by-value; for objects, the value passed is the reference.

P4Medium

Count the frames in a recursion

Compute 5! recursively and print how deep the stack went, using a static counter that rises on each call and falls on each return.

Input
n = 5
Expected output
5! = 120
Deepest stack: 5 factorial frames (+ main)
Depth after return: 0
Show solution and explanation
public class Main {
    static int depth = 0;
    static int maxDepth = 0;

    static long factorial(int n) {
        depth++;
        if (depth > maxDepth) maxDepth = depth;
        long result = (n <= 1) ? 1 : n * factorial(n - 1);
        depth--;
        return result;
    }

    public static void main(String[] args) {
        System.out.println("5! = " + factorial(5));
        System.out.println("Deepest stack: " + maxDepth + " factorial frames (+ main)");
        System.out.println("Depth after return: " + depth);
    }
}
5! = 120 Deepest stack: 5 factorial frames (+ main) Depth after return: 0

How it works: each call pushes a frame and increments depth; each return pops a frame and decrements it. factorial(5) calls 4, 3, 2, 1, so 5 factorial frames are on the stack at the deepest point. The counters are static, so they are not in any frame: they belong to the class and survive all the pushes and pops.

P5Medium

Recursion vs loop for a long series

Sum the numbers 1 to 1,00,000. A recursive version needs one frame per number and risks StackOverflowError; write the loop version and show it uses a single frame.

Input
n = 100000
Expected output
Sum 1..100000 = 5000050000
Formula check  = 5000050000
Show solution and explanation
public class Main {
    // Recursive idea (do NOT use for large n):
    // static long sum(int n) { return n == 0 ? 0 : n + sum(n - 1); }

    static long sumLoop(int n) {
        long total = 0;
        for (int i = 1; i <= n; i++) {
            total += i;
        }
        return total;
    }

    public static void main(String[] args) {
        int n = 100_000;
        System.out.println("Sum 1.." + n + " = " + sumLoop(n));
        System.out.println("Formula check  = " + (long) n * (n + 1) / 2);
    }
}
Sum 1..100000 = 5000050000 Formula check = 5000050000

How it works: the recursive version would need 1,00,000 frames at once. With the default 1 MB stack that is usually too many, so it fails with StackOverflowError (the exact limit depends on the machine and JIT). The loop reuses the same two locals in one frame, whatever n is. The cast to long in the formula prevents int overflow.

P6Medium

Catch a runaway recursion and report it

A menu-category tree accidentally contains a loop: category A's parent is B and B's parent is A. Finding the root recursively never ends. Catch the StackOverflowError once, at the top, and print a clear message.

Input
parent[0] = 1, parent[1] = 0  (a cycle)
Expected output
Category tree has a cycle: StackOverflowError
Program continues
Show solution and explanation
public class Main {
    static int[] parent = {1, 0};   // 0 -> 1 -> 0 -> 1 ... never reaches -1

    static int findRoot(int c) {
        if (parent[c] == -1) return c;
        return findRoot(parent[c]);
    }

    public static void main(String[] args) {
        try {
            System.out.println("Root: " + findRoot(0));
        } catch (StackOverflowError e) {
            System.out.println("Category tree has a cycle: " + e.getClass().getSimpleName());
        }
        System.out.println("Program continues");
    }
}
Category tree has a cycle: StackOverflowError Program continues

How it works: each findRoot call pushes a frame and none ever returns, so the thread's stack fills and the JVM throws StackOverflowError. When it propagates to main, all those frames have already been popped, so the program can continue. This only reports the bug; the real fix is to detect the cycle (for example with a visited array) or to walk the tree with a loop.

P7Business problem

Batch the orders, reuse one buffer

A warehouse job processes 10 order amounts in batches of 4. Instead of creating a new array for each batch, reuse one buffer array. Print each batch total and the grand total.

Input
amounts = {1200, 450, 980, 300, 2200, 150, 640, 875, 410, 990}
batch size = 4
Expected output
Batch 1 (4 orders): Rs 2930
Batch 2 (4 orders): Rs 3865
Batch 3 (2 orders): Rs 1400
Grand total: Rs 8195
Show solution and explanation
public class Main {
    public static void main(String[] args) {
        int[] amounts = {1200, 450, 980, 300, 2200, 150, 640, 875, 410, 990};
        int[] buffer = new int[4];          // one object on the heap, reused
        int grand = 0, batchNo = 0;
        for (int start = 0; start < amounts.length; start += buffer.length) {
            int count = Math.min(buffer.length, amounts.length - start);
            System.arraycopy(amounts, start, buffer, 0, count);
            int batchTotal = 0;
            for (int i = 0; i < count; i++) batchTotal += buffer[i];
            batchNo++;
            grand += batchTotal;
            System.out.println("Batch " + batchNo + " (" + count + " orders): Rs " + batchTotal);
        }
        System.out.println("Grand total: Rs " + grand);
    }
}
Batch 1 (4 orders): Rs 2930 Batch 2 (4 orders): Rs 3865 Batch 3 (2 orders): Rs 1400 Grand total: Rs 8195

How it works:

  1. Only two arrays ever exist on the heap: the input and the 4-slot buffer. Batch processing keeps memory use flat however many orders arrive, which is how real import jobs avoid OutOfMemoryError.
  2. count handles the last, shorter batch (2 orders).
  3. The primitives start, count and batchTotal are just slots in main's frame, reused every round.
P8Business problem

Month-end interest without a deep stack

A savings account of Rs 1,00,000 earns 0.5% interest per month, credited monthly for 12 months. A teammate wrote it recursively. Rewrite it with a loop (one frame for any number of months) and print the balance after 12 months, rounded to paise.

Input
principal = 100000, monthly rate = 0.5%, months = 12
Expected output
Balance after 12 months: Rs 106167.78
Interest earned: Rs 6167.78
Show solution and explanation
public class Main {
    static double balanceAfter(double principal, double monthlyRate, int months) {
        double balance = principal;
        for (int m = 1; m <= months; m++) {
            balance += balance * monthlyRate / 100;
        }
        return balance;
    }

    public static void main(String[] args) {
        double b = balanceAfter(100000, 0.5, 12);
        System.out.printf("Balance after 12 months: Rs %.2f%n", b);
        System.out.printf("Interest earned: Rs %.2f%n", b - 100000);
    }
}
Balance after 12 months: Rs 106167.78 Interest earned: Rs 6167.78

How it works:

  1. The recursive version (balanceAfter(p * 1.005, rate, months - 1)) works for 12 months, but a 30-year loan schedule with daily steps would need about 11,000 frames. The loop always uses one frame.
  2. Each month's interest is calculated on the updated balance: that is compounding.
  3. Real banking code uses BigDecimal for money; double is fine for this exercise because we only round for display.
P9Business problem

Shared cart object between two screens

In a shopping app, the cart screen and the checkout screen both hold a reference to the same Cart object. Adding an item on one screen must show on the other. Model it and print what each screen sees.

Input
cart screen adds Rs 499 shoes, checkout adds Rs 99 delivery
Expected output
Cart screen sees: 2 items, Rs 598
Checkout sees:    2 items, Rs 598
Objects on the heap for carts: 1
Show solution and explanation
public class Main {
    static class Cart {
        int items;
        int total;
        void add(int price) {
            items++;
            total += price;
        }
    }

    public static void main(String[] args) {
        Cart cart = new Cart();
        Cart cartScreen = cart;       // both screens get the same reference
        Cart checkoutScreen = cart;

        cartScreen.add(499);
        checkoutScreen.add(99);

        System.out.println("Cart screen sees: " + cartScreen.items + " items, Rs " + cartScreen.total);
        System.out.println("Checkout sees:    " + checkoutScreen.items + " items, Rs " + checkoutScreen.total);
        System.out.println("Objects on the heap for carts: " + (cartScreen == checkoutScreen ? 1 : 2));
    }
}
Cart screen sees: 2 items, Rs 598 Checkout sees: 2 items, Rs 598 Objects on the heap for carts: 1

How it works: there is one Cart object on the heap and three references to it on main's stack frame. Every add call pushes a frame whose this points to that same object, so both screens always agree. If each screen had called new Cart(), they would hold two independent carts and the totals would drift apart.

Real-world scenario questions (asked in company interviews)

Memory questions are favourites in backend interviews because every production team has fought these errors. Decide your answer first, then open the model answer.

Fintech · referral rewardsFresher

The reward job that dies on one customer

A nightly job credits referral rewards by following each customer's “referred by” link recursively up to the first customer. It runs fine for months, then crashes every night with StackOverflowError. The logs show the same two customer IDs alternating in the stack trace. What happened and how do you fix it?

The interviewer is checking: can you read a stack trace and connect StackOverflowError to recursion and bad data?

Show a strong answer
  • The alternating IDs mean a cycle: A was referred by B and B by A (bad data from a migration or a bug). The recursion never reaches a customer with no referrer, so frames pile up until the thread's stack is full.
  • Fix the data, and make the code safe: keep a Set of visited IDs and stop on a repeat, or walk the chain with a loop and a maximum depth.
  • Add a database constraint or validation so a customer can't become their own ancestor.

Avoid: “Increase -Xss.” A cycle is infinite; no stack size is enough.

E-commerce · containers3+ years

Killed without an OutOfMemoryError

An order service runs in a container with a 1 GB memory limit and is started with -Xmx1g. Under festival-sale load, the container is restarted by the platform with “OOMKilled”, but the application logs show no OutOfMemoryError. Explain and fix.

The interviewer is checking: do you know the JVM uses more than the heap?

Show a strong answer

The heap alone was allowed to use the whole 1 GB. The JVM process also needs metaspace, a stack for every thread, JIT-compiled code, GC structures and native buffers. Total process memory crossed the container limit, so the operating system killed it before the heap was full; the JVM never got a chance to throw.

  • Leave headroom: for example -Xmx700m, or -XX:MaxRAMPercentage=70 so it adapts to the limit.
  • Bound thread pools (each thread has its own stack) and consider capping metaspace.
  • Monitor process memory (RSS), not only heap usage.

Avoid: making -Xmx equal to the container limit.

Logistics · tracking API1–3 years

OutOfMemoryError after a bulk export

A tracking API has an endpoint that exports all shipments for a date range. It loads every shipment into one List, builds a big report String and returns it. When a client asks for a full year (about 40 lakh shipments), the server logs java.lang.OutOfMemoryError: Java heap space and other requests start failing too. What would you change?

The interviewer is checking: do you understand that the heap is shared and that live objects can't be collected?

Show a strong answer
  • All 40 lakh objects plus the report are reachable at once, so the GC can free nothing. The heap is shared, so every other request on the server fails too.
  • Stream the export: read in pages (say 5,000 rows), write each page to the response or a file, and drop it before reading the next. Memory use stays flat.
  • Limit the date range or make big exports an asynchronous job that emails a download link.
  • Enable -XX:+HeapDumpOnOutOfMemoryError so the next incident can be analysed.

Avoid: only raising -Xmx. The next client will ask for two years.

Campus interview · fundamentalsFresher

“Where is each of these stored?”

The interviewer writes: class Bill { static int count; double amount; } and in main: int qty = 3; Bill b = new Bill();. They ask where qty, b, the Bill object, amount, count and the code of class Bill are stored.

The interviewer is checking: can you go beyond “primitives on stack, objects on heap”?

Show a strong answer
  • qty: a local primitive, so the value 3 is in main's frame on the stack.
  • b: a local reference, in main's frame; the Bill object it points to is on the heap.
  • amount: an instance field, so inside the Bill object on the heap.
  • The class metadata and bytecode of Bill: metaspace (Java 8+). The static count value belongs to the class, not to any object; in HotSpot it is kept with the class's Class object on the heap.

Avoid: saying amount is on the stack because it is a double.