5 lệnh invoke và invokedynamic — method dispatch trong JVM
invokestatic, invokespecial, invokevirtual, invokeinterface, invokedynamic — mỗi opcode một kịch bản dispatch với cost riêng. Vì sao lambda compile thành invokedynamic thay vì anonymous class, String concat Java 9+ và pattern switch Java 21 dùng indy thế nào.
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.
List<String> list = new ArrayList<>();
list.add("hello");
Bytecode:
aload_1
ldc "hello"
invokevirtual #5 // Method java/util/List.add:(Ljava/lang/Object;)Z
Static type là List — symbolic ref ghi List.add. Runtime, JVM tra vtable (virtual method table) của instance thực (ArrayList):
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.
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 → tableswitch theo hash; tầng 2 String.equals confirm (vì 2 string có thể cùng hash). Vẫn O(1) average.
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ì?▸
invokestatic: gọi static method. Khôngthis. Compile-time biết chính xác method → direct call.invokespecial: gọi constructor (<init>),super.method(), private method (Java < 11). Bypass virtual lookup — gọi method exact của type compile-time.invokevirtual: gọi instance method non-private trên class. Runtime dispatch qua vtable — tìm override trong subclass.invokeinterface: gọi method declare ở interface. Runtime dispatch qua itable (interface method table) — phức tạp hơn vtable vì 1 class implement nhiều interface.invokedynamic: target method compute runtime qua bootstrap. Lambda, String concat (Java 9+), pattern switch (Java 21) đều dùng.
Compiler decide dựa trên:
- Method
static? →invokestatic. - Là constructor /
super.x/ private? →invokespecial. - Method declare ở interface? →
invokeinterface. - Lambda / String concat / pattern? →
invokedynamic. - Còn lại (instance method class) →
invokevirtual.
Java 11+ private method gọi qua invokevirtual (JEP 181) thay invokespecial — đơn giản hoá nest mate access.
Q2Lambda () -> System.out.println("hi") compile thành bytecode gì? Tại sao Java 8+ chọn invokedynamic thay vì anonymous class?▸
() -> System.out.println("hi") compile thành bytecode gì? Tại sao Java 8+ chọn invokedynamic thay vì anonymous class?2 phần được sinh:
- Method body: javac sinh private static method
lambda$Ntrong class chứa, body là code lambda:private static void lambda$0() { System.out.println("hi"); } - Indy callsite: tại nơi tạo lambda, javac emit
invokedynamic:invokedynamic #0:run:()Ljava/lang/Runnable; bootstrap: LambdaMetafactory.metafactory args: ..., reference to lambda$0, ...
Runtime: lần đầu chạy indy → bootstrap metafactory generate class YourClass$$Lambda$1 implement Runnable (qua InnerClassLambdaMetafactory — dùng ASM). Class này delegate run() sang lambda$0. Trả về instance qua CallSite.
Lần sau: indy callsite cache, gọi direct constructor không bootstrap lại.
Lý do chọn indy thay anonymous class:
- Lazy class gen: lambda chưa chạy → không class. Pre-Java 8 mỗi lambda 1 file
Outer$1.class→ app 1000 lambda + 1000 class load chậm startup. - Optimization headroom: indy tách "bootstrap strategy" khỏi bytecode → JDK update strategy không cần đổi class file user. Strategy hiện tại có thể đổi (vd cache instance lambda no-capture — singleton).
- JIT inline tốt: indy specialize callsite qua MethodHandle, JIT thấy direct chain → inline lambda body vào caller. Performance gần bằng inline manual.
- Future-proof: feature mới (record, pattern, valhalla) reuse indy infrastructure.
Đây là 1 trong design quyết định nhất Java 7-8 era — Brian Goetz và Mark Reinhold thiết kế để Java tránh "exponential class file bloat".
Q3Vì sao invokeinterface cần itable thay vì dùng thẳng vtable như invokevirtual?▸
invokeinterface cần itable thay vì dùng thẳng vtable như invokevirtual?Với invokevirtual, mỗi method có slot cố định trong vtable theo chain kế thừa: Animal.sound ở index 5 thì Dog.sound override cũng ở index 5. JVM chỉ cần 1 phép index — O(1) tuyệt đối.
Interface phá vỡ giả định đó: 1 class implement nhiều interface, và mỗi interface được implement bởi nhiều class không liên quan. Drawable.draw không thể có cùng index trong vtable của Circle (implement Drawable, Serializable) và Button (implement Drawable, Clickable, Focusable) — các interface chen nhau, không có cách gán slot toàn cục cố định.
Giải pháp: itable — mỗi class có bảng phụ map "interface → vị trí method block". invokeinterface phải search itable theo interface ID trước, rồi mới index vào method — đắt hơn 1 phép index của vtable.
Thực tế modern JVM: inline cache ghi nhớ class gặp lần trước tại callsite — nếu lần sau cùng class, bỏ qua itable lookup. Callsite monomorphic (1 type) gần như free; chỉ callsite megamorphic (3+ type xen kẽ) trả giá lookup đầy đủ. Bài 04 (JIT) phân tích sâu inline cache và type profile.
Q4Vì sao invokedynamic chậm ở lần gọi đầu nhưng các lần sau gần bằng direct call?▸
invokedynamic chậm ở lần gọi đầu nhưng các lần sau gần bằng direct call?Lần đầu: JVM phải chạy bootstrap method — code Java thực sự (vd LambdaMetafactory.metafactory): đọc thông tin callsite từ constant pool, có thể generate class mới qua ASM, link MethodHandle, tạo CallSite. Tốn micro giây đến mili giây — chậm hơn 1 lời gọi method hàng nghìn lần.
Các lần sau: JVM không chạy lại bootstrap. CallSite đã được cache ngay tại callsite trong code — instruction indy trở thành lời gọi qua MethodHandle đã link. JIT thấy chain MethodHandle ổn định → inline xuyên qua, sinh native code gọi thẳng target — performance ngang invokestatic.
Đây là pattern "pay once, amortize forever": chi phí bootstrap chia đều cho hàng triệu lần gọi sau. Hệ quả thực tế:
- App nhiều lambda khởi động chậm hơn một chút (bootstrap dồn lúc warmup) — JDK giảm bằng lazy generation + CDS archive.
- Benchmark phải warm-up trước khi đo (JMH làm tự động) — đo lần gọi đầu sẽ kết luận sai "indy chậm".
Q5Vì sao Java 9 (JEP 280) chuyển String concat từ StringBuilder sang invokedynamic?▸
StringBuilder sang invokedynamic?Trước Java 9, "a" + b + c được javac dịch cứng thành chuỗi new StringBuilder().append(...).append(...).toString() ngay trong bytecode. 2 vấn đề:
- Strategy bị đóng băng trong class file: muốn cải tiến cách nối chuỗi (vd tính trước tổng length để alloc đúng 1 lần) phải recompile mọi class — không thể nâng cấp qua JDK update.
- JIT khó optimize: chuỗi append qua nhiều call với StringBuilder trung gian — escape analysis không phải lúc nào cũng loại bỏ được allocation thừa.
JEP 280: javac chỉ emit invokedynamic với bootstrap StringConcatFactory.makeConcatWithConstants — bytecode chỉ nói "nối các giá trị này", còn cách nối do JDK runtime quyết định. Strategy hiện tại sinh MethodHandle tree tính trước length chính xác, alloc đúng 1 lần byte[] — nhanh hơn StringBuilder trong đa số benchmark.
Lợi ích chiến lược: JDK đổi strategy bao nhiêu lần tuỳ ý (đã đổi vài lần từ Java 9), code compile từ 2017 tự hưởng optimization 2026 mà không recompile. Lưu ý: chỉ áp dụng cho concat biểu thức; concat trong vòng lặp vẫn nên dùng StringBuilder tường minh vì mỗi += là 1 indy callsite tạo String mới.
Bài tiếp theo: JIT compiler — interpreter, C1, C2, tiered compilation
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