How do a frame's local variable array and its operand stack cooperate while bytecode executes, and how are arguments and return values handed between a caller's frame and a callee's frame?
answer
- load → compute → store
- locals = storage, operand stack = calculator
- caller pushes receiver + args; they become callee locals 0..n
- return value lands on the caller's operand stack
- long/double are two-wide in both areas; this = slot 0
basics
~20 sLocals are addressable storage; the operand stack is the working area. Bytecode loads locals onto the operand stack, instructions pop operands and push results, and stores put results back. To call a method, the caller pushes receiver and arguments onto its operand stack; the invoke pops them into the callee's low local slots, and the callee's return value is pushed onto the caller's operand stack.
solid answer
~50 sThe JVM is a stack machine with addressable local storage, so every computation is a shuttle between the two areas of a frame. - **Locals** are an indexed array — read with `iload`/`aload`, written with `istore`/`astore`. Instructions never compute on a local in place. - **The operand stack** is where arithmetic happens: `iload_1; iload_2; iadd` leaves the sum on top; `istore_3` moves it into a local. Calls use the same area as the argument-passing protocol. The caller pushes the receiver (for instance calls) then the arguments in order; `invokevirtual`/`invokestatic`/etc. pop exactly that many values, and they materialise as the **callee's** local slots 0..n in its brand-new frame. On return, the callee's frame is discarded and its return value is pushed onto the **caller's** operand stack, ready for the next instruction. Two width rules matter: `long` and `double` take two local slots and two units of operand-stack depth, and slot 0 of an instance method is `this`.
code
java · 6 linesclass Frames {
int scale(int factor) { // instance method: slot 0 = this, slot 1 = factor
return factor * base();
}
int base() { return 10; }
}go deeper
Show the load–compute–store shape on a one-line expression and know that slot 0 of an instance method is this.
Explain the call protocol precisely: caller pushes receiver and arguments, they become the callee's locals 0..n, the return value comes back on the caller's operand stack; state the two-slot rule.
Read real javap -c output fluently, explain the operand-stack clear on exception dispatch, and note that JIT-compiled code keeps this only as metadata for stack traces and deoptimization.
Discuss why a stack machine was chosen — compact, verifiable, register-allocation-free class files — and what it costs and buys the compiler pipeline downstream.
## Two areas, two jobs A JVM frame separates *storage* from *computation*. - The **local variable array** is random-access storage indexed by number. It holds the parameters and locals of the invocation. Nothing computes here. - The **operand stack** is a small last-in-first-out working area, empty when the frame is created and (for a well-formed method) empty again when it completes. Every instruction that computes takes its inputs from the top of this stack and leaves its output there. That split is why almost all bytecode reads as *load, compute, store*. ``` int c = a + b; // a=slot1, b=slot2, c=slot3 iload_1 // stack: [a] iload_2 // stack: [a, b] iadd // pops both, pushes sum -> [a+b] istore_3 // pops into slot 3, stack empty again ``` Depth here peaks at 2, which is what the compiler records as `max_stack`. ## Slot layout rules - **Instance methods**: slot 0 is the receiver, `this`. **Static methods**: slot 0 is the first parameter. This is why `aload_0` at the top of an instance method means "push `this`". - Parameters follow in declaration order, then the body's locals. - **`long` and `double` occupy two consecutive slots**, referenced by the lower index; slot *n+1* is reserved and must not be addressed on its own. They likewise consume two units of operand-stack depth. (`getfield` on a `long` field pops one reference and pushes two units.) - The compiler may **reuse a slot** for source variables whose scopes do not overlap, so slot indices do not map one-to-one onto declared names. ## The call protocol between frames The operand stack is not only for arithmetic — it *is* the argument-passing mechanism, and this is the part candidates most often get vague about. 1. In the caller's frame, push the receiver first for an instance call (`invokevirtual`, `invokeinterface`, `invokespecial`), then each argument in left-to-right order. A static call (`invokestatic`) pushes arguments only. 2. The invoke instruction consumes exactly those values from the caller's operand stack. It creates a **new frame** for the callee, and the consumed values become the callee's local slots starting at 0 — receiver at 0 for an instance method, then the arguments, widening `long`/`double` across two slots each. 3. The callee runs with its own empty operand stack. 4. On `ireturn`/`areturn`/etc., the return value is popped from the callee's operand stack; the callee frame is destroyed; the value is pushed onto the **caller's** operand stack. A `void` method (`return`) pushes nothing. So the caller's operand stack behaves like an argument staging area followed immediately by a result slot — which is why chained expressions such as `a.f().g()` compile to a run of pushes and invokes with no intermediate locals at all. ``` total = list.size() + 1; aload_1 // push list (receiver) invokevirtual size:()I // pops receiver, pushes int result iconst_1 iadd istore_2 ``` Notice `size()`'s frame came and went entirely between instructions 1 and 2; all the caller ever sees is that the receiver disappeared and an `int` appeared. ## Abrupt completion If an exception is thrown and a handler exists in the current method, the operand stack of the frame is **cleared**, the exception object is pushed as the single value, and execution jumps to the handler's bytecode index. If no handler matches, the frame is popped and the exception is rethrown in the caller — which is exactly why a stack trace shows the chain of frames unwound. ## Why the design looks like this A stack machine with statically known depth makes the class file compact (no register allocation encoded, no operand addresses), makes verification tractable (the verifier simulates the operand stack's types at every instruction), and leaves the real machine's register allocation to the JIT. When a method is compiled, this shuttle largely evaporates: values stay in machine registers, and the operand stack exists only as metadata that lets the runtime reconstruct the abstract state when it needs to build a stack trace or deoptimize back to the interpreter. ## What to take away Locals are named storage; the operand stack is the calculator's display. Arguments go *out* through the caller's operand stack and arrive *in* as the callee's low local slots; results come back onto the caller's operand stack. Long and double are two-wide in both areas, and `this` is slot 0 of an instance method.
- Why does a `long` local occupy two slots when a modern 64-bit JVM could store it in one?The slot width is part of the class-file and verifier contract, defined so that the format is independent of the host word size; a slot is specified to hold an int, float, reference or returnAddress, and 64-bit values are defined to span two consecutive slots. It is an encoding rule, not a statement about physical storage — a compiled frame may keep the value in a single 64-bit register.
- What happens to the operand stack when an exception is thrown inside a method that has a matching catch block?The frame's operand stack is cleared, the exception object is pushed as the only value, and control transfers to the handler's bytecode offset recorded in the exception table. If no handler matches, the frame is popped instead and the exception continues propagating into the caller.
saying these in an interview costs you the question
- Saying arithmetic operates directly on local variables — every value must be loaded onto the operand stack first.
- Believing the callee reads the caller's frame to fetch its arguments; the invoke instruction moves them into the callee's own local slots.
- Assuming a return value is written into a local of the caller automatically rather than pushed onto the caller's operand stack.
- Forgetting that `long`/`double` are two-wide, then miscounting slot indices.
- Thinking slot numbers map one-to-one onto declared variable names, ignoring slot reuse.