方法派发机制的核心在于编译期能否确定调用目标:静态分派在编译时绑定,依赖声明类型和签名,无vtable参与;动态分派则依赖运行时对象实际类型,通过类独有的虚方法表(vtable)实现——表中按固定顺序存储可重写方法地址,子类vtable继承父类布局并替换重写方法的入口,对象通过隐式指针(如vptr或isa)指向所属类vtable,调用时查表跳转,支撑多态。

方法派发机制的核心,在于编译期能否确定调用目标。静态分派靠类型和签名在编译时绑定,动态分派则依赖运行时对象的实际类型,而虚方法表(vtable)正是支撑后者高效执行的关键结构。
静态分派:编译期就“定死”的调用
静态分派对应的是方法重载(Overload),它不涉及继承或子类,只看声明类型和参数签名。编译器根据变量声明的类型、传入参数的个数与类型,直接选出唯一匹配的方法,生成固定地址调用(如 invokestatic 或 invokespecial)。
- 比如
print(int)和print(String)在同一个类中,调用哪个完全由实参决定,无需查表 - 私有方法、
final方法、静态方法、构造器都走静态分派——它们无法被重写,也就无需运行时决策 - 没有 vtable 参与:这些方法不进虚方法表,也不受多态影响
动态分派:靠 vtable 实现“一个接口,多种实现”
动态分派对应方法重写(Override),发生在具有继承关系的类之间。当父类引用指向子类实例,调用被 virtual(Java/C#)、override(C++/Swift)修饰的方法时,JVM 或运行时需在运行时决定执行哪个版本——这就靠虚方法表。
- vtable 是每个类独有的数组,按固定顺序存放该类所有可被动态调用的实例方法入口地址
- 子类 vtable 继承父类布局:相同签名的方法占据相同索引位;若子类重写了某方法,就替换对应索引处的地址
- 对象内存中隐含一个指针(如 C++ 的
vptr,Swift 的isa),指向所属类的 vtable,调用时先取指针、再按索引查表、最后跳转执行
vtable 是怎么构建出来的?
vtable 不是手动写的,而是在类加载的连接阶段由虚拟机或编译器自动生成。它的内容取决于类定义本身和继承关系:
- 只要类中至少有一个虚方法(即可能被重写的方法),JVM 就为其生成 vtable
- 父类方法若未被重写,子类 vtable 中该槽位仍指向父类方法实现(避免冗余拷贝)
- 接口方法另有一套
itable,原理类似但结构更复杂,用于invokeinterface指令 - 像
Object.toString()这种通用方法,只要子类没重写,其 vtable 中就直接引用Object类中的实现地址
为什么需要区分这两种派发?
性能与语义不可兼得,设计上必须取舍:
- 静态分派快——无查表开销,适合确定性高的场景(如工具方法、构造逻辑)
- 动态分派支持多态——是面向对象扩展性的基础,代价是每次调用多一次指针解引用和数组索引操作
- 现代编译器(如 Swift SIL、JVM JIT)会在安全前提下做优化:比如某个虚方法实际从未被重写,就可能内联为静态调用

















