Class file và javap — đọc instruction JVM từ binary
Class file binary (magic 0xCAFEBABE, constant pool, bytecode) và JVM máy stack-based; dùng javap đọc bytecode, operand stack, local slot và method descriptor.
TL;DR: Compiler javac không sinh machine code — nó sinh bytecode: instruction của một máy ảo stack-based. Mọi tính toán đi qua operand stack (push arg, pop kết quả), khác CPU x86 dùng register. File .class là binary với magic 0xCAFEBABE, major version (65 = Java 21), constant pool (bảng symbolic reference mà bytecode index vào), và bytecode từng method. javap -c disassemble bytecode, javap -v in thêm constant pool. Pitfall lớn nhất: compile target version cao hơn JRE runtime → UnsupportedClassVersionError. Đọc được bytecode là nền tảng để hiểu method dispatch (bài 03) và JIT (bài 04).
Bạn viết:
int x = a + b * 2;
Compiler không sinh ra "máy lệnh CPU" — nó sinh ra bytecode, instruction của một máy ảo trừu tượng JVM. Cùng file .class chạy trên Linux x86, macOS arm64, Windows — JVM mỗi platform translate bytecode → CPU instruction native lúc runtime.
Bytecode quan trọng vì:
- Là đơn vị compile Java — compiler frontend (javac) và optimizer backend (JIT) giao tiếp qua bytecode.
- Là boundary tương thích — Kotlin, Scala, Groovy, Clojure compile sang cùng bytecode → chạy chung JVM.
- Là tool debug ultimate: lambda gen ra gì,
String.formatthật sự gọi gì, switch expression compile thành table hay lookup —javapcho câu trả lời chính xác. - Là nền tảng hiểu JIT (bài 04). JIT đọc bytecode, profile, sinh native code optimize. Không hiểu bytecode → không debug được vì sao JIT không inline method bạn nghĩ là hot.
Bài này đi qua phần "tĩnh": stack-based VM (khác register-based), cấu trúc class file, javap, operand stack + local variable, và constant pool. Phần "động" — 5 lệnh invoke* và invokedynamic — nằm ở bài 03.
1. Analogy — Máy tính bỏ túi RPN vs máy thường
Máy tính bỏ túi thường (Casio): bạn bấm 2 + 3 = để được 5. Trong CPU x86 cũng vậy — instruction có register: add eax, ebx (cộng eax với ebx, kết quả eax).
Máy tính RPN (Reverse Polish Notation, HP 12C, HP 50G): bạn nhập 2 ENTER 3 + để được 5. Mỗi số push vào stack. Operator pop 2 số trên cùng, push kết quả.
JVM là máy RPN. Mọi tính toán qua operand stack. Mọi instruction pop arg từ stack, push kết quả.
Java: int sum = a + b;
Bytecode: iload a -> push a vao stack
iload b -> push b vao stack
iadd -> pop 2 so, cong, push ket qua
istore sum -> pop ket qua, luu vao local sum
| Đời thường | JVM |
|---|---|
| Máy tính bỏ túi Casio | CPU x86/arm (register-based) |
| Máy tính RPN HP | JVM (stack-based) |
| Stack RPN | Operand stack |
| Phím số | iconst_*, bipush |
| Phím toán | iadd, imul, isub |
| Bộ nhớ tạm M+ | Local variable (istore/iload) |
JVM = máy RPN. Mọi instruction pop arg từ operand stack, push kết quả. Khác CPU x86 dùng register. Khi đọc bytecode, hình dung stack — mỗi dòng làm gì với stack top.
2. Class file — anatomy
Trước khi đọc bytecode, biết class file chứa gì.
.class file là binary, layout cố định (JVMS §4):
ClassFile {
u4 magic = 0xCAFEBABE // Marker JVM nhan dien
u2 minor_version
u2 major_version // 65 = Java 21, 61 = Java 17, 52 = Java 8
u2 constant_pool_count
cp_info constant_pool[count-1] // String, class name, method ref, ...
u2 access_flags // public, final, abstract, ...
u2 this_class
u2 super_class
u2 interfaces_count
u2 interfaces[count]
u2 fields_count
field_info fields[count]
u2 methods_count
method_info methods[count] // Bytecode trong day
u2 attributes_count
attribute_info attributes[count] // SourceFile, LineNumberTable, ...
}
Quan trọng nhất:
- Constant pool: bảng symbolic references — string literal, tên class, tên method, tên field. Bytecode tham chiếu các slot trong pool qua index 2-byte.
- methods: mỗi method có bytecode array, max stack depth, max local count.
Magic number 0xCAFEBABE
4 byte đầu mọi class file. Hex CAFE BABE — James Gosling chọn vì dễ nhớ và đọc được. JVM check magic — không match → reject "not a class file".
Major version
| Java | Major |
|---|---|
| 1.0 | 45 |
| 8 | 52 |
| 11 | 55 |
| 17 | 61 |
| 21 | 65 |
Class compile target Java 21 (major 65) chạy trên JRE 17 → UnsupportedClassVersionError. JRE chỉ chạy class major ≤ JRE version. Đây là lý do "compile target version phải ≤ runtime version".
javac --release 17 Foo.java → ép major = 61, đảm bảo chạy được JRE 17+.
3. javap — đọc bytecode
javap (Java disAssembler) ship với JDK. Dùng:
javap -c MyClass.class # Disassemble bytecode
javap -p -c MyClass.class # Include private member
javap -v MyClass.class # Verbose: constant pool + attributes
Ví dụ đơn giản
public class Hello {
public int add(int a, int b) {
int sum = a + b;
return sum;
}
}
Compile và disassemble:
javac Hello.java
javap -c Hello.class
Output:
public class Hello {
public Hello();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public int add(int, int);
Code:
0: iload_1 // Push a (local slot 1) vao stack
1: iload_2 // Push b (local slot 2) vao stack
2: iadd // Pop 2 int, cong, push ket qua
3: istore_3 // Pop, luu vao local slot 3 (sum)
4: iload_3 // Push sum
5: ireturn // Return int top of stack
}
Đọc cột:
0:,1:,2:— bytecode offset (vị trí trong byte array).iload_1— opcode + operand._1là implicit operand (local slot 1).- Comment sau
//từ constant pool — javap resolve symbolic ref thành text.
Phân tích add method
a + b chỉ cần 3 instruction + return:
iload_1 # Push a
iload_2 # Push b
iadd # Pop 2 int -> push tong
ireturn # Pop -> return
Local slot:
- Slot 0:
this(instance method cóthisở slot 0). - Slot 1:
a(param 1). - Slot 2:
b(param 2). - Slot 3:
sum(local).
Tại sao iadd không có operand? — Vì JVM stack-based: opcode iadd ngầm định "pop 2 int trên cùng stack, push tổng". Không cần chỉ định argument.
So với x86:
mov eax, [a]
add eax, [b]
mov [sum], eax
ret
x86 cần chỉ rõ register eax. Bytecode không cần — stack ngầm định.
iconst_* và bipush
Push constant nhỏ vào stack:
iconst_m1 # Push -1
iconst_0 # Push 0
iconst_1 # Push 1
iconst_2 # Push 2
iconst_3 # Push 3
iconst_4 # Push 4
iconst_5 # Push 5
bipush 100 # Push byte (8-bit, -128 ~ 127)
sipush 1000 # Push short (16-bit, -32768 ~ 32767)
ldc #5 # Load constant tu pool (int 32-bit, float, String, ...)
Vì sao tách? Optimize size: iconst_0 1 byte, bipush 0 2 byte. Số dùng nhiều (-1 đến 5) có opcode riêng.
4. Operand stack và local variable
Mỗi method invocation tạo stack frame:
+---------------------+
| Operand Stack | <- max_stack (compute boi compiler)
+---------------------+
| Local Variables | <- max_locals
+---------------------+
| Frame Data | <- return PC, exception table reference, ...
+---------------------+
Frame nằm trên JVM Stack của thread (không phải heap). Method return → pop frame.
Stack tracing ví dụ
int compute(int x) {
int y = x * 2;
return y + 10;
}
Bytecode:
0: iload_1 # stack: [x]
1: iconst_2 # stack: [x, 2]
2: imul # stack: [x*2]
3: istore_2 # stack: [] local: [..., y=x*2]
4: iload_2 # stack: [y]
5: bipush 10 # stack: [y, 10]
7: iadd # stack: [y+10]
8: ireturn # return
Stack snapshot mỗi dòng — đọc tay theo dõi. Compiler tính trước max_stack = 2 (max depth tại bất kỳ điểm nào) và max_locals = 3 (this + x + y).
Verifier (mục bài 01) check: bytecode không ghi quá max_stack, không pop khi rỗng, không type mismatch (iadd cần 2 int trên top).
Type-prefix opcode
Opcode prefix biểu thị type:
i— intl— longf— floatd— doublea— reference (object, array)b— byte (chỉ trong load/store array)c— chars— short
Thao tác cộng: iadd, ladd, fadd, dadd. JVM không có generic add — type cố định để verifier check.
iload_1 # push int
lload_1 # push long (chiem 2 slot vi long 64-bit)
aload_1 # push reference
iadd # cong int
ladd # cong long
long và double chiếm 2 slot local (vì JVM slot 32-bit, 64-bit cần 2 slot). Slot 1 và 2 dùng cho long ở slot 1.
5. Constant pool
Mỗi class có constant pool — bảng symbolic reference. Bytecode tham chiếu pool qua index.
javap -v Hello.class
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
#5 = Utf8 <init>
#6 = Utf8 ()V
#7 = Class #8 // Hello
#8 = Utf8 Hello
...
Loại entry phổ biến:
Utf8— string literal text (tên method, tên class, signature).Class— reference 1 class (chứa index Utf8 chứa tên).Methodref— reference 1 method (Class + NameAndType).NameAndType— name + descriptor pair (vd<init>:()V).String— string literal Java.Integer,Long,Float,Double— numeric constant.
Bytecode invokespecial #1 nghĩa "invoke method tại pool slot 1" — tức java/lang/Object.<init>:()V.
Method descriptor
Format compact biểu diễn signature:
(int, String) -> boolean => (ILjava/lang/String;)Z
() -> void => ()V
(byte[]) -> int => ([B)I
(Object, int[]) -> long[] => (Ljava/lang/Object;[I)[J
Quy tắc:
Bbyte,Sshort,Iint,Jlong,Ffloat,Ddouble,Cchar,Zboolean.Vvoid.L<class>;reference —Ljava/lang/String;.[<type>array —[Iint array,[[Iint 2D,[Ljava/lang/String;String array.
Format này từ JVMS §4.3.3 — format network/file binary cô đọng. Nhìn quen sau vài lần.
6. Pitfall tổng hợp
❌ Nhầm 1: Tưởng int chiếm 1 slot, long 1 slot.
JVM Long chiem 2 slot local. lstore_1 ghi long vao slot 1+2.
✅ Memorize: int/float/ref = 1 slot; long/double = 2 slot.
❌ Nhầm 2: Nghĩ bytecode chậm vì là VM.
// Bytecode chay tren JIT-compiled native code, sau warmup
// gan bang C performance.
✅ Bytecode là input cho JIT, không phải execution model thực sự.
❌ Nhầm 3: Compile target version cao hơn JRE runtime.
javac --release 21 Foo.java # Class major 65
java -version # JRE 17 (major 61)
java Foo # UnsupportedClassVersionError
✅ Compile target ≤ JRE version. CI matrix test multiple version.
❌ Nhầm 4: Đọc bytecode mà bỏ qua constant pool.
javap -c chi disassemble bytecode + comment.
javap -v in ca constant pool, attribute, line table.
✅ Debug deep cần -v.
7. 📚 Deep Dive Oracle
Spec / reference chính thức:
- JVMS §4 The class File Format — class file binary layout chi tiết.
- JVMS §6 The Java Virtual Machine Instruction Set — full opcode reference, mô tả từng instruction.
javapman page — đầy đủ flag.
Ghi chú: JVMS §6 dày 200+ trang, mỗi opcode 1 entry. Không cần thuộc — biết tra. Khi debug bytecode lạ, copy opcode ra search trong §6 — có description chính xác (operand, stack effect, exception).
8. Tóm tắt
- JVM là stack-based VM — instruction pop arg từ operand stack, push kết quả. Khác CPU x86/arm register-based.
- Class file binary, magic
0xCAFEBABE, major version chỉ JDK target. Major lớn hơn runtime →UnsupportedClassVersionError. - Constant pool chứa symbolic reference (string, class name, method ref). Bytecode index vào pool.
- Stack frame mỗi method invocation: operand stack + local variables + frame data. Frame trên JVM Stack thread.
- Local slot: int/float/ref = 1 slot; long/double = 2 slot. Slot 0 thường là
thischo instance method. - Opcode type-prefix:
i*int,l*long,f*float,d*double,a*reference. Verifier check type strict. - Method descriptor:
(ILjava/lang/String;)Z= nhận int + String, trả boolean. Format JVMS §4.3.3. javap -cdump bytecode.javap -vthêm constant pool + attribute. Tool debug essential.- Bytecode → JIT → native code. Bytecode không chạy trực tiếp sau warmup — JIT specialize.
9. Tự kiểm tra
- Q1Vì sao JVM thiết kế stack-based thay vì register-based như CPU x86?
- Q2Đoạn sau compile ra bao nhiêu instruction?
int x = 5; int y = x + 3; return y * 2; - Q3Vì sao class compile target Java 21 không chạy được trên JRE 17?
- Q4Vì sao constant pool tồn tại — tại sao không inline string/method ref trực tiếp vào bytecode?
- Q5Vì sao
longvàdoublechiếm 2 slot local variable trong khiintvà reference chỉ 1 slot?
Bài tiếp theo: 5 lệnh invoke và invokedynamic — method dispatch trong JVM
Bài này đáng gửi cho bạn học cùng?
Copy link đã gắn nguồn — dán group, chat, hoặc LinkedIn.
Bài này có giúp bạn hiểu bản chất không?
Hỏi đáp về bài này
Chưa có câu hỏi
Có gì chưa rõ trong bài? Đặt câu hỏi đầu tiên — câu trả lời từ cộng đồng giúp bạn (và người sau).
Đặt câu hỏi đầu tiên