Back to course

The Header and the Fixed Order

18 minbeginnerModule 1: The ContainerContinues from jvm-intro
18 min · Module 1: The Container · Foundations

Why This Matters

The class file has no section table. There is no list of blocks, no id byte telling you what follows, no index to build first. Every field is at a fixed position given the ones before it, and a reader walks the structure from the top knowing exactly what comes next at every step, all the way to the last byte.

That is a genuinely unusual design and it has one dominant consequence: the file is parsed, never indexed. There is no random access. You cannot jump to the methods without walking the pool, the fields, and everything between, because the only way to find the methods is to count your way there. A format with a section table lets a reader do that in two steps; this one makes it one long step.

The second consequence is less obvious and more annoying in practice. Adding a name renumbers the file. The constant pool is built in the order the compiler encounters things, so insert one string literal in the middle of a method and every entry after it moves. Nothing in the file refers to a name by position in a stable way, so any tool that caches a byte offset into a class file — a build system, an incremental compiler, a coverage tool — has to throw that cache away when the class changes at all.

A Simple Model

The whole structure, with the sizes that are fixed and the ones that are not:

offset size field fixed? 0 4 magic yes, always CAFEBABE 4 2 minor_version no, but only 0 or 65535 exist 6 2 major_version no, pinned to the Java release 8 2 constant_pool_count no, but always pool_count = usable + 1 10 ... constant_pool no, this is most of the file ... 2 access_flags no, a bitfield ... 2 this_class no, a pool index ... 2 super_class no, a pool index, or 0 ... 2 interfaces_count ... 2*n interfaces n pool indices ... 2 fields_count ... ... fields variable, self-delimiting ... 2 methods_count ... ... methods variable, self-delimiting ... 2 attributes_count ... ... attributes variable, self-delimiting

Only the first eight bytes are at a known offset. Everything after the pool's count is a walk, and the walk works because every variable-length record states its own length:

member (field or method): access_flags 2 bytes name_index 2 bytes, into the pool descriptor_index 2 bytes, into the pool attributes_count 2 bytes attributes attributes_count records attribute: attribute_name_index 2 bytes, into the pool attribute_length 4 bytes <-- the escape hatch info exactly that many bytes

That attribute_length is the single most important field in the format after the magic, and it is worth understanding what it is for. It is the format's only extension mechanism. A reader that meets an attribute name it does not know skips attribute_length bytes and carries on — so the JVM can read a class file containing an attribute invented after the JVM shipped, and ignore it. A new attribute is therefore free and needs no version bump.

What the length does not buy is a new top-level block. There is nowhere to put one, because the order is fixed and the reader has no way to skip a block it does not recognise. That is the bargain: attributes are open, the file is not. Java has shipped sealed classes, records and modules by adding attributes and, where the change was too big for that, by adding new attributes and bumping the major version — which is why the major version is the Java release number rather than a format counter that could have moved independently.

The Actual Detail

The 409-byte Hello.class from this course's samples, walked from the top. Every number below is from the file on disk, and the offsets are given so you can check them against a hex dump:

  0x0000  4  magic              ca fe ba be
  0x0004  2  minor_version      00 00
  0x0006  2  major_version      00 46          = 70, Java 26
  0x0008  2  constant_pool_count 00 1d         = 29
  0x000a     the pool, 28 usable entries, ending at 0x0124
  0x0125  2  access_flags       00 21          ACC_PUBLIC ACC_SUPER
  0x0127  2  this_class         00 15          #21, which is "Hello"
  0x0129  2  super_class        00 02          #2, "java/lang/Object"
  0x012b  2  interfaces_count   00 00
  0x012d  2  fields_count       00 00
  0x012f  2  methods_count      00 02
  0x0131  ... two method records
  0x018f  2  attributes_count   00 01
  0x0191  ... SourceFile
  0x0199     end of file: exactly 409 bytes

Three things in that walk are worth stopping on.

The pool's count is one more than the number of entries. The file says 29 and there are 28 usable entries, because the pool is one-indexed: index 0 is reserved and never used, so a stored count of n means entries 1 through n-1. A reader that allocates n entries and loops to n will read one past the end, and a reader that treats index 0 as a valid entry — the way a C array naturally invites — will resolve a name to whatever happens to be at offset zero of the table.

Indices in the header are pool indices, and they are the pool's own numbering.this_class is 0x15 = 21, and entry 21 is a CONSTANT_Class pointing at entry 22, which is the Utf8"Hello". So the class's own name is two indirections from the field that names it. There is no shortcut to a name anywhere in this format; everything goes through the pool, always.

And the pool is most of the file. 28 entries occupy bytes 0x0a to 0x0124 — 283 bytes of 409, which is 69% of a hello-world. The code that actually does the work is 14 bytes across two methods. The ratio is the argument for the whole design: a format that names everything by index into one shared table pays for the table once and gets compression everywhere else, and on real code the table is where the bytes are.

What the version field is really for

Worth being precise, because the intuition is usually wrong in both directions. The major version is not incremented when the file format changes — it is the Java feature release that produced the file, and the format's own history is welded to it. Major 52 is Java 8 and introduced the invokedynamic-based lambda implementation; every major number since has been a Java release. So a class file's major version tells you two things at once: which features the class might use, and which JVM can run it.

And the compatibility rule is absolute, not graded. A JVM reads the major version, checks it against what it implements, and if it is too new the class is refused before anything else is read. There is no partial support, no "I can run the methods but not the constant pool", and no attempt to read what it recognises. Compare an ELF file with an unfamiliar machine type, which a loader will usually attempt anyway, or a PE file with an unfamiliar Magic, where the same is true.

Why that hardness is a feature. A class file is not going to be concatenated, patched, or partially interpreted by something that only understands the old layout — there is no linker that could get it subtly wrong, and no loader that could guess. The hard boundary is what makes it safe for a machine to refuse in six bytes instead of attempting a load it cannot complete. Every other format in this collection is designed to be partially understood, which is more flexible and considerably harder to reason about, because a reader that half-works is worse than one that refuses. The class file gives up partial compatibility on purpose, and the minor version's entire job is to flag the one case where that would be inconvenient: a preview feature, marked by setting minor to 0xFFFF, which is a promise that this file will only load in the exact JVM build that compiled it.

The renumbering problem

Here is the practical consequence, and it is worth seeing concretely rather than being told about. Take Hello.class and add one string literal to the middle of main. The compiler encounters the new string while walking the method, so it lands in the middle of the pool, and every entry after it moves up by one. The Utf8 that was #24 is now #25, and so is everything else.

Nothing in the file is broken by this — every reference is updated consistently, because the compiler writes the file after it has finished building the pool. But it means the byte offset of any given name is not a property of the name. It is a property of this compilation of this source. Any tool that recorded "the string hello is at offset 0x1D4 in this class" has recorded something true only until the class is recompiled from anything but identical source. That is why incremental build systems keyed on class files are fragile, and why the tools that do it key on a hash of the content instead.

A Real Example

Reading the file to the last byte, which is the discipline that catches everything: after the walk, the cursor must be exactly at the end. Not past it, not short of it.

walk(path): p = 0 assert read_u4(p) == 0xCAFEBABE # the only sanity check available p = 8 cp_count = read_u2(p); p += 2 # the pool is walked by ENTRY, not by byte count, because a Long or a # Double entry is longer than its tag suggests and consumes two indices i = 1 while i < cp_count: tag = read_u8(p); p += 1 p += payload_size(tag, p) # varies: 2, 3, 4, 8, or u2+varying i += 1 if tag not in (Long, Double) else 2 access = read_u2(p); p += 2 this_i = read_u2(p); p += 2 super_i = read_u2(p); p += 2 for _ in range(read_u2(p)): p += 2 # interfaces for _ in range(read_u2(p)): p = member(p) # fields for _ in range(read_u2(p)): p = member(p) # methods p = attributes(p, read_u2(p)) assert p == len(file), "trailing or missing bytes"

That final assertion is the one that earns its keep, and it is the same habit the WebAssembly course arrived at independently. A structural reader that does not check where it ended cannot distinguish a correct parse from a parse that lost sync two hundred bytes ago. The two produce the same output — plausible names, plausible indices — and the only difference is that one of them is wrong. An end-of-file assertion turns that class of bug from silent into loud, and it costs one comparison.

It is worth being concrete about what desynchronisation looks like here, because this format makes it unusually easy. Every count in the header is a u2, so a reader that misjudges a length will read a number that is very often a plausible small integer. A method's attributes_count is typically 1 or 2. A misread that turns into 1 or 2 is not going to be noticed — the walk continues, reads two more six-byte attribute headers, and arrives at a position that is wrong by twelve bytes. The names it reports afterwards will still be pool indices, and most pool indices resolve, because a pool is large and most of its entries are strings.

And the failure is invisible in the output. A decoder that has lost sync by a multiple of two will still report method names from the pool, because method names are pool indices and the pool does not care what they point at. The result reads perfectly: plausible names, plausible descriptors, a plausible method count. Nothing in the printed output distinguishes it from a correct parse. The end-of-file assertion is the only thing that does, which is why this course's decoder prints its final byte position on every file and why the crosscheck harness treats "did not consume the whole file" as a failure rather than a note. It is the same lesson as the run-length local declarations in the WebAssembly course, arriving from a completely different format: in a binary format with a variable-length record, the only reliable signal that you have gone wrong is that you ended up in the wrong place.

Try It Yourself

$ javap -v -p out/Hello.class
$ python3 courses/jvm/assets/samples/class_decode.py out/Hello.class

Check Your Understanding

Without looking back: a class file's constant_pool_count is 29, and its methods_count is 2. How many usable constant pool entries are there, why is that number not 29, and what does a reader that allocates 29 slots and loops i = 0; i < 29 actually do?

Apply It

Your class file reader reports a plausible method count and a plausible list of method names for a 40 KB class, and the names are real method names. The Code attributes it then decodes contain instruction bytes that disassemble to nothing sensible. The end-of-file assertion passes. Nothing else is wrong: the header, the pool and the member counts are all correct. What is the most likely defect, and why does the fact that the end-of-file assertion passing narrow it sharply?

The general habit: an assertion that passes is evidence, not just absence of evidence. "The walk ended in the right place" narrows the search space more than any amount of staring at the values, because it eliminates every bug whose mechanism is moving the cursor.

Connect

The fixed, self-delimiting layout is the same idea as a PE section table reached from the opposite direction, and the comparison is worth making carefully because the two formats have made opposite bets on the same problem. A PE file has a header, then a table of section headers, each carrying a name, a virtual size and a raw-data pointer. Nothing after the table is at a fixed offset, and a loader that wants the import directory must find it in the table first. The class file has no table and no directory; everything is where counting says it is. PE buys flexibility at the cost of two passes; the class file buys one pass at the cost of inflexibility. The second cost is the one that hurts in practice, and it is why a format with a table can grow a new section without a new version while this one cannot.

The escape hatch is the interesting part, because it is a third option neither of those formats uses. An attribute is a name plus a length, and a reader that does not recognise the name skips the length. That is COFF's auxiliary record idea with the variability pushed into a length field, and it is why a class file can carry an attribute the JVM has never heard of. The parallel to DWARF's versioned tag ranges is exact in the other direction: DWARF reserved whole numeric ranges per version so a new tag could not collide, while the class file uses a name and gets collision-freedom for nothing. DWARF pays in tag space, the class file pays in a version number, and both avoid the problem of a new record colliding with an old one.

The renumbering behaviour has no direct counterpart in the other formats here, and that is because of what a constant pool is for. An ELF symbol table is an array too, and adding a symbol renumbers it — but nothing inside an ELF file refers to a symbol by index, so the renumbering is invisible to the rest of the file. In a class file, essentially every field is a pool index, so renumbering rewrites the whole file. The class file's pervasive indirection is what buys it the small code size, and it is also what makes the file unstable under recompilation — the same decision producing both the compression and the cache-hostility, and neither being separable from the other.

Next: the constant pool — twenty tags, and the two-slot rule that makes some indices permanently unusable.