skip to content
everything.jvm.classFileFormat

the class file

CAFEBABE, the constant pool, and every byte of a tiny .class.

chapter
chapter 13.02
reading time
8 min read

a .class file is a flat run of bytes with a fixed shape: the magic CAFEBABE, a version, a constant pool (every name, type and literal the class uses, numbered), then the class's flags, fields, methods and attributes. almost everything after the pool is a two-byte index into the pool, not the thing itself.

it's small enough to read by hand. so let's.

the picture

you know how .class files feel like opaque blobs that only the JVM is allowed to look at? they're not. they're one of the most readable binary formats around, and javac's output for a tiny class fits on one screen.

think of it like a recipe book with a glossary in the front. the glossary (the constant pool) lists every ingredient and every technique once, numbered. the recipes themselves just say "use #7" instead of spelling it out. that's why the pool is most of the file: the rest is mostly numbers pointing back into it.

here's the class:

Point.java
public class Point {
    int x;
    int getX() { return x; }
}

compiled with javac -g:source Point.java on jdk 25 (that flag keeps the file name but skips line numbers, so the file stays tiny). the result is 224 bytes. here's every one of them.

0000cafebabe0000004500120a0002000307·······E········
001000040c000500060100106a6176612f6c··········java/l
0020616e672f4f626a6563740100063c696eang/Object···<in
003069743e01000328295609000800090700it>···()V·······
00400a0c000b000c010005506f696e740100·········Point··
0050017801000149010004436f6465010004·x···I···Code···
00606765745801000328294901000a536f75getX···()I···Sou
007072636546696c6501000a506f696e742erceFile···Point.
00806a617661002100080002000000010000java·!··········
0090000b000c000000020001000500060001················
00a0000d0000001100010001000000052ab7··············*·
00b00001b1000000000000000e000f000100················
00c00d0000001100010001000000052ab400·············*··
00d007ac0000000000010010000000020011················
constant poolindex into the poolheader + bytecodecounts + flags
0x0000 to 0x0003 · 4 bytes · header
magic number
magic = 0xCAFEBABE
bytes: ca fe ba be

every class file starts with these four bytes. the JVM refuses anything else. a joke from the early 90s that is now load-bearing.

hover or click any byte (or focus the figure and use the arrow keys). purple bytes are constant pool entries, blue bytes are indexes pointing into the pool: select one and the entry it points at lights up. start at 0x00ae to find the bytecode of the constructor.

see the strip on top? 122 of the 224 bytes are the constant pool. the actual code (both methods' instructions) is 10 bytes.

the shape

the JVM spec defines the whole file as one struct. u2 and u4 are unsigned 2 and 4 byte numbers, big-endian:

JVMS §4.1
ClassFile {
    u4             magic;
    u2             minor_version;
    u2             major_version;
    u2             constant_pool_count;
    cp_info        constant_pool[constant_pool_count-1];
    u2             access_flags;
    u2             this_class;
    u2             super_class;
    u2             interfaces_count;
    u2             interfaces[interfaces_count];
    u2             fields_count;
    field_info     fields[fields_count];
    u2             methods_count;
    method_info    methods[methods_count];
    u2             attributes_count;
    attribute_info attributes[attributes_count];
}

no padding, no alignment, no offsets table. the only way to find the methods is to walk the pool entry by entry (each tag tells you how long that entry is). that's what the JVM's class file parser does, and it's what javap -v prints for you:

javap -v Point.class
public class Point
  minor version: 0
  major version: 69
  flags: (0x0021) ACC_PUBLIC, ACC_SUPER
  this_class: #8                          // Point
  super_class: #2                         // java/lang/Object
  interfaces: 0, fields: 1, methods: 2, attributes: 1
Constant pool:
   #1 = Methodref          #2.#3          // java/lang/Object."<init>":()V
   #2 = Class              #4             // java/lang/Object
   #3 = NameAndType        #5:#6          // "<init>":()V
   #4 = Utf8               java/lang/Object
   ...
   #7 = Fieldref           #8.#9          // Point.x:I
quick check
javap says constant_pool_count is 18. how many entries does Point's constant pool actually have?

the constant pool is a graph

the pool isn't a flat list of strings. entries point at other entries. getfield #7 in getX names a field, and resolving what that means takes four hops:

  • invokespecial in the constructor
  • ├──
  • │ └── #4 Utf8 java/lang/Object
  • └──
  • ├── #5 Utf8 <init>
  • └── #6 Utf8 ()V
  • getfield in getX
  • ├── also this_class
  • │ └── #10 Utf8 Point
  • └──
  • ├── #11 Utf8 x
  • └── #12 Utf8 I
the two references the bytecode actually uses, and everything they hang off. only the Utf8 leaves hold real text. click a node to fold it.

this indirection is why linking is lazy. the bytes say "the field x of type I in class Point", by name. the first time getfield #7 runs, the JVM looks that up, finds the field's real byte offset inside a Point object, and caches the answer in the runtime constant pool. after that it's a plain memory read. more in class loading.

descriptors

types in a class file are written as compact strings called descriptors. you just saw I and ()V. the full alphabet:

B C D F I J S Z
byte, char, double, float, int, long, short, boolean. (J and Z because L and B were taken.)
Ljava/lang/String;
a class type: L, the internal name with slashes, a semicolon.
[I · [[Ljava/lang/Object;
arrays: one [ per dimension.
V
void. only allowed as a return type.
(ILjava/lang/String;)Z
a method: parameters in parentheses, then the return type. this one is boolean m(int, String).

notice what's not in a descriptor: generic type arguments (List<String> is just Ljava/util/List;, see type erasure) and parameter names. generics survive only in an optional Signature attribute, for reflection and the compiler.

versions

bytes 6 and 7 are the major version. each java release bumps it by one, and a JVM refuses anything newer than itself.

javamajor version
1.145
852
1155
1761
2165
2569
2771
predict the output
terminal
# Point.class from above, with byte 7 changed from 0x45 (69) to 0x46 (70)
$ java -cp . Run

writing one without javac

since java 24, the JDK can read and write class files itself: the Class-File API (java.lang.classfile, JEP 484) java 24+. here's a class built from nothing, loaded and called, no source code involved:

Main.java
void main() throws Throwable {
    byte[] bytes = ClassFile.of().build(ClassDesc.of("Answer"), cls -> cls
        .withMethodBody("get", MethodTypeDesc.of(ConstantDescs.CD_int),
            ClassFile.ACC_PUBLIC | ClassFile.ACC_STATIC,
            code -> code.bipush(42).ireturn()));
 
    IO.println(HexFormat.of().formatHex(bytes, 0, 4));
    Class<?> answer = MethodHandles.lookup().defineClass(bytes);
    IO.println(answer.getMethod("get").invoke(null));
}
output, from jdk 25
cafebabe
42

this one needs a real JVM (a library class the browser runtime doesn't have), so it doesn't run in the browser. copy it into java Main.java to try it.

on jdk 25 that's a 106-byte class: magic, version 69, a constant pool, one method whose code is bipush 42, ireturn. frameworks that generate classes at runtime (mocking libraries, proxies, ORMs) have always needed a library like ASM or ByteBuddy for this. the JDK itself has been moving its own class generation (lambdas, proxies, records) off an internal copy of ASM and onto this API.

one level deeper:attributes: the extension mechanism+

everything optional in a class file is an attribute: a name (a pool index to a Utf8 like Code), a length, and bytes. Code holds a method's bytecode. LineNumberTable maps bytecode offsets to source lines (stack traces need it). LocalVariableTable keeps local names (javac -g). StackMapTable gives the verifier the type of every stack slot at every jump target, so verification is one linear pass. Signature keeps generics, Record lists record components, NestMembers lists inner classes allowed to touch each other's privates, BootstrapMethods holds the recipes for invokedynamic.

the rule that makes this work: a JVM must silently skip attributes it doesn't recognize. that's how a 1996 format keeps absorbing lambdas, records, sealed types and modules without a redesign.

keylines
  • magic, version, constant pool, flags, this/super, interfaces, fields, methods, attributes. in that order, no padding.
  • the constant pool is most of the file. everything else mostly points into it.
  • names stay symbolic until first use. that's why linking can be lazy.
  • major 69 is java 25. a JVM refuses a newer one.
  • javap -v reads it for you. the Class-File API writes it for you.