Challenge

Measure your own stack

20 minintermediate28 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 Starter

Hints

  1. Hint 1

    Thread's four-argument constructor takes a stack size. That is the only portable way to ask for one per thread.

  2. 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?

  3. Hint 3

    Add a few unused long locals to the recursive method and measure again.

  4. 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?