
上一篇【第48篇】类和对象测试——HelloWorld 终于活了下一篇【第50篇】方法符号引用解析与参数传递——方法调用的准备摘要第 6 章结束时jvmgo 已经能创建对象、读写字段了但它有个致命缺陷它没有调用栈。INVOKE_SPECIAL只是把this弹掉假装调用过INVOKE_VIRTUAL靠识别描述符来 hackSystem.out.println。第 7 章要一次性解决这个问题。这一章的主角是5 条方法调用指令指令用途绑定方式invokestatic静态方法静态绑定invokespecial构造器 / 私有方法 /super.调用静态绑定invokevirtual实例方法动态绑定invokeinterface接口方法动态绑定invokedynamicLambda / 动态语言本书不实现本文先把全景图画清楚方法能怎么分类、5 条指令各自负责什么、JVM 调用一个方法到底要走哪几步。具体的解析算法、指令实现和类初始化留给后面三篇。读完这篇你会明白为什么super.equals(null)用invokespecial而不是invokevirtual用后者会死循环以及为什么接口方法不能和invokevirtual合并vtable 的前提是继承层次固定。一、方法的三组分类维度在讨论指令之前先把方法这件事分清楚。书里用了三个不同的维度混着看容易乱我整理成下面这张表。1.1 按调用方式分静态方法 vs 实例方法publicclassDemo{publicstaticvoids(){}// 静态方法通过类调用Demo.s()publicvoidi(){}// 实例方法通过对象调用obj.i()}静态方法类方法属于类不依赖任何对象实例。调用它不需要this。实例方法属于对象。调用它需要一个隐藏的第 0 号参数this。这个区别直接影响栈帧布局静态方法main的局部变量表第 0 个 slot 就是args而实例方法test的第 0 个 slot 是this参数从 1 开始。1.2 按实现方式分三类类别说明本章处理抽象方法只有签名没有实现abstract解析/查找到它要抛AbstractMethodErrorJava 方法用 Java或 Groovy/Scala 等 JVM 语言实现有Code属性本章主角本地方法用 C/C 实现native无Code属性第 9 章本章遇到就panic注意一条规则静态方法和抽象方法互斥。Java 8 之前接口里只能有抽象方法Java 8 为支持 Lambda 放宽了限制允许接口有static方法和default方法。本章不实现接口的静态方法和默认方法代码是按 JVM 规范第 7 版写的这点在第 7 章反复被强调——如果你拿 JDK 8 的rt.jar跑某些接口-heavy 的代码可能会遇到意料之外的行为。1.3 按绑定时机分静态绑定 vs 动态绑定这是本章最核心的概念。静态绑定static binding / early binding编译期就能确定最终调用哪个方法。静态方法没有多态Demo.s()永远调用Demo.s。私有方法子类看不见不可能被重写。构造器init不会被重写。final方法语法上禁止重写不过 JVM 层面 HotSpot 对final仍走invokevirtual。动态绑定dynamic binding / late binding编译期只知道方法名 描述符运行期才知道对象的真实类型从而决定调哪个实现——这就是多态。AnimalanewDog();// 编译期类型 Animal运行期类型 Doga.speak();// 实际调用 Dog.speak()a.speak()编译出的字节码是invokevirtual Animal.speak:()V但执行时 JVM 看的是栈顶那个对象的真实类Dog从Dog开始往上找speak的实现。编译期 运行期 ┌────────────────────┐ ┌────────────────────┐ │ invokevirtual │ │ 栈顶 ref 指向的对象 │ │ Animal.speak:()V │ ───────▶ │ 真实类 Dog │ │ 符号引用 │ │ → 查 Dog.speak() │ └────────────────────┘ └────────────────────┘ 只知道叫 speak 才知道是狗在叫二、JVM 的 5 条方法调用指令Java 7 之前有 4 条Java 7 为支持动态类型语言加了第 5 条。2.1 分工表┌─ invokestatic 静态方法static binding │ ┌─ 静态绑定 ────────┼─ invokespecial 构造器 init │ │ 私有方法 │ │ super.xxx() 超类方法 │ └─ final 方法仍走 invokevirtual 方法调用│ │ ┌─ invokevirtual 类类型的实例方法多态分派 └─ 动态绑定 ────────┤ ├─ invokeinterface 接口类型的实例方法多态分派 └─ invokedynamic Lambda / 动态语言本章不实现用一个 Java 类把前四条全演示一遍——这是第 7 章的测试类InvokeDemopackagejvmgo.book.ch07;publicclassInvokeDemoimplementsRunnable{publicstaticvoidmain(String[]args){newInvokeDemo().test();// invokespecial(init) invokevirtual(test)}publicvoidtest(){InvokeDemo.staticMethod();// invokestaticInvokeDemodemonewInvokeDemo();// invokespecial initdemo.instanceMethod();// invokespecial私有方法super.equals(null);// invokespecialsuper 调用this.run();// invokevirtual((Runnable)demo).run();// invokeinterface}publicstaticvoidstaticMethod(){}privatevoidinstanceMethod(){}Overridepublicvoidrun(){}}javap -c一下test()就能看到编译器的选择0: invokestatic #2 // Method staticMethod:()V 3: new #3 // class InvokeDemo 6: dup 7: invokespecial #4 // Method init:()V 10: astore_1 11: aload_1 12: invokespecial #5 // Method instanceMethod:()V ← 私有方法也用 invokespecial 15: aload_0 16: aconst_null 17: invokespecial #6 // Method java/lang/Object.equals:(Ljava/lang/Object;)Z 20: pop 21: aload_0 22: invokevirtual #7 // Method run:()V ← this.run() 25: aload_1 26: invokeinterface #8, 1 // InterfaceMethod java/lang/Runnable.run:()V 31: return两个值得注意的点demo.instanceMethod()是私有方法编译成invokespecial而不是invokevirtual。因为私有方法不参与多态静态绑定更快。同一个run()方法this.run()是invokevirtual((Runnable) demo).run()是invokeinterface——指令的选择取决于引用的静态类型而不是对象本身。2.2 为什么需要单独的 invokeinterface这是面试高频题。书里的解释很精炼统一使用invokevirtual完全可以但是可能会影响效率。原因在于vtable虚函数表优化的前提invokevirtual 的情况 this 引用指向某个「类」或其子类的实例 → 类的继承层次是「线性固定」的 → 虚拟机可以预先为每个类算好一张 vtable → 每个虚方法在表里有一个「固定下标」 → 调用时obj.vtable[固定下标] → O(1) invokeinterface 的情况 this 引用可以指向「任何实现了该接口的类」的实例 → Cat 实现 RunnableDog 也实现 Runnable它俩毫无继承关系 → 无法给 Runnable.run() 分配一个全 JVM 通用的固定下标 → 只能用 itable接口方法表先按接口找到表再在表里线性/二分查找 → 调用时O(1) ~ O(n)用图表示Object Runnable (interface) │ ▲ Animal ┌──────┴──────┐ ╱ ╲ │ │ Dog Cat Dog Car 毫无共同父类 invokevirtual: Dog 的 vtable 里 speak 永远在下标 3 Animal 的 vtable 里 speak 也永远在下标 3 → 父类和子类下标对齐查一次就够 invokeinterface: Runnable.run 在 Dog 的 itable 里可能在下标 0 在 Car 的 itable 里可能在下标 5 → 下标不对齐必须先按接口名做一次搜索jvmgo 作为教学实现没有做 vtable/itable 优化两条指令都是老老实实地调LookupMethodInClass做继承链遍历。但指令本身的区分被保留下来了——这正是 JVM 规范的设计意图给实现者留出优化空间。2.3 invokedynamic 为什么不用管invokedynamic是 Java 7 为动态类型语言JRuby、Groovy加的Java 8 的Lambda 表达式是它在 Java 语言层面的第一个大规模应用。它和前四条有个本质区别前四条的分派逻辑由 JVM 硬编码在规范里而invokedynamic的分派逻辑由用户代码Bootstrap Method决定。Runnabler()-System.out.println(hi);// 编译后// 0: invokedynamic #2, 0 // InvokeDynamic #0:run:()Ljava/lang/Runnable;// ↑ 指向 BootstrapMethods 属性里的一个方法实现它需要方法句柄MethodHandle、调用点CallSite、LambdaMetafactory……一整套java.lang.invoke基础设施。本书没实现我们这个系列跟着书的节奏走也不实现。但invokedynamic是 JVM 8 里最值得单独写一篇的指令等 70 篇主线走完如果有精力可以补一篇番外。三、JVM 调用一个方法的完整流程不管哪条 invoke 指令骨架都是一样的。书里给了这段伪代码堪称第 7 章的纲func(self*INVOKE_XXX)Execute(frame*rtda.Frame){// ① 拿常量池cp:frame.Method().Class().ConstantPool()// ② 通过索引取方法符号引用methodRef:cp.GetConstant(self.Index).(*heap.MethodRef)// ③ 解析符号引用 → 得到一个直接可引用的方法resolved:resolveMethodRef(methodRef)// ④ 检查static? 访问权限? 抽象?checkResolvedMethod(resolved)// ⑤ 查找最终要调用的方法动态绑定的关键toBeInvoked:findMethodToInvoke(methodRef)// ⑥ 创建新栈帧压入 JVM 栈newFrame:frame.Thread().NewFrame(toBeInvoked)frame.Thread().PushFrame(newFrame)// ⑦ 从调用者操作数栈弹出参数填入新帧的局部变量表passArgs(frame,newFrame)}七步对应的后文步骤内容在哪一篇①②③方法符号引用解析第 050 篇⑥⑦建帧与参数传递第 050 篇④各类Error检查第 051 篇⑤invokestatic/special/virtual/interface各自的分派第 051 篇—方法返回6 条返回指令第 051 篇—栈空检测、clinit触发第 052 篇画成时序图调用者帧 JVM 栈 被调用者帧 ──────── ──────── ────────── 操作数栈: [arg2] [arg1] [this ] ← 栈顶 │ │ ①②③④⑤ 解析符号引用 查找目标方法 ▼ 找到 Method M │ │ ⑥ newFrame : thread.NewFrame(M) │ thread.PushFrame(newFrame) ├────────────────────▶ ┌──────────┐ │ │ 新帧 M │ ◀── 当前帧切换 │ ├──────────┤ │ ⑦ passArgs: │ 新帧 main│ │ 从调用者栈 pop └──────────┘ │ 从 argSlotCount-1 │ 倒序到 0 ├──────────────────────▶ LocalVars: │ [0] this │ [1] arg1 │ [2] arg2 ▼ loop() 继续但 CurrentFrame() 已经是新帧 → 从 M 的字节码第 0 条开始执行关键点invoke 指令执行完就结束了它不阻塞等待返回。返回是由被调用方法的最后一条返回指令return/ireturn/ …完成的——把当前帧弹掉返回值推进调用者帧的操作数栈顶。loop()每轮都取thread.CurrentFrame()所以切换是自动发生的。这个设计非常优雅方法调用和返回被完全解耦成两条独立指令。四、操作数n1 个从哪来书里有句话容易看漏但很重要方法调用指令需要n1 个操作数其中第 1 个操作数是 uint16 索引……剩下的 n 个操作数是要传递给被调用方法的参数从操作数栈中弹出。也就是说字节码里 [opcode][index_hi][index_lo] └──── 1 个显式操作数常量池索引 运行时栈上 ... , this, arg1, arg2 └──── n 个隐式操作数参数在操作数栈上n 参数的 slot 数argSlotCount。对实例方法n还要 1算上this。4.1 argSlotCount 是怎么算出来的参数占多少 slot完全由方法描述符决定func(self*Method)calcArgSlotCount(){parsedDescriptor:parseMethodDescriptor(self.descriptor)for_,paramType:rangeparsedDescriptor.parameterTypes{self.argSlotCountifparamTypeJ||paramTypeD{self.argSlotCount// long 和 double 占 2 个 slot}}if!self.IsStatic(){self.argSlotCount// 实例方法额外加一个 this}}举例方法描述符静态?参数 slotthisargSlotCount()Vstatic000(I)Vstatic101(J)Vstatic202(IJD)Vstatic122505()V实例011(Ljava/lang/String;)V实例112(Ljava/lang/String;J)V实例12314为什么argSlotCount要缓存在Method结构体里因为每次方法调用都要用它——既要用来从操作数栈上定位thisGetRefFromTop(argSlotCount-1)又要用来循环传参。每次都重新解析一遍描述符太浪费所以在newMethods()里算一次存下来。五、本章要抛的那些 Error方法调用是 JVM 里抛异常种类最多的操作之一。提前列个清单第 051 篇看到具体代码时就不懵了异常触发条件IncompatibleClassChangeError类/接口方法符号引用解析到了错误种类的东西invokestatic遇到非静态方法invokespecial/invokevirtual遇到静态方法invokeinterface遇到 static 或 private 方法对象没实现该接口NoSuchMethodError解析不到方法init的声明类 ≠ 解析出的类NullPointerExceptionthis引用为 nullIllegalAccessError访问控制不通过protected方法被非法的类调用接口方法非 publicAbstractMethodError找到的方法是抽象的没有实现记忆技巧IncompatibleClassChangeError→ “东西的种类不对”静态/实例、类/接口NoSuchMethodError→ “找不到”IllegalAccessError→ “找到了但没权限”AbstractMethodError→ “有权限但没实现”是一个从存在性到可用性的层层递进。六、本章的代码结构调整第 7 章相比第 6 章改动的面比第 5 章还大先列个清单心里有数ch07/ ├── rtda/ │ ├── thread.go ← 新增 IsStackEmpty()、TopFrame() │ ├── jvm_stack.go ← 新增 isEmpty() │ ├── frame.go ← 新增 RevertNextPC() │ ├── local_vars.go ← 新增 SetSlot() │ └── heap/ │ ├── class.go ← 新增 initStarted / InitStarted() / StartInit() │ │ 新增 GetClinitMethod() │ ├── method.go ← 新增 argSlotCount / ArgSlotCount() / calcArgSlotCount() │ ├── method_ref.go ← 新增 ResolvedMethod() / resolveMethodRef() │ ├── interface_method_ref.go ← 新增 ResolvedInterfaceMethod() 等 │ ├── method_lookup.go ← 新增 lookupMethod / LookupMethodInClass / lookupMethodInInterfaces │ └── operand_stack.go ← 新增 GetRefFromTop() ├── instructions/ │ ├── base/ │ │ ├── method_invoke_logic.go ← 【新增】InvokeMethod() │ │ └── class_init_logic.go ← 【新增】InitClass() │ ├── control/return.go ← 【新增】6 条返回指令 │ └── references/ │ ├── invokestatic.go ← 【新增】 │ ├── invokespecial.go ← 第 6 章有本次重写 │ ├── invokevirtual.go ← 第 6 章有本次重写 │ └── invokeinterface.go ← 【新增】 ├── interpreter.go ← 大改支持多帧循环 ├── cmd.go ← 新增 verboseClassFlag / verboseInstFlag └── main.go ← 改 startJVM()两个新文件值得单独说method_invoke_logic.go放InvokeMethod()——建帧 压栈 传参的公共逻辑。四条指令都要用抽出来避免重复。class_init_logic.go放InitClass()——类初始化的公共逻辑。new/getstatic/putstatic/invokestatic四条指令都要用。这是典型的**“提取公共逻辑到 base 包”**模式和第 5 章把Branch()放到 base 包是一脉相承的。本篇小结第 7 章开篇我们把方法调用的全景图铺开了方法有三种分类维度——调用方式静态/实例、实现方式抽象/Java/本地、绑定时机静态绑定/动态绑定。动态绑定就是多态是invokevirtual和invokeinterface存在的理由。5 条 invoke 指令的分工本质是按绑定方式 引用类型做二分invokestatic静态、invokespecial构造器/私有/super、invokevirtual类引用、invokeinterface接口引用、invokedynamic本书跳过。invokeinterface不能并入invokevirtual不是功能问题而是性能问题——接口的多实现破坏了 vtable 的下标对齐前提必须走 itable。方法调用七步走取常量池 → 取符号引用 → 解析 → 检查 → 查找目标方法 → 建帧压栈 → 传参。调用与返回被解耦成两条独立指令靠thread.CurrentFrame()自动切换。argSlotCount是贯穿全章的关键数字既用来在操作数栈上定位this又用来循环传参由方法描述符算出并缓存在Method上。下一篇第 050 篇进入第一块硬骨头方法符号引用解析与参数传递。我们会实现ResolvedMethod()、lookupMethod()、InvokeMethod()并回答一个有意思的问题为什么传参要倒序循环。上一篇【第48篇】类和对象测试——HelloWorld 终于活了下一篇【第50篇】方法符号引用解析与参数传递——方法调用的准备