Back to course

Why a Class File

15 minbeginnerModule 1: The Container
15 min · Module 1: The Container · Foundations

Why This Matters

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.

A Simple Model

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:

class file: magic 4 bytes minor_version 2 bytes major_version 2 bytes constant_pool_count 2 bytes constant_pool the pool, and most of the file access_flags 2 bytes this_class 2 bytes super_class 2 bytes interfaces_count 2 bytes, then that many 2-byte indices fields_count 2 bytes, then that many field records methods_count 2 bytes, then that many method records attributes_count 2 bytes, then that many attribute records

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.

The Actual Detail

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.

The version pair is not what it looks like

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:

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.

A Real Example

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.

READER 1 class_decode.py written from the specification, by this course. Prints structure. Deliberately NOT a validator. READER 2 javap -v the JDK's own disassembler. 30+ years old, and still the thing every Java developer actually reaches for. READER 3 java.lang.classfile the JDK's own STANDARD API for parsing class files, added in Java 22. Reaches the parser through a supported interface rather than through a disassembler, so a bug in one implementation cannot hide in both. ORACLE the JVM verifier not a reader at all: it accepts or refuses. A class that loads has passed a format check and a type check performed by code that shares nothing with the three above.

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.

Try It Yourself

$ 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.py

Check Your Understanding

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?

Apply It

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.

Connect

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.