动态分派的核心是invokevirtual指令,它在运行时根据对象的实际类型查找虚方法表(vtable)确定方法版本,实现重写的多态;Java为动态单分派,仅依据接收者实际类型决策,不考虑参数类型。

Java 中 invokevirtual 指令的底层核心是动态分派,它让 JVM 在运行时根据对象的**实际类型**(而非声明类型)决定调用哪个方法版本。这正是重写(override)能实现多态的关键。
invokevirtual 为什么必须查虚方法表?
编译期只能知道变量的静态类型(比如 Animal a = new Dog(); 中的 Animal),但无法确定最终执行的是 Animal.speak() 还是 Dog.speak()。JVM 必须等到运行时,拿到 a 所指向对象的真实类(Dog.class),再从中查找该方法的具体实现。
每个类在加载时,JVM 都会为它构建一张**虚方法表(vtable)**,表中按固定顺序存放所有可被重写的实例方法(public/protected 非 static、非 final、非 private)的入口地址。子类的 vtable 会继承父类的结构,并把被重写的方法项替换成自己的地址。
- 调用
a.speak()时,JVM 先通过对象头定位到其实际类(Dog)的 vtable - 再根据方法在常量池中的符号引用,在 vtable 中找到对应索引位置
- 最后跳转到该索引指向的实际字节码地址执行
动态分派只看一个宗量:接收者实际类型
Java 是**动态单分派**语言。所谓“单分派”,是指 invokevirtual 的分派决策只依赖一个因素——方法的**接收者(即 this)的实际类型**。
它不关心参数类型(那是静态分派的事,比如重载)。哪怕你写了多个同名但参数不同的 speak(String msg) 和 speak(int volume),编译器早已在编译期就根据参数的静态类型选好了具体调用哪个签名;而 invokevirtual 要解决的,只是“这个 speak() 到底是 Dog 版本还是 Cat 版本”的问题。
和其它调用指令的本质区别
invokevirtual 和 invokestatic、invokespecial 最根本的不同在于:它的目标方法不能在类加载解析阶段就绑定为直接引用,必须延迟到运行时,靠对象实例的类型信息才能完成定位。
-
invokestatic和invokespecial调用的是非虚方法,解析阶段就能确定唯一版本 -
invokeinterface也属动态分派,但它需要额外做一次“查找实现类”的过程,性能略低于invokevirtual -
invokedynamic完全由用户定义引导逻辑,与 JVM 内置分派机制无关
final 方法为何仍走 invokevirtual?
虽然 final 方法不能被重写,理论上可以像静态方法一样提前绑定,但 Java 字节码规范仍要求用 invokevirtual 指令调用它。这是因为:
- JVM 不在字节码层面做“是否 final”的语义检查,统一按虚方法流程处理更简洁
- Just-In-Time 编译器(如 HotSpot)会在运行时识别出 final 方法,并进行去虚拟化(devirtualization)优化,直接内联或硬编码调用,避免查表开销
- 这样既保持字节码兼容性,又不妨碍高性能执行
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











