Worked example - Week 5: reading bytecode¶
Companion to Java Mastery → Month 02 → Week 5: Class Loading and Bytecode. The week explains what bytecode is and how class loading works. This page walks one tiny method through the toolchain so you can see every step concretely.
The source¶
Three lines that do something. Compile it:
Adder.class is the bytecode. It's not text - it's a binary format. We never read it as bytes; we use javap (a JDK tool) to print a human-readable disassembly.
The bytecode, narrated¶
(-v is verbose; -p shows private members too.)
The interesting part:
public int addOne(int);
descriptor: (I)I
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=2, args_size=2
0: iload_1
1: iconst_1
2: iadd
3: ireturn
LineNumberTable:
line 3: 0
Walk it line by line:
-
descriptor: (I)I- the method's type, written in JVM internal notation.(I)Imeans "takes one int, returns an int."(Ljava/lang/String;)Vwould mean "takes a String, returns void." The shorthand letters never go away; you'll see them in stack traces and bytecode forever. -
stack=2, locals=2- at runtime this method needs at most 2 slots on the operand stack and uses 2 local variable slots. The JVM verifier checked these limits when the class was loaded; if they're wrong the class won't load. -
args_size=2- two arguments:this(instance methods get an implicitthisas local 0) andx(local 1). Astaticmethod would haveargs_size=1for this signature. -
0: iload_1- push local variable 1 (x) onto the operand stack. Theiprefix is "integer."iload_1is a single-byte opcode optimized for the common case "load int from local 1." -
1: iconst_1- push the integer constant1. Again a single-byte opcode for a tiny common case. -
2: iadd- pop the top two ints from the stack, add them, push the result. After this instruction the stack has one int:x + 1. -
3: ireturn- pop the top int and return it to the caller.
That's the whole method: 4 bytes of bytecode plus a 1-entry line number table.
What this tells you¶
- The JVM is a stack machine. Operations consume and produce values on an evaluation stack, not registers. The JIT will later allocate those stack slots to real CPU registers, but the bytecode itself is stack-based for portability.
- There are dozens of "constant int" opcodes.
iconst_0throughiconst_5, plusiconst_m1,bipush(one-byte int),sipush(two-byte),ldc(load from constant pool). Each is one byte; the verbose ones are last-resort. thisis just local 0. Instance methods are static methods with an extra implicit argument. Theinvokevirtualopcode you'd see at a call site pushesthisfirst, then the arguments.
The trap¶
You might assume the JVM interprets this bytecode and that's why Java is "slower than C." That's true for the first ~10,000 calls. After that, HotSpot's C2 compiler typically inlines addOne into the caller and emits one CPU instruction (add eax, 1 or similar). The bytecode is the source for the JIT, not the runtime program.
This is why the curriculum's later weeks on the JIT matter: bytecode is the contract, but it's almost never what actually runs.
Exercise¶
- Write a class with two methods:
addOne(int)andaddOne(long). Compile andjavap -v -p. Compare the bytecode. What changed? - Add a third method
addOne(Integer). Predict the bytecode before runningjavap. Then look. What does autoboxing produce? - Use the new Class-File API (JDK 22+,
java.lang.classfile) to generate the sameAdder.classprogrammatically and write it to disk. Confirmjavapoutput matches the compiler's.
Related reading¶
- The main Week 5 chapter covers the loading process around bytecode.
- The senior Glossary defines Bytecode, Class loader, Constant pool.
- The Type systems cross-topic page puts JVM descriptors next to Rust's monomorphization and Go's interfaces.