ExerciseChallenge
Challenge
Measure your own stack
20 minintermediate2–8 yrs
Edge cases. You have to reason, and two valid fixes differ.
What this teaches
- Every thread has its own stack, sized when the thread is created
- Frame size depends on the method, so depth is not a constant
- A StackOverflowError takes down one thread, not the JVM
- Raising -Xss buys proportional headroom; it does not fix unbounded recursion
Starter
Starter.javaOpen in playground
import java.util.*;
/**
* CHALLENGE — 20 minutes.
*
* "How deep can Java recurse?" has no single answer, and this exercise is
* about making that concrete rather than accepting it.
*
* TASKS
* 1. Run it as-is. Note the depth.
* 2. Fill in depthOnStackOf() so the probe runs on a thread with the
* requested stack size, and measure 256 KB, 1 MB and 8 MB.
* 3. Run the SAME size three times. The numbers differ. Explain why in a
* comment. (Hint: something compiles the method partway through.)
* 4. Implement descendWithLocals() — same recursion, but with several
* unused long locals — and measure it. It should reach a lower depth.
* Say why in a comment.
* 5. Prove the blast radius: let a worker thread overflow, catch it there,
* and show main is unharmed afterwards.
*/
public class Starter {
static int depth;
static void descend() {
depth++;
descend();
}
/** TASK 4: the same recursion, but each frame carries more locals. */
static void descendWithLocals() {
// TODO: declare several long locals that the recursion actually uses,
// then recurse. Unused locals may be optimised away, so use them.
throw new UnsupportedOperationException("task 4");
}
/**
* TASK 2: run descend() on a thread with exactly this stack size and
* return the depth it reached.
*
* Thread(ThreadGroup, Runnable, String, long) is the constructor you want.
* Remember to join() — otherwise you read `depth` before it is finished.
*/
static int depthOnStackOf(long stackBytes) throws Exception {
// TODO
depth = 0;
descend(); // wrong: this runs on main, ignoring stackBytes
return depth;
}
public static void main(String[] args) throws Exception {
// Step 1, as shipped: whatever main's stack happens to be.
depth = 0;
try {
descend();
} catch (StackOverflowError e) {
System.out.println("depth on main's own stack : " + depth);
}
// Step 2, once depthOnStackOf works:
// System.out.println("256 KB : " + depthOnStackOf(256 * 1024));
// System.out.println("1 MB : " + depthOnStackOf(1024 * 1024));
// System.out.println("8 MB : " + depthOnStackOf(8 * 1024 * 1024));
// Step 5: one thread overflows; main must survive it.
System.out.println("main is still running : true");
}
}Run it locally:
cd exercises/java/jvm/heap-vs-stack/02-challenge
javac Starter.java -d /tmp/out && java -cp /tmp/out StarterHints
Hint 1
Thread's four-argument constructor takes a stack size. That is the only portable way to ask for one per thread.
Hint 2
Run the probe more than once with the same size. Do you get the same number? What does that tell you about what a frame costs?
Hint 3
Add a few unused long locals to the recursive method and measure again.
Hint 4
For the last task: catch the StackOverflowError inside the thread and print from main afterwards. Does main still work?
Done when
- Depth is measured for at least three different stack sizes
- A second recursive method with more locals is measured and reaches a lower depth
- One thread overflows and main keeps running, demonstrated in the output
- A comment states when raising -Xss is the right fix and when it is not
Stretch
Replace the platform thread with a virtual thread and measure again. You
cannot set a stack size on one — work out what that implies about where a
virtual thread's frames live, and what limits its depth instead.
← Back to What lives on the heap and what lives on the stack?