Everything so far has been metadata: names, types, flags, versions. The Code attribute is the first place a class file contains something a machine executes, and it is structured in a way that exists for one reason — to let a virtual machine allocate a frame before it runs a single instruction.
That is the whole design brief. The JVM is allowed to interpret, to compile to machine code, or to do nothing at all until a method is hot. All three need the same thing up front: a frame of a known size. So the file states the size, twice, in two fields that are not derivable from the code and cannot be discovered by reading it cheaply.
That is why max_stack and max_locals exist. They are not descriptions of what the code does; they are promises about the worst case, made by the compiler, checked by the verifier, and used by the runtime to size a frame without executing anything. Getting either wrong is not a subtle bug: it is memory corruption, or a verifier rejection.
Four fields, in this order, and then the code and its table:
Two things make this attribute different from everything else in the format, and both are consequences of it being the only part that runs.
It is the only place with a variable-length byte array that is not a table of records. The instructions are not self-describing in the way an attribute is; they are a stream, and code_length is what delimits them. A reader cannot skip a method's code by looking at a tag — it must either decode the instructions or trust the length. Compare a COFF relocation table, where every entry is fixed-width and skippable, or a WebAssembly instruction stream, where the same problem appears and is solved the same way, with a declared length.
And it nests. A method's attributes live inside its Code attribute, not beside it, because they describe the body rather than the method. LineNumberTable maps code offsets to source lines, and that is meaningless without the code. So the Code attribute ends with its own attribute list, and a reader that has just walked a method's attributes has to go one level deeper before it has finished the method.
The smallest complete Code attribute in this course's samples — a default constructor, five bytes of instructions — read from the file:
Code attribute, 29 bytes total, at 0x013f:
u2 max_stack = 1
u2 max_locals = 1
u4 code_length = 5
u1 code[5] = 2a b7 00 01 b1
u2 exception_table_len = 0
u2 attributes_count = 1
attribute: LineNumberTable, 6 bytes
u2 code_length? no -- the attribute ended, 0x015c
the five instructions:
2a aload_0 push local 0, which is this
b7 00 01 invokespecial #1 call java/lang/Object."<init>":()V
b1 return
Every field accounted for: 2 + 2 + 4 + 5 + 2 + 2 = 17 bytes of the 29, and the remaining 12 are the nested LineNumberTable attribute's own 6-byte header plus its 6 bytes of body. The instruction stream is five bytes and the frame declaration is eight. That ratio is worth noticing: for a trivial method, the format spends more space declaring the frame than encoding the work.
Now max_locals in practice, across the methods of a real class. The previous concept established that it counts this plus the parameters; this is what happens when a method has locals of its own:
method max_stack max_locals code_length <init> 1 1 5 go 0 1 1 empty body takes 0 2 1 takes(Object): this + o args 0 7 1 (IJLjava/lang/String;[ILjava/lang/Object;)V ret 1 1 2 manyRet 2 1 2 wide 14 2 70 12 int locals summed the wide method is the interesting one: 12 declared locals, and max_locals is only 2, because this is a STATIC method whose one parameter is an int. The locals live ABOVE that mark, in slots the header does not have to describe.
max_locals is a lower bound, not a description. In the wide method above, twelve locals are live and max_locals says 2. That is not a bug and not a slack field: the locals are numbered from the top of the declared parameter area downwards, or equivalently the parameter area is at the bottom and the body's locals start above it — and the actual frame is sized by the verifier's own analysis, with max_locals used only as the starting point for parameters. A reader that treats max_locals as "how many locals this method has" is wrong on any method with body locals, and wrong in the direction that looks safe, because the real number is larger. The generated class with 300 live locals makes the point at the other extreme: max_locals is 303 there, so the field is neither a tight bound nor a constant — it is whatever the compiler computed, and the only way to know is to read it.max_stack is the deepest the operand stack ever gets. Two of the values above are worth reading together: the constructor has max_stack = 1 for its aload_0, and manyRet has max_stack = 2 because returning a long pushes two slots onto the stack for the return value. The same 64-bit rule from the previous concept, showing up a third time in the same format — a long is two slots in the pool, two in the frame, and two on the stack, and all three come from the same decision.
Why the runtime needs the number at all, given that it could compute it: computing it requires decoding every instruction. A JIT that wanted to know the frame size before starting would have to walk the whole method, and an interpreter that wanted to allocate a frame per call would pay that on every call. Stating it turns an O(code length) analysis into a single field read. And because the verifier checks the field against the code, a wrong value is rejected at load time rather than corrupting a frame at run time — which is the whole safety argument for putting the number in the file.
Every Code attribute in this course's 22 samples carries a LineNumberTable — all 65 of them, without exception, because javac emits debug information by default. That is itself a finding: the default class file is larger for being debuggable, and the same method without it is smaller by the size of that table.
LineNumberTable, nested inside Code:
u2 line_number_table_length
then that many pairs of:
u2 start_pc an OFFSET into the code array
u2 line_number a source line
and note the offset base: start_pc is measured from the FIRST BYTE
of the instruction array, not from the start of the Code attribute
and not from the start of the method.
And start_pc is an absolute code offset, not a branch offset. That distinction is the next concept's whole subject and it is worth flagging here, because the format contains both conventions within one attribute and nothing marks which is which. A LineNumberTable entry says “offset 10” and means exactly the tenth byte of the code. A branch instruction says “offset 10” and means ten bytes past the branch's own opcode. Two different meanings for the same two-byte field in the same structure, and the only way to tell them apart is to know which table you are in.
Walking a method's Code attribute to its end, which is the discipline that catches everything in this format:
That last assertion is the one that matters, and it is a two-field check: the attribute's own attribute_length has to agree with the sum of what you just read. The format gives you two independent statements about how big a method's code is — code_length inside the attribute and attribute_length around it — and a reader that checks one against the other catches a desync that would otherwise be silent.
That double statement is a real safety net and it is worth seeing why it is there. code_length says how many bytes of instructions follow; attribute_length says how many bytes the whole attribute occupies. They have to differ by a fixed, computable amount — the eight bytes of max_stack, max_locals and code_length, plus two for the exception table count, plus two for the attributes count, plus the exception table and the nested attributes. So the difference between the two numbers is a checksum over the structure you just walked, and a reader that computes it has verified the whole thing without knowing anything about instructions.
It is the same idea as the end-of-file assertion in the header concept, one level down. The file's total length checks the walk; a Code attribute's declared length checks the method walk. A format with nested length-delimited records gives you a checksum at every level, and the discipline is to use it at every level — because a reader that only checks the outermost one will happily report a perfectly plausible method with a corrupted instruction stream in the middle of it.
And the reason this matters more here than anywhere else in the format is that the instruction stream is the one part a reader cannot cheaply skip. An attribute can be skipped by its length without understanding it; a pool entry can be skipped by its tag. Instructions cannot be skipped at all, because there is no tag that says how long the next one is — a reader that does not know an opcode cannot know its width. So the length field is the only thing standing between a desynchronised reader and the rest of the file, and it is the field a hand-rolled decoder is most likely to trust rather than check.
$ javap -c -v -p out/Hello.class
$ python3 courses/jvm/assets/samples/class_decode.py out/Hello.classmax_stack and max_locals for four methods before looking. An empty static method, one returning an int, one returning a long, one taking three parameters and returning nothing. Then check, and expect to be wrong about exactly one of them — the long return, for the two-slot reason.max_locals exceeds the parameter count. Any method with body locals will do, and the generated 300-local class is the extreme. Compare the declared parameters to the field and work out where the rest of the frame comes from — that gap is the verifier's business, and it is the boundary this concept draws.-g:none and compare file sizes and the number of nested attributes. All 65 Code attributes in this course's samples carry a LineNumberTable by default, and seeing what that is worth in bytes makes the nesting feel less like free.LineNumberTable and check its offset base. Find an entry with a start_pc, then look at the instruction at that offset in the code array. Confirm the offset is counted from the first instruction byte, not from the attribute — and then remember it, because the next concept shows a table in the same attribute where the same-looking number means something else entirely.Without looking back: a Code attribute has max_stack = 3, max_locals = 4, code_length = 40, an empty exception table, and one nested attribute of 8 bytes. How long is the whole Code attribute, and what would a reader that trusted code_length to find the end of the method get wrong?
Your class file reader reports correct method names, correct descriptors and correct max_stack and max_locals for all 400 methods in a large dependency. The instructions it reports for about forty of them are nonsense, and the methods themselves are otherwise indistinguishable from the rest. Your reader trusts code_length to find where a method's code ends. The affected methods are exactly the ones with a non-empty exception table. Nothing errors. What is the defect, what does trusting code_length actually cost here, and what single check would have caught it on the first method you read?
The general habit: a length field named for one thing and used for another is the most expensive bug in a length-delimited format, because the name misleads the person writing the reader. And when a bug correlates perfectly with an optional structure, the missing or wrong handling is in that structure's branch.
Stating the worst case so a runtime can allocate before executing is not unique to the JVM, and comparing the three ways of doing it shows why this one is shaped the way it is. An ELF program header declares p_memsz, a static size the loader can trust without reading a byte of code. A PE section header declares VirtualSize against SizeOfRawData, covering the same ground with two fields. The JVM needs something none of those can supply, because the JVM compiles the method at run time — the frame is not a static region carved out of the image, it is built per call, so the size has to be in the file rather than implied by a segment boundary.
The instruction stream's framing is the same problem WebAssembly has, and the two solutions are worth comparing directly. Both wrap a variable-width byte stream in a declared length. Both forbid a reader from skipping instructions it does not understand, because there is no per-instruction width field. And both therefore have the property that a single unknown opcode makes the rest of the method unreadable — a WebAssembly module with an unknown opcode is rejected wholesale, and a class file with one is rejected by the verifier. Neither format has an escape hatch, and both live with it by making the opcode space closed and versioned. That is the opposite of the class file's attribute design, which is open, and the contrast is the interesting part: a format can be closed where it executes and open where it describes.
The nesting is a small instance of a larger pattern. COFF puts relocations in their own section rather than inside the code they patch; DWARF puts line numbers in their own section rather than beside the code. The JVM puts them inside the Code attribute, and the reason is a coupling the other two avoid: a line number is a code offset, and a relocation is an index into a symbol table, but a line number is meaningless unless you already have the code it indexes. Nesting is chosen where the child references the parent's content, and kept separate where it references something else.
And the LineNumberTable's absolute offsets set up the next concept directly. This attribute says “offset 10” and means the tenth byte. The next one shows an offset field in the same structure where the same number means something else — and it is the sharpest single trap in the format.