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.
Sir, it says I ran out of stack. But my laptop has 8 GB and most of it is free!
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.
Then where do my 3,000 Student objects go? Not into that 1 MB?
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.
What are the stack and the heap?
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.
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
Memory areas at a glance
| Stack | One per thread. Frames for method calls. Size set by -Xss (1 MB by default on 64-bit Linux). Full → StackOverflowError |
|---|---|
| Heap | One 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 |
| Metaspace | Class metadata in native memory (Java 8+). Unlimited by default; cap with -XX:MaxMetaspaceSize. Full → OutOfMemoryError: Metaspace |
| Local primitive | int count = 2; → the value 2 sits in the frame |
| Local object variable | Order o = new Order(); → the reference sits in the frame, the object on the heap |
| Field of an object | Inside 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.
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.
-XssHeap (shared)
Every object and array, from all threads. Objects stay as long as something references them; then the garbage collector reclaims them.
-Xms / -XmxMetaspace (class data)
For each loaded class: its bytecode, method and field descriptions, constant pool. Lives outside the heap, in native memory.
-XX:MaxMetaspaceSizeCopying 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);
}
}Stack vs heap, side by side
| Stack | Heap | |
|---|---|---|
| Belongs to | One thread | The whole JVM, all threads |
| Holds | Frames: locals (primitive values and references), operand stack, return info | Objects and arrays, including their fields |
| Freed when | The method returns (frame popped) | No references remain and the GC runs |
| Allocation cost | Very cheap: move the stack pointer | Also cheap (a pointer bump in a per-thread buffer), but objects must be collected later |
| Typical size | About 1 MB per thread (-Xss) | Hundreds of MB to many GB (-Xmx) |
| Error when full | StackOverflowError | OutOfMemoryError: Java heap space |
| Thread safety | Private to its thread: nothing to share | Shared: 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.
From java Main to the first object
The JVM reserves its areas
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.java -Xms256m -Xmx1g -Xss1m MainThe class is loaded
The class loader readspublic class Main { … }Main.class. Its bytecode, constant pool and method descriptions go into metaspace. Static field values are kept with the class'sClassobject on the heap (Lesson 8.4 covers loading in detail).main is called: the first frame is pushed
The frame size is decided at compile time.static int addGst(int price) { int tax = price * 18 / 100; return price + tax; }javap -von this method printsstack=2, locals=2: two slots for locals (price,tax) and room for two values on the operand stack, whereimulandidivdo the maths.new creates an object on the heap
The bytecode isOrder o = new Order(101, 500.0);new(allocate and zero-fill),dup, theninvokespecial Order.<init>(run the constructor in its own frame), thenastore_1: the reference is stored in local slot 1 of main's frame.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).
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.
- Primitive local:
countholds the value 2 directly. - new: an
Orderobject is created;firstrefers to it. - Call:
withGst(first)starts running with its own variables. - Return: the method hands back 590.0 and is finished.
- StackOverflowError: a recursive method with no base case calls itself until the JVM gives up.
- OutOfMemoryError: a loop keeps adding 1 MB arrays to a list until no heap is left.
- main's frame is at the bottom of the stack.
countoccupies one slot and holds 2. Metaspace already has classMain. - The
Orderclass is loaded into metaspace, object #1 is allocated on the heap, and main's slotfirststores its address. - Calling
withGstpushes a new frame. Its parameterorderis a copy of the reference, so two frames point at one object. - On return, the top frame is popped and its locals disappear. #1 is still referenced by
first, so it stays. - Each recursive call pushes another frame and none returns. When the thread's stack (1 MB by default) has no space for the next frame, the JVM throws
StackOverflowError. - The list keeps every 1 MB array reachable, so the garbage collector can't free any of them. When the heap reaches its maximum (
-Xmx), allocation fails withOutOfMemoryError.
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.
- Two requests at once: the server runs each on a separate thread. Every thread has its own stack, starting with a frame for
handle(). - Method calls:
addToCart()pushes a frame on each thread's stack. Asha'sqty = 2and Ravi'sqty = 1can't interfere: they are in different stacks. - Objects: each request creates a Cart object. Objects never live on a stack; they are on the heap, and the frames only hold references to them.
- Shared data: both threads use the same Catalog object on the heap. Sharing saves memory, but shared objects that change need care (Module 12: synchronization).
- Finish: when the methods return, their frames vanish instantly. The carts stay on the heap as long as something (say, the user's session) refers to them; the garbage collector frees them later.
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.
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).
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.
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.
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.
Examples are simplified. Exact default sizes depend on the JVM version, operating system and container settings; check yours with java -XX:+PrintFlagsFinal -version.
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));
}
}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, sofirst.amountchanges too.withGst(first)Pushes a frame withorderandtax; popped when it returns 1180.0.
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.
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.
prices = {200, 50}
qtys = {2, 3}Bill: Rs 649
Approach
- Keep prices and quantities in two arrays: they are objects, so they live on the heap.
maincallslineTotal, which callsaddGst: three frames at the deepest point.- 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.
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.
account: Riya, Rs 1000 deposit: Rs 500
Riya 1500.0, cash 500.0
Approach
- The
Accountobject lives on the heap;riyain main's frame holds a reference to it. - Java passes a copy of every argument. For an object, the copy is the reference, so
depositreaches the same object. - Assigning a new object to the parameter inside
replacechanges 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.
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.
legs = {12, 30, 8}Route: 50 km
Approach
- Base case: when
ireacheslegs.lengththere are no legs left, so return 0. - Otherwise ask the next call for the rest of the route, then add this leg.
- Every call is a new frame with its own
iandrest. 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 inStackOverflowError.
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.
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:
-Xssstack per thread,-Xms/-Xmxheap 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.
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.
int a = 10; int[] scores = {50, 60}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));
}
}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.
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.
class Student { String name; int marks; boolean passed; double fee; }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
}
}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").
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.
p = 5, q = 9
pair = {5, 9}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));
}
}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.
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.
n = 5
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);
}
}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.
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.
n = 100000
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);
}
}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.
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.
parent[0] = 1, parent[1] = 0 (a cycle)
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");
}
}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.
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.
amounts = {1200, 450, 980, 300, 2200, 150, 640, 875, 410, 990}
batch size = 4Batch 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);
}
}How it works:
- 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. counthandles the last, shorter batch (2 orders).- The primitives
start,countandbatchTotalare just slots in main's frame, reused every round.
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.
principal = 100000, monthly rate = 0.5%, months = 12
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);
}
}How it works:
- 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. - Each month's interest is calculated on the updated balance: that is compounding.
- Real banking code uses
BigDecimalfor money;doubleis fine for this exercise because we only round for display.
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.
cart screen adds Rs 499 shoes, checkout adds Rs 99 delivery
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));
}
}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.
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
Setof 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.
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=70so 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.
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:+HeapDumpOnOutOfMemoryErrorso the next incident can be analysed.
Avoid: only raising -Xmx. The next client will ask for two years.
“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 countvalue belongs to the class, not to any object; in HotSpot it is kept with the class'sClassobject on the heap.
Avoid: saying amount is on the stack because it is a double.