Skip to content

Java class file

Level: 201 · for anyone with a hex editor open

One line: CA FE BA BE starts every Java class file and every Mach-O universal binary, and file(1) separates them with one comparison — the big-endian integer at offset 4 is a slice count below 25 in a fat binary and a version above 30 in a class — while inside the class every string is modified UTF-8 with a 16-bit byte count, so café, a NUL and an emoji cost 5, 2 and 6 bytes.

What the loader reads

A .class file is what javac writes and the JVM loads, specified in chapter 4 of the JVM Specification ↗. It is big-endian from end to end, with no field to say so, and it opens:

offset field width what it says
0 magic 4 ca fe ba be
4 minor_version 2 usually 0
6 major_version 2 45 for Java 1.1, 52 for 8, 61 for 17, 65 for 21, 69 for 25
8 constant_pool_count 2 one more than the number of entries
10 constant_pool[] tagged entries: 1 Utf8, 3 Integer, 5 Long, 7 Class, 8 String, 10 Methodref, 12 NameAndType …
access_flags, this_class, super_class 2 each indexes into the pool
interfaces, fields, methods, attributes each a 16-bit count and a list

The constant pool is where the strings are, and a CONSTANT_Utf8_info is a tag byte, a 16-bit length in bytes, and that many bytes of modified UTF-8.

In Python

Verified output of java_class_py.py — regenerated by tools/run_examples.py, never hand-typed.

1. MAGIC, THEN MINOR, THEN MAJOR, ALL BIG-ENDIAN
------------------------------------------------------------------------
   ca fe ba be 00 00 00 45   magic 0xcafebabe   minor 0   major 69 = Java 25

   major 45  Java 1.1   bytes 00 2d
   major 52  Java 8     bytes 00 34
   major 61  Java 17    bytes 00 3d
   major 65  Java 21    bytes 00 41
   major 69  Java 25    bytes 00 45

   The class file is big-endian from the first byte to the last, by
   specification; no field says so and no field could be read before
   deciding it. Read as little-endian the version is 0x4500 = 17664.

2. A CONSTANT_Utf8 IS A 16-BIT BYTE COUNT AND MODIFIED UTF-8
------------------------------------------------------------------------
   the string      'café\x00😀'   6 code points, 10 bytes of UTF-8
   built here      01 00 0d 63 61 66 c3 a9 c0 80 ed a0 bd ed b8 80
   javac 25 wrote  01 00 0d 63 61 66 c3 a9 c0 80 ed a0 bd ed b8 80
   identical       True

   tag 01, length 00 0d = 13 bytes: café is five, the NUL is c0 80,
   and the emoji is two surrogates of three bytes each. The length
   counts bytes of MUTF-8, not characters and not UTF-16 units -- a
   third answer to 'how long is this string', beside DEX's and C's.

3. THE POOL IS COUNTED FROM 1, AND TWO KINDS TAKE TWO SLOTS
------------------------------------------------------------------------
   constant_pool_count   00 07 = 7   (entries + 1, and a Long counts twice)

   #1   Utf8        length 5  b'Hello'
   #2   Class       -> #1
   #3   Long        1
   #4   (unusable)  the second half of the Long: no entry may use this index
   #5   Utf8        length 1  b'x'
   #6   String      -> #4

   #0 does not exist, and the count is one more than the last index.
   The JVM specification calls the two-slot rule 'a poor choice' in
   its own words, and keeps it, because every class file has it.

4. THE LENGTH FIELD IS 16 BITS OF BYTES
------------------------------------------------------------------------
   'A' x 65535   65535 bytes   fits
   'é' x 32767   65534 bytes   fits
   '😀' x 10922   65532 bytes   fits
   '日' x 21845   65535 bytes   fits

   65,535 is the ceiling in BYTES, so how many characters fit depends
   on which characters: 65,535 of A, 32,767 of é, 10,922 emoji. A
   string constant longer than that is a compile error, not a runtime
   one, and the limit is measured after encoding.

Measured: what javac wrote

Section 2 above compares its own encoder with a class compiled on this Mac, and the bytes are the same. The source held one constant, "caf\u00e9\u0000\uD83D\uDE00":

Measured 2026-09-13 — javac 25.0.4.1 (openjdk, brew) on macOS 26.6.2; xxd -l 16 Hello.class, then javap -v, abridged. Not machine-checked: the class is a property of that JDK
00000000: cafe babe 0000 0045 0020 0a00 0200 0307  .......E. ......

  minor version: 0
  major version: 69
  #15 = String             #16            // café\u0000😀
  #16 = Utf8               café\u0000😀

the bytes of #16:  01 00 0d 63 61 66 c3 a9 c0 80 ed a0 bd ed b8 80

00 20 at offset 8 is the pool count, 32, and the first entry's tag 0a is a Methodref. Entry 16 is tag 01, length 00 0d, thirteen bytes — and javap prints the NUL as \u0000 because it cannot draw it, and the emoji as itself, having decoded the six bytes back to one character.

Modified UTF-8, and the length that counts it

The encoding is DEX's page too, because Dalvik took it from here: U+0000 becomes c0 80, so no string holds a zero byte, and a supplementary character becomes two surrogates of three bytes each. The class file adds a rule DEX does not have — the length field is bytes of MUTF-8, sixteen bits of them, so the longest string constant is 65,535 bytes and how many characters that is depends on the characters. Section 4 counts: 65,535 of A, 32,767 of é, 10,922 emoji. The kata below asks for the length field of five strings, and it equals the UTF-8 length for three of them.

Cafebabe, shared

The magic is Java's; Apple reused it for the Mach-O fat header some years later, and a fat binary and a class file are told apart by the next four bytes. Both read them big-endian: a class has minor_version then major_version, and no major has ever been below 45; a fat header has nfat_arch, and no binary has more than a couple of dozen slices. The rule in file(1)'s magic database is exactly that threshold, and its author's comment explains the number:

/usr/share/file/magic/cafebabe — file-5.41, macOS 26.6.2, read 2026-09-13; the comment and the two tests, verbatim
# ... Since there are only
# only 23 labeled Mach-O architectures at current, and the first released
# Java class format was version 43.0, we can safely choose any number
# between 23 and 39 to test the number of architectures against
0	belong		0xcafebabe
>4	ubelong		>30		compiled Java class data,

And measured on both builds, on the class above and on /bin/ls:

Measured 2026-09-13 — file --mime-type -b, file-5.41 on macOS 26.6.2 and file-5.45 in ubuntu:24.04. Not machine-checked
Hello.class   application/x-java-applet   compiled Java class data, version 69.0     (both builds)
/bin/ls       application/x-mach-binary   Mach-O universal binary with 2 architectures  (both builds)

The pool is counted from one

Section 3 builds a six-entry pool and reads it back. Two conventions bite: entries are numbered from 1, and constant_pool_count is one more than the last index; and a Long or Double occupies two slots, the second of which no entry may reference — the specification says, of its own rule, "In retrospect, making 8-byte constants take two constant pool entries was a poor choice." A String entry does not hold text; it holds the index of a Utf8 entry, which is why javap prints #15 = String #16.

What Ghidra checks

JavaLoader lives in the JVM processor module rather than with the other loaders, peeks a big-endian int at offset 0, refuses anything but 0xcafebabe, and loads under the fixed language JVM:BE:32:default — the byte order and width are in the language's name because the file has no field for either. Its name is Java Class File. It does not look at offset 4; a fat Mach-O offered to it would pass the magic test and fail while parsing the pool.

If you are coming from Python or ABAP

Python. struct.unpack('>IHH', data[:8]) is the head, and '>' is not optional. Walking the pool means reading a tag byte and knowing each tag's size — the program does it for five tags — and remembering to advance the index by two after a 5 or a 6. The MUTF-8 decoder is the same dozen lines as on the DEX page; bytes.decode('utf-8') raises on c0 80 and on the surrogates, which is correct of it, and errors='surrogatepass' gets the surrogates through but still refuses the overlong NUL.

ABAP. (Not machine-checked — CI cannot run ABAP.) Nothing on an ABAP stack reads a class file, but the SAP JVM writes them and the length rule is worth knowing when a Java constant travels into a string: the class file's 65,535 is a limit in UTF-8-ish bytes, ABAP's strlen counts UTF-16 units, and a Java String with 40,000 CJK characters fits a Java String and does not fit a class-file constant. Verify any code-page number against the system.

Try it

  1. javac any file and xxd -l 8 the result. Say the Java version from the last two bytes before javap -v confirms it.
  2. javap -v on the same class and find a Utf8 entry with a non-ASCII character. Locate its bytes in the dump and check the length field against the byte count.
  3. Add a "\u0000" to a string constant, recompile, and grep the class for the two bytes c0 80 under LC_ALL=C.
  4. Write a string literal of 30,000 é and compile. Then 33,000. The second is a compiler error about a constant string, and the number it fails at is a byte count.
  5. file a class file and a fat Mach-O side by side, then xxd -l 8 both and find the number file's rule compared.

Practice

Five constants. For each string, say the two bytes javac writes as the length of its CONSTANT_Utf8_info:

"A"
"café"
"Łódź"
"a\u0000b"
"😀"

Before running: which of the five have a length equal to their UTF-8 byte count, and why do the other two differ?

Answers

Verified output of java_class_kata_py.py — regenerated by tools/run_examples.py, never hand-typed.

   string       chars UTF-16 UTF-8 MUTF-8   the two length bytes

   'A'              1      1     1      1   00 01
   'café'           4      4     5      5   00 05
   'Łódź'           4      4     7      7   00 07
   'a\x00b'         3      3     3      4   00 04
   '😀'              1      2     4      6   00 06

   The field is the last column, big-endian. It equals the UTF-8 length
   for three of the five and differs for the two the encoding exists
   to change: the NUL costs one byte more, the emoji two. It never
   equals the character count except for pure ASCII, and it equals
   the UTF-16 count for nothing here but 'A'.

See also

  • DEX — the same encoding with a different length field, and the byte-order tag this format lacks
  • Mach-O — the fat header that shares the magic, and the <25 half of the rule quoted above
  • GZF — Java's writeUTF, which is this entry's encoding and length rule used by Ghidra's own container
  • Overlong sequencesc0 80, and the three formats that break the shortest-form rule on purpose
  • rune is an int32 — Go's answer to the same question, beside Java's char
  • JVM Specification, chapter 4 ↗ — the ClassFile structure and §4.4.7, the CONSTANT_Utf8_info definition