Java Internals & Concurrency/Class file và javap — đọc instruction JVM từ binary
51/75
Bài 51 / 75~14 phútJVM InternalsMiễn phí lượt xem

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ì:

  1. đơn vị compile Java — compiler frontend (javac) và optimizer backend (JIT) giao tiếp qua bytecode.
  2. boundary tương thích — Kotlin, Scala, Groovy, Clojure compile sang cùng bytecode → chạy chung JVM.
  3. tool debug ultimate: lambda gen ra gì, String.format thật sự gọi gì, switch expression compile thành table hay lookup — javap cho câu trả lời chính xác.
  4. 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*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ườngJVM
Máy tính bỏ túi CasioCPU x86/arm (register-based)
Máy tính RPN HPJVM (stack-based)
Stack RPNOperand stack
Phím sốiconst_*, bipush
Phím toániadd, imul, isub
Bộ nhớ tạm M+Local variable (istore/iload)
💡 Cách nhớ

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

JavaMajor
1.045
852
1155
1761
2165

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. _1 là 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_*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 — int
  • l — long
  • f — float
  • d — double
  • a — reference (object, array)
  • b — byte (chỉ trong load/store array)
  • c — char
  • s — 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

longdouble 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:

  • B byte, S short, I int, J long, F float, D double, C char, Z boolean.
  • V void.
  • L<class>; reference — Ljava/lang/String;.
  • [<type> array — [I int array, [[I int 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

📚 Deep Dive Oracle

Spec / reference chính thức:

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à this cho 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 -c dump bytecode. javap -v thê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

Tự kiểm tra
0/5 câu đã trả lời
  1. Q1
    Vì sao JVM thiết kế stack-based thay vì register-based như CPU x86?
  2. Q2
    Đoạn sau compile ra bao nhiêu instruction? int x = 5; int y = x + 3; return y * 2;
  3. Q3
    Vì sao class compile target Java 21 không chạy được trên JRE 17?
  4. Q4
    Vì sao constant pool tồn tại — tại sao không inline string/method ref trực tiếp vào bytecode?
  5. Q5
    Vì sao longdouble chiếm 2 slot local variable trong khi int và 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

Đặt 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

Bài tiếp theo

5 lệnh invoke và invokedynamic — method dispatch trong JVM