A Java compiler does not produce a program. It produces a file — a single, self-contained, fully resolved artifact that a virtual machine can load, check, and refuse. The compiler's entire output is that file, and the format is the contract between the two halves of the toolchain.
The interesting part is what the contract has to carry. A native object file is full of unresolved names and meaningless until a linker fills them in. A class file has no linker in the usual sense, no symbol table to resolve, and nothing left dangling. Everything the machine needs to know — every name, every type, every string — is in the file already, in one table that everything else points into.
That table is the constant pool, and it is why this format is worth studying carefully rather than skimming. The class file is a graph of numbers, and almost every number is an index into one array. Get the array right and the rest of the format is mechanical; get it wrong and nothing else can be trusted, because every field downstream is a pointer into it.
The smallest useful class file, produced by this course's own sample, is 409 bytes and contains twenty-eight constant pool entries for a program that prints one word. Here is the whole shape, in the order the bytes appear:
Three things about that layout are worth noticing before any of the details.
It is not extensible. There is no section table, no id byte, no length-delimited list of blocks. Every field is at a fixed position given the ones before it, and a reader walks the structure top to bottom knowing what comes next at every step. Compare ELF's section header table, where anything can be anywhere, or WebAssembly's numbered sections, where a new id buys a new block. The class file has no mechanism for that, and that is a deliberate trade: nothing can be added without changing the specification, and in exchange a reader needs no lookup table to find anything.
It is not the order you wrote. The pool comes before the class's own name, which comes before its methods, which come before the code inside them. Source order is nowhere in the file. A reader that expects things in the order a programmer thinks about them will be wrong immediately.
Almost everything is an index.this_class is a number. A method's name is a number. Its descriptor is a number. Its attributes are a list of numbers that head length-delimited blocks. The class file's central design decision is that naming is indirection, and the constant pool is the table of names.
What a class file looks like when you ask a real compiler to make one. The source is four lines:
public class Hello
public static void main(String[] args)
System.out.println("hello")
And here is what the first sixteen bytes of the result are, annotated completely:
0000: ca fe ba be 00 00 00 46 00 61 0a 00 02 00 03
| | | | | | | | | | | | | | | +-- 0x0002: cp count
| | | | | | | | | | | | | | +----- 0x000a: the first
| | | | | | | | | | | | | | pool ENTRY, at 0x0a
| | | | | | | | | | | | | +---------- 0x0061: a Utf8 tag
| | | | | | | | | | | | +------------- and 97 bytes of it
| | | | | | | | | | | +---------------- 0x000a: entry length
| | | | | | | | | | +------------------- 0x0000: minor version
| | | | | | | | | +---------------------- 0x0046: major = 70
| | | | | | | | +------------------------- 0x0000: minor (0)
| | | | | | | +---------------------------- 0x0000: version high
| | | | | | +------------------------------- 0x0000: version low
| | | | | +---------------------------------- 0xbe: magic high
| | | | +------------------------------------- 0xba: magic
| | | +---------------------------------------- 0xfe: magic
+-------------------------------------------------- 0xca: magic
One thing in that annotation is the whole first lesson of this course and it is invisible unless you look for it: the bytes are in the order ca fe ba be, which reads as the number 0xCAFEBABE read big-endian. The magic number is famous precisely because it reads the same way in a hex dump as it does in the source that checks it. A little-endian reader — which is what you would write if you were arriving from ELF, PE or x86, all of which are little-endian — sees the value 0xBEBAFECA and rejects the file.
So: the class file format is big-endian throughout, and the magic is not an exception to that rule. It is the rule, stated first, in four bytes, before anything else has a chance to be ambiguous. This is worth sitting on, because it is a design decision with no technical justification — the JVM had no reason to prefer either endianness — and it was made the same way the magic was: to be unmistakable. Both choices exist to catch a mistake at the earliest possible moment.
Two two-byte fields, and the naming is a trap. minor_version is 0 and major_version is 70. The instinct is to read that as "version 70.0" and be done. Two corrections:
0xFFFF. That is the whole scheme: a minor of 65535 means "this file uses preview features and needs the same JVM build that compiled it". A two-byte field that is almost always zero and carries one bit of meaning is a shape worth recognising, because a reader that treats it as part of a version number will accept a preview class file it cannot run.And the compatibility rule is absolute rather than graded. A JVM will load a class file whose major version it knows, and refuse one whose major version it does not, with no attempt to read what it can. Compare that to a native binary, where an unknown ELF EI_OSABI byte or an unfamiliar machine type usually produces a warning and a best effort. The class file format chose a hard boundary over graceful degradation, and it is a boundary the machine can check in four bytes before reading anything else.
What the four independent implementations are, and what each one is good for. This is not decoration: the whole reason this course can make claims is that it has four readers that cannot all be wrong the same way.
Why three readers and not one. A decoder written from a specification can be wrong in the ways the specification is ambiguous, and it will be confidently wrong, because it has no way to notice. `javap` can be wrong in the ways a disassembler is wrong — pretty-printing choices, and it is a disassembler, so it is built to show you code rather than to show you bytes. The standard API is neither: it is the JDK parsing its own format through a contract, which makes it the closest thing to an oracle that still prints structure.
And the oracle is different in kind. A reader tells you what it thinks the file says. The verifier tells you whether the file is legal, and it is the only one of the four that can be right about a file the others misread. That asymmetry is useful: when the three readers agree and the JVM refuses the file, the readers have all missed something real, and hunting for it is the most instructive thing that can happen in this course.
What the harness actually compares is written down in its own source, at the bottom of the file, because a verification harness whose limits are undocumented will eventually be trusted past them. It compares structure, framing, pool slots and member names. It does not compare attribute bodies — only attribute names — and it does not compare the values inside constant pool entries. Both limits are stated there rather than left for a reader to discover by hitting them.
And the harness was itself tested by injecting faults, which is the part most harnesses skip. Five were tried. Three were caught immediately. One was not caught, and the reason was a bug in the harness rather than in the decoder: it computed "which pool indices are unusable" in its own code as well as in the decoder's, exercised only its own copy, and so a fault in the other copy passed cleanly. The fix was to make it one function. That episode is written up in the course's research record in full, because a check that re-implements the thing it is checking is testing the re-implementation, and the only way to find that out is to break something and see whether the harness notices.
$ javac -d out Hello.java
$ javap -v -p out/Hello.class
$ python3 courses/jvm/assets/samples/class_decode.py out/Hello.class
$ java -cp courses/jvm/assets/samples Cf out/Hello.class
$ python3 courses/jvm/assets/samples/crosscheck.pyjavac --release 8, --release 17, and no flag. The major version changes and nothing else in the header does. Then try to load the newest with java -cp and read the refusal — the error message names the version, which is the format's whole compatibility policy in one sentence.javap -v before and after, then diff. Almost everything shifts, because the pool is renumbered — and that reordering is the next concept's whole subject.Without looking back: a class file begins ca fe ba be 00 00 00 46. How many bytes is that, what is the version, how is the magic read, and what happens if a reader on a little-endian machine reads those same four bytes as a 32-bit number?
You are writing a tool that reads class files. It works on every class your build produces and reports the wrong major version for one artifact from a dependency — a plausible large number, not zero, and not an obviously byte-swapped one. Your tool reads big-endian. The file is 40 KB. The dependency was built by a different organisation's JDK, six months older than yours. What is the most likely cause, what is the one-line check, and what does the fact that your tool is big-endian tell you about the diagnosis?
The general habit: when a field reads as a wrong-but-plausible value, the bug is usually a rule being applied too strictly rather than bytes being read wrongly. Plausibility is the signal — a byte-order bug produces implausible values, which is exactly why they are easy to spot and yours is not.
The class file's fixed, non-extensible layout is the sharpest contrast in this collection, and the three formats it is worth setting it against are all in courses here. ELF puts a table of section headers at the end, each with a type, an offset and a size, so anything can be anywhere and a reader scans the table first. COFF does something in between: a fixed header, then a table of section headers that gives names and locations. WebAssembly numbers its sections and requires them in ascending order, which is a compromise between the two. The class file takes none of the flexibility, and what it buys is a reader that never has to build an index before it can read anything.
That choice has a direct cost that this course will keep running into: every new feature needs a new specification version. There is no way to add an attribute kind in the abstract — attributes are already length-delimited blocks, so a new one is easy — but there is no way to add a new top-level block, and a new class file version is how Java shipped a sealed-classes attribute, records, and every other structural change since 1995. Compare the PE data directories, where a producer needing a new table adds a directory entry and bumps a count; or an ELF EI_OSABI byte, which exists precisely so a format can grow a variant without a new magic number. The class file's rigidity is why its major version is the Java release number and not a format counter.
The endianness point is not isolated either, and it is worth noticing how rare a big-endian executable format is. Every native format in this collection — ELF, PE, COFF, Mach-O, x86 — is little-endian on the architectures that actually run, because the hardware is. A network format is big-endian for historical reasons going back to the original IP header. The class file is a file format with no network role and no hardware constraint, and it chose big-endian anyway, which means the choice was made for the same reason the magic was: to be visibly, unmistakably not anything else. Both are four bytes of deliberate conspicuousness, and both are stated before there is anything to be ambiguous about.
Next: the header and the fixed order — the eight bytes just decoded, followed by the pool and everything that points into it.