skip to content

The JVM specification describes a 'native method stack' alongside the Java virtual machine stack. What is that second stack for, and how does HotSpot actually arrange the two for a running thread?

level: seniorimportance: nice to knowfreq 28%

answer

  1. native methods have no bytecode → no operand stack
  2. spec: separate native stack; HotSpot: one OS stack, interleaved
  3. -Xss sizes the whole thing
  4. stack banging + guard pages → throwable error for Java frames
  5. native overflow → hs_err crash, not an exception

basics

~20 s

The Java stack holds frames for bytecode methods; the native method stack holds frames for methods written in C or other native languages, reached through JNI, which have no bytecode and no operand stack. The specification allows them to be separate; HotSpot interleaves Java and native frames on one OS thread stack sized by -Xss.

solid answer

~50 s

A `native` method has no bytecode, so it has no local variable array or operand stack in the JVM sense — it needs the host machine's calling convention instead. The specification therefore permits a separate **native method stack** ("C stack") used when a thread calls into native code, and allows an implementation that supports no native methods to omit it entirely. HotSpot does **not** keep two physically separate stacks. Each platform thread has one OS stack; Java frames and native frames interleave on it, so a thread that goes Java → JNI → callback into Java has all of those frames on the same stack, and `-Xss` (equivalently `-XX:ThreadStackSize`) sizes the whole thing. Two practical consequences. First, JNI code that recurses deeply competes for the same budget as Java frames. Second, overflow of Java frames is detected by guard pages and reported as an error the JVM can throw and unwind, whereas an overflow deep in native code can escape that machinery and abort the process instead.

code

text · 7 lines
text
java.lang.RuntimeException
    at com.example.Callback.handle(Callback.java:31)   <- Java frame, called back from C
    at com.example.Bridge.process(Native Method)       <- the native method's Java-visible stub
    at com.example.Service.run(Service.java:88)        <- Java frame

// The C frames of Bridge.process live between lines 2 and 1 on the SAME OS stack,
// but Throwable.getStackTrace() cannot see them.

go deeper

for a junior

Know that native methods are implemented outside the JVM and so cannot use the bytecode frame model, which is why the specification mentions a separate native stack.

for a middle

Add that HotSpot puts Java and native frames on one OS stack per platform thread and that -Xss sizes it, and that stacks are native memory outside the heap.

for a senior

Explain guard pages and stack banging, why Java frame exhaustion is throwable while native overflow tends to abort the process, and how mixed stacks show up in dumps and crash logs.

for a principal

Reason about the consequences at system scale: native memory budget for thread stacks, the reliability asymmetry that native code introduces into an otherwise recoverable failure mode, and pinning of virtual threads by native frames.

## Why the specification names two stacks The JVM's execution model is defined for **bytecode**: a frame with an indexed local variable array, an operand stack whose depth is fixed at compile time, and a run-time constant pool reference. A `native` method has none of that. It is declared in Java but implemented in C, C++ or another language and bound through JNI; its arguments and locals live wherever the host platform's calling convention puts them, in machine registers and native stack slots. So the specification defines a second per-thread area, the **native method stack**, used to support native method invocations. It is deliberately vague about it: an implementation may size it fixed or dynamically, may use conventional "C stacks", and an implementation that supports no native methods need not have one at all. Like the Java stack, it is per-thread, is not part of the heap, and is not garbage collected. If a thread needs more native stack than can be provided, the specification says the JVM throws `StackOverflowError`; if it cannot even create the area, `OutOfMemoryError`. ## What HotSpot actually does The two-stack picture is a specification abstraction, and interviews at senior level often probe whether you know the implementation diverges. In HotSpot, each **platform** thread is backed by one OS thread with one OS stack, and both interpreted/compiled Java frames and native frames live on it, interleaved in call order. A typical mixed chain looks like: ``` ... Java frames ... Java frame for the native method declaration (a stub) native C frames (JNI implementation, library internals) Java frames again, if the native code called back via JNI ``` A single flag, `-Xss` (or `-XX:ThreadStackSize`), sizes that one stack; there is no separate knob for "native stack" for Java threads. This is also why HotSpot's mixed-mode stack walking exists — tools that print native and Java frames together (`jcmd Thread.print` with native frames, crash-log stack traces) are walking one physical stack whose frames have different layouts. ## Guard pages and the two overflow outcomes HotSpot protects the end of each thread stack with guard pages and, on entry to a Java method, performs a *stack bang* — it touches the address a frame-size below the stack pointer. If that touch hits the reserved guard region, the runtime knows the frame will not fit and can raise `StackOverflowError` *before* the frame is created, which is what makes the error throwable and the stack unwindable rather than fatal. Native code participates in no such protocol. A deep recursion inside a C library, or a native routine that allocates a very large buffer on the stack, can run past what the JVM expected and blow through the protection. The usual outcomes are a hard crash with a fatal error log (`hs_err_pid*.log`) rather than a Java exception, precisely because the JVM was never given a chance to check. That asymmetry — Java overflow is a catchable error, native overflow is often a process abort — is the practical point worth stating. ## Related consequences - **Thread state.** A thread executing native code is in a distinct runtime state. This matters for coordination with the runtime: the JVM tracks the Java/native transition on every JNI entry and exit, which is also what makes it possible to reason about a thread that is not currently executing bytecode. - **Stack traces.** A Java stack trace shows the `native` method as a frame with source `(Native Method)` — the Java-visible stub — and stops there; the native frames beneath are invisible to `Throwable.getStackTrace()` and appear only in native-aware dumps. - **Memory accounting.** Thread stacks, Java and native alike, are native memory, not heap. A process with thousands of platform threads consumes stack reservations that no heap sizing will explain — a routine surprise when reading resident-set size against a modest `-Xmx`. Native Memory Tracking reports it as a `Thread` category. - **Virtual threads.** Virtual threads store their Java frames in heap-managed stack chunks and mount onto a carrier platform thread to run. Native frames cannot be relocated that way, which is why a virtual thread that is inside a native call (or holds a frame that cannot be unmounted) pins its carrier for the duration. ## Summary The native method stack exists in the specification because native methods cannot be expressed in the JVM's frame model. In HotSpot it is not a separate allocation: one OS stack per platform thread carries both kinds of frame, sized by `-Xss`. The difference that bites in production is protection — the JVM can detect and report Java frame exhaustion cleanly, but has no equivalent guarantee once execution is inside native code.

  • Why does a stack overflow inside JNI code often crash the JVM instead of throwing StackOverflowError?
    HotSpot detects impending Java-frame exhaustion by banging the stack against guard pages on method entry, so it can raise a throwable error before the frame is built and let the stack unwind. Native code performs no such check and can run past the guarded region in ways the runtime cannot recover from, so the failure surfaces as a fatal error with an hs_err log rather than as a Java exception.
  • Does -Xss control only the Java frames of a thread?
    No. In HotSpot a platform thread has a single OS stack that carries both Java and native frames, and -Xss (or -XX:ThreadStackSize) sizes that whole stack. Deep JNI call chains therefore consume the same budget as deep Java recursion.

saying these in an interview costs you the question

  • Asserting HotSpot allocates a physically separate native stack per thread with its own size setting.
  • Claiming a Java stack trace shows the C frames beneath a native method.
  • Saying thread stacks come out of the Java heap and are covered by -Xmx.
  • Believing native stack overflow is always reported as a catchable StackOverflowError.
  • Thinking a native method still has an operand stack and local variable array in the JVM sense.

context