多态调用查vtable或itable:非接口引用走vtable,接口引用走itable;二者均在类加载链接阶段静态构建,final/private/static方法不入表。

多态调用到底查的是哪个表?vtable 还是 itable
Java 多态的动态分派,不是靠“猜”对象类型,而是靠两张硬编码在类元数据里的跳转表:子类继承链用 vtable(虚方法表),实现接口则额外维护一张 itable(接口方法表)。JVM 在解析 invokevirtual 指令时,先看调用目标是否是接口引用——是,走 itable;否则一律走 vtable。
- 父类引用指向子类实例,比如
Animal a = new Dog(); a.speak();→ 查Dog的vtable,索引位置和Animal.speak()在其 vtable 中的偏移一致 - 接口引用调用,比如
Runnable r = () -> {}; r.run();→ JVM 先在r实际类型(如LambdaForm$T123)的itable中定位Runnable.run对应的实现入口 -
vtable是每个类一份,按继承顺序“摊平”所有可被重写的方法(含从 Object 继承的),空缺位填abstract或父类实现;itable是接口维度组织,每个实现类为每个它实现的接口单独存一张映射
为什么 final 方法、private 方法、static 方法不进 vtable
因为它们压根不参与动态绑定——编译期就锁死了目标字节码符号,JVM 直接生成 invokespecial 或 invokestatic 指令,绕过所有查找表。你哪怕在子类里“重写”了一个 private 方法,那也只是个同名新方法,跟父类的毫无关系。
-
final void close()被调用时,字节码是invokespecial,不查表,也不管运行时对象是谁 -
private String helper()即使子类也定义了同签名方法,父类内部调用它永远只绑定到父类版本 -
static方法本质是“类级别函数”,调用目标由字节码里的符号引用决定,与对象实例无关,自然不进任何实例方法表
虚方法表不是运行时生成的,是类加载时静态构建的
vtable 和 itable 都在类加载的“链接”阶段(具体是验证后的准备阶段)由 JVM 构建完成,不是每次 new 对象时临时算的。这意味着:表结构在类第一次被主动使用前就固定了,后续所有该类实例共享同一份表指针。
- 子类新增重写方法,会覆盖父类
vtable中对应槽位;若新增非重写方法(如新 public 方法),则追加到表尾 - 如果子类没重写某个父类方法,
vtable里那个槽位直接存父类方法的直接引用(可能跨多个继承层级) - 接口默认方法(default)会被加入实现类的
vtable,但接口静态方法(static)不会——它只进接口自己的常量池,不进任何itable
容易被忽略的坑:接口多实现时 itable 的冲突与桥接
当一个类同时实现多个接口,且这些接口定义了相同签名的 default 方法,编译器会强制要求你显式 override 并选择调用哪一个——否则连编译都过不去。但更隐蔽的坑在于:JVM 在构建 itable 时,对每个接口独立填充,不合并;而某些 default 方法在字节码层面会生成桥接方法(bridge method),导致 itable 条目指向的其实是合成的桥接器,而非你写的原始方法。
- 比如
InterfaceA和InterfaceB都有default void log(String s),你实现类里写了@Override public void log(String s) { ... },JVM 会在itable中为两个接口分别填入这个方法的引用,但实际执行时走的是你写的那个实现 - 泛型接口的 default 方法(如
<T> T get())可能触发桥接,itable条目指向的是编译器生成的get()Ljava/lang/Object;,而不是你源码里带泛型的那个 - 用
javap -v看 class 文件,搜索interfaces:和itable:区域,能直接看到每张表的条目和对应方法引用——这是调试多态行为最硬核的手段








