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:
| Opcode | Dùng cho | Resolve khi nào |
|---|---|---|
invokestatic | Static method | Compile time, dispatch trực tiếp |
invokespecial | Constructor, super.x(), private | Compile time, dispatch trực tiếp (no override) |
invokevirtual | Instance method (non-private) | Runtime, dynamic dispatch theo type instance |
invokeinterface | Method declare ở interface | Runtime, lookup qua itable |
invokedynamic | Lambda, indy callsite | First 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
invokevirtualvớ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ế:
- Lần đầu chạy
invokedynamic— JVM gọi bootstrap method chỉ định trong constant pool (BootstrapMethods attribute). - Bootstrap method trả về
CallSitechứaMethodHandleđến target method thực sự. - 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
Spec / reference chính thức:
- JVMS §6 — invoke* instructions — mô tả chính xác resolution + dispatch từng opcode.
- JEP 309: Dynamic Class-File Constants —
CONSTANT_Dynamiccho indy nâng cao. - JEP 280: Indify String Concatenation — Java 9 đổi
+String sang indy. - JEP 441: Pattern Matching for switch — Java 21, pattern switch dùng indy.
- LambdaMetafactory javadoc — bootstrap method cho lambda indy.
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, nothis, 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+LambdaMetafactorybootstrap. 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ặclookupswitch(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
- Q15 invoke* opcode khác nhau thế nào, và compiler quyết định emit cái nào dựa trên gì?
- Q2Lambda
() -> System.out.println("hi")compile thành bytecode gì? Tại sao Java 8+ chọninvokedynamicthay vì anonymous class? - Q3Vì sao
invokeinterfacecần itable thay vì dùng thẳng vtable nhưinvokevirtual? - Q4Vì sao
invokedynamicchậm ở lần gọi đầu nhưng các lần sau gần bằng direct call? - Q5Vì sao Java 9 (JEP 280) chuyển String concat từ
StringBuildersanginvokedynamic?
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
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