Java Internals & Concurrency/5 lệnh invoke và invokedynamic — method dispatch trong JVM
52/75
Bài 52 / 75~14 phútJVM InternalsMiễn phí lượt xem

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

5 opcode dispatch (invokestatic/special/virtual/interface/dynamic), vtable vs itable, và vì sao lambda + String concat Java 9+ compile thành invokedynamic.

TL;DR: JVM có 5 instruction gọi method, mỗi cái cho một kịch bản dispatch: invokestatic (static, direct call), invokespecial (constructor, super, private pre-Java 11), invokevirtual (instance method, dynamic dispatch qua vtable), invokeinterface (interface method, lookup qua itable), và invokedynamic (indy — target compute lúc runtime qua bootstrap method, cache cho lần sau). Indy là feature đột phá Java 7: cho phép compiler emit code mà target chưa tồn tại lúc compile. Lambda (Java 8), String concat (Java 9, JEP 280) và pattern switch (Java 21, JEP 441) đều build trên indy. Hiểu dispatch cost từng opcode để debug performance — và để không sợ indy "chậm".

Bài 02 dừng ở phần "tĩnh" của bytecode: stack, slot, constant pool. Nhưng câu hỏi thú vị nhất khi đọc javap output là phần "động": một lời gọi list.add("x") compile thành opcode gì, và JVM tìm đúng method để chạy bằng cách nào khi list có thể là ArrayList, LinkedList hay class bạn tự viết?

Bài này đi qua 5 invoke* opcode (invokevirtual / static / special / interface / dynamic), cơ chế vtable/itable, và case study lambda compile thành invokedynamic — feature design clever nhất của Java 7+.

1. 5 invoke* opcode — cốt lõi method dispatch

JVM có 5 instruction để gọi method, mỗi cái cho kịch bản khác:

OpcodeDùng choResolve khi nào
invokestaticStatic methodCompile time, dispatch trực tiếp
invokespecialConstructor, super.x(), privateCompile time, dispatch trực tiếp (no override)
invokevirtualInstance method (non-private)Runtime, dynamic dispatch theo type instance
invokeinterfaceMethod declare ở interfaceRuntime, lookup qua itable
invokedynamicLambda, indy callsiteFirst call → compute target, cache cho lần sau

invokestatic

class M {
    public static int sum(int a, int b) { return a + b; }
}

M.sum(1, 2);

Bytecode:

0: iconst_1
1: iconst_2
2: invokestatic  #2  // Method M.sum:(II)I

Đơn giản nhất. Không có this. Compile-time biết chính xác method nào — direct call.

invokespecial

class B extends A {
    B() {
        super();        // invokespecial A.<init>
    }
    void test() {
        super.foo();    // invokespecial A.foo (no override lookup)
    }
}

Dùng cho:

  • Constructor (<init>).
  • super.method() — gọi method parent specifically, không lookup virtual.
  • Private method (Java trước 11; Java 11+ dùng invokevirtual với check).

invokevirtual — dynamic dispatch

Đây là "polymorphism" của Java implement.

ArrayList<String> list = new ArrayList<>();
list.add("hello");

Bytecode:

aload_1
ldc "hello"
invokevirtual  #5  // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z

Static type là ArrayList (class, không phải interface) — symbolic ref ghi ArrayList.add. Runtime, JVM tra vtable (virtual method table) của instance thực:

ArrayList vtable:
  index 0: add(Object) -> ArrayList.add
  index 1: get(int) -> ArrayList.get
  ...

invokevirtual lookup add(Object) trong vtable của ArrayList, gọi ArrayList.add. Đây là "late binding" / dynamic dispatch. (Opcode do static type quyết định: nếu khai báo List<String> list — kiểu interface — javac sẽ emit invokeinterface qua itable, xem mục kế.)

Cost: 1 vtable lookup mỗi call. Cache CPU + JIT inlining làm gần free trong hot loop.

invokeinterface

interface Drawable { void draw(); }

Drawable d = new Circle();
d.draw();

Bytecode:

aload_1
invokeinterface  #5,  1  // InterfaceMethod Drawable.draw:()V

Khác invokevirtual: vtable interface không thẳng index. Vì 1 class implement nhiều interface, mỗi interface có method ở slot khác → JVM dùng itable (interface method table) — search nhanh.

Trước Java 8 invokeinterface chậm hơn invokevirtual ~10%. Modern JVM optimize bằng inline cache (itable lookup chỉ lần đầu, cache class result) → khác biệt không đáng kể.

invokedynamic — feature đột phá Java 7

invokedynamic (gọi tắt indy) khác hoàn toàn 4 cái trên: target method tự compute lúc runtime.

invokedynamic #5, 0  // BootstrapMethod #0

Cơ chế:

  1. Lần đầu chạy invokedynamic — JVM gọi bootstrap method chỉ định trong constant pool (BootstrapMethods attribute).
  2. Bootstrap method trả về CallSite chứa MethodHandle đến target method thực sự.
  3. JVM cache CallSite — lần sau gọi indy direct, không bootstrap lại.

Lần đầu chậm. Lần sau gần bằng invokestatic direct (JIT inline qua MethodHandle).

Tại sao quan trọng? Cho phép compiler emit code mà target chưa tồn tại lúc compile. Use case lớn nhất: lambda.

2. Case study — lambda compile thành gì?

List<Integer> nums = List.of(1, 2, 3);
nums.forEach(n -> System.out.println(n));

Pre-Java 8 compile cách "cũ" (vẫn hợp lệ): mỗi lambda → 1 anonymous inner class:

nums.forEach(new Consumer<Integer>() {
    public void accept(Integer n) {
        System.out.println(n);
    }
});

Sinh Outer$1.class cho mỗi lambda. Bloat: 1000 lambda = 1000 file .class. Class load thêm 1000 lần. Slow startup.

Java 8 dùng invokedynamic — không sinh class trước:

javac Test.java
javap -c -p Test

Output (lược):

0: invokestatic  #2  // Method java/util/List.of(...):...   (tao list)
...
8: invokedynamic #4, 0  // InvokeDynamic #0:accept:()Ljava/util/function/Consumer;
13: invokeinterface #5, 2  // List.forEach

Dòng 8: invokedynamic produce object Consumer (cái bọc lambda). Bootstrap method LambdaMetafactory.metafactory (JDK chuẩn) sinh class lambda runtime, link với target body method (đã compile thành private static lambda$0 trong class chứa).

Lợi ích:

  • Lazy class generation: lambda chưa dùng → không class. App startup nhanh.
  • JIT optimize tốt: indy đã specialize qua metafactory, JIT inline lambda body vào caller.
  • Ít class file: không sinh Outer$1, Outer$2, ...

String concatenation Java 9+ cũng dùng indy (JEP 280):

String s = "Hello, " + name + "!";

Trước Java 9: javac sinh new StringBuilder().append(...).append(...).toString(). Java 9+: indy với bootstrap StringConcatFactory.makeConcatWithConstants — runtime sinh code optimize cho strategy phù hợp (vd biết length, alloc 1 lần).

3. Switch expression và bytecode

Switch trên int compile thành tableswitch hoặc lookupswitch:

int dayName(int d) {
    switch (d) {
        case 1: return 100;
        case 2: return 200;
        case 3: return 300;
        default: return 0;
    }
}

Bytecode:

tableswitch  {
   1: 28
   2: 32
   3: 36
   default: 40
}
28: bipush 100  ireturn
32: sipush 200  ireturn
36: sipush 300  ireturn
40: iconst_0    ireturn

tableswitch: O(1) — index trực tiếp vào bảng theo (d - min). Compile thành tableswitch khi case dày đặc (1,2,3,4,5).

lookupswitch: case thưa (1, 100, 1000) → bảng (key, target) sort theo key, binary search O(log n).

So với chuỗi if/else (O(n) sequential), switch nhanh hơn nhiều với nhiều case.

Switch trên String (Java 7+) compile 2 tầng: tầng 1 hash → lookupswitch theo hashCode() (hash thưa nên dùng lookupswitch + binary search, không phải tableswitch); tầng 2 String.equals confirm (vì 2 string có thể cùng hash). Chi phí O(log n) trên hash, gần như hằng số thực tế.

Switch pattern matching (Java 21, JEP 441) compile thành invokedynamic với bootstrap SwitchBootstraps — runtime decide dispatch logic.

4. Pitfall tổng hợp

Nhầm 1: Tưởng tất cả method instance = invokevirtual.

Static -> invokestatic
Constructor / super / private (pre-11) -> invokespecial
Interface method -> invokeinterface
Instance non-private -> invokevirtual
Lambda / String concat / pattern switch -> invokedynamic

✅ Mỗi opcode có dispatch cost riêng. Hiểu để debug perf.

Nhầm 2: Tưởng invokedynamic luôn chậm.

Indy cham lan dau (bootstrap). Lan sau JIT cache callsite, gan bang invokestatic.

✅ Đo qua JMH trước khi optimize.

5. 📚 Deep Dive Oracle

📚 Deep Dive Oracle

Spec / reference chính thức:

Ghi chú: JEP 280 và 441 minh hoạ pattern "đổi compile target từ class cụ thể sang indy" — design pattern đáng học cho ai viết compiler / DSL trên JVM: bytecode chỉ ghi "ý định", strategy implementation nằm trong JDK runtime, nâng cấp JDK là code cũ tự hưởng optimization mới mà không cần recompile.

6. Tóm tắt

  • 5 invoke*:
    • invokestatic — static, no this, direct call.
    • invokespecial — constructor, super, private (pre-11).
    • invokevirtual — instance method, dynamic dispatch qua vtable.
    • invokeinterface — interface method, dispatch qua itable.
    • invokedynamic — bootstrap compute target lần đầu, cache lần sau.
  • Lambda compile thành invokedynamic + LambdaMetafactory bootstrap. Lazy gen class — startup nhanh hơn anonymous inner class.
  • String concat Java 9+ dùng invokedynamic + StringConcatFactory.
  • Pattern switch Java 21 dùng invokedynamic + SwitchBootstraps.
  • Switch trên int → tableswitch (dense) hoặc lookupswitch (sparse). O(1) hoặc O(log n).
  • Indy chậm lần đầu (bootstrap), các lần sau JIT inline qua MethodHandle — gần bằng direct call.

7. Tự kiểm tra

Tự kiểm tra
0/5 câu đã trả lời
  1. Q1
    5 invoke* opcode khác nhau thế nào, và compiler quyết định emit cái nào dựa trên gì?
  2. Q2
    Lambda () -> System.out.println("hi") compile thành bytecode gì? Tại sao Java 8+ chọn invokedynamic thay vì anonymous class?
  3. Q3
    Vì sao invokeinterface cần itable thay vì dùng thẳng vtable như invokevirtual?
  4. Q4
    Vì sao invokedynamic chậm ở lần gọi đầu nhưng các lần sau gần bằng direct call?
  5. Q5
    Vì sao Java 9 (JEP 280) chuyển String concat từ StringBuilder sang invokedynamic?

Bài tiếp theo: JIT compiler — interpreter, C1, C2, tiered compilation

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

JIT compiler — interpreter, C1, C2 và tiered compilation