在 Java 中,无论继承链有多长,每个类的 Class 对象中的方法表(vtable)都包含从 Object 开始的所有可继承方法(包括继承的、重写的和新增的)。这是一个扁平化的完整方法表。
// 继承链:Object → Animal → Mammal → Dog
class Animal {
public void eat() {
System.out.println("Animal eating");
}
public void sleep() {
System.out.println("Animal sleeping");
}
public void move() {
System.out.println("Animal moving");
}
}
class Mammal extends Animal {
@Override
public void move() { // 重写
System.out.println("Mammal walking");
}
public void nurse() { // 新方法
System.out.println("Mammal nursing");
}
}
class Dog extends Mammal {
@Override
public void eat() { // 重写
System.out.println("Dog eating");
}
public void bark() { // 新方法
System.out.println("Dog barking");
}
}
Dog dog = new Dog(); dog.sleep(); // 调用从 Animal 继承的方法 // 字节码层面: // 1. 加载对象引用:aload_1 // 2. 虚方法调用:invokevirtual #5 // 方法表索引 6 // // 运行时: // 1. 获取对象的 Class 指针 → Dog.class // 2. 在 Dog.class 的方法表中查找索引 [6] // 3. 找到:sleep() → Animal.sleep // 4. 调用 Animal.sleep 的实现
Animal animal = new Dog(); // 多态 animal.eat(); // 运行时: // 1. animal 引用指向 Dog 对象 // 2. 获取实际类型的 Class:Dog.class // 3. 在 Dog.class 方法表索引 [5] 处查找 eat() // 4. 找到:eat() → Dog.eat(已被重写!) // 5. 调用 Dog.eat 的实现 // // 注意:即使声明类型是 Animal,也会调用 Dog.eat // 因为查找的是实际对象的 Class,而不是引用类型
// 编译时(javac) Animal animal = ...; // 不知道实际类型 animal.eat(); // 编译器生成的字节码: invokevirtual #ref_to_Animal_eat // 解析为索引 [5] // 运行时(JVM) // 不管 animal 实际是 Dog、Cat 还是其他子类 // JVM 都会在实际类型的方法表索引 [5] 处查找 // // Dog.class[5] → Dog.eat() // Cat.class[5] → Cat.eat() // // 这就要求:所有子类在索引 [5] 处必须是 eat() 方法
任何被继承或重写的方法,在整个继承链中的所有子类的方法表中,必须占据相同的索引位置。这是虚方法调用的基础。
接口方法不在主方法表中,而是使用单独的接口方法表(itable)。
interface Flyable {
void fly();
}
interface Swimmable {
void swim();
}
class Duck extends Animal implements Flyable, Swimmable {
public void fly() { ... }
public void swim() { ... }
}
class Fish extends Animal implements Swimmable {
public void swim() { ... }
}
// 问题:swim() 方法在 Duck 和 Fish 的方法表中的索引不同!
// Duck: [..., fly(), swim()]
// Fish: [..., swim()]
//
// 所以接口调用不能用固定索引
// 接口调用流程 Swimmable s = new Duck(); s.swim(); // 运行时: // 1. 获取 Duck.class // 2. 在 itable 中查找 Swimmable 接口 // 3. 在 Swimmable 的映射中查找 swim() // 4. 找到对应的实现方法指针 // 5. 调用 // // 比虚方法调用多一次查找,但仍然很快(通过哈希或缓存优化)
| 类 | 继承深度 | 方法表条目数 | 方法表大小(64位) |
|---|---|---|---|
| Object | 0 | ~12 (本地方法+核心方法) | ~96 字节 |
| Animal | 1 | 12 + 3 = 15 | ~120 字节 |
| Mammal | 2 | 15 + 1 = 16 | ~128 字节 |
| Dog | 3 | 16 + 1 = 17 | ~136 字节 |
// C++ 的做法
class Animal {
virtual void eat();
virtual void sleep();
virtual void move();
};
class Dog : public Animal {
virtual void eat() override; // 重写
virtual void bark(); // 新增
};
// C++ vtable(只包含虚方法)
Animal vtable: [eat, sleep, move] // 3 个条目
Dog vtable: [eat, sleep, move, bark] // 4 个条目
// Java 方法表(包含所有可继承方法)
Animal: [Object的所有方法..., eat, sleep, move] // ~15 个条目
Dog: [Object的所有方法..., eat, sleep, move, bark] // ~16 个条目
// Java 的方法表更大,但查找更简单统一
虽然每个类都有完整的方法表,但 JVM 在实际实现中会做优化:
// 热点代码
for (int i = 0; i < 1000000; i++) {
animal.eat(); // 调用点
}
// JIT 编译器观察到:
// - 这个调用点 99% 的时间 animal 都是 Dog 类型
//
// JIT 生成优化代码:
if (animal.getClass() == Dog.class) {
Dog.eat(animal); // 直接调用,已内联!
} else {
// 慢速路径:通过方法表查找
invokeVirtual(animal, eat_method_index);
}
// 单态调用点:完全消除虚调用开销
// 多态调用点:生成多路分支,仍比查表快
// 源代码
final class Dog extends Animal {
public void eat() { ... }
}
void feed(Dog dog) {
dog.eat(); // 虚方法调用
}
// JIT 分析:
// 1. Dog 是 final 类,不能被继承
// 2. eat() 方法不可能被进一步重写
// 3. 可以安全地去虚化
//
// JIT 生成代码:
void feed(Dog dog) {
Dog.eat(dog); // 直接调用,可内联!
}
import java.lang.reflect.Method;
public class MethodTableDemo {
public static void main(String[] args) {
printMethodTable(Dog.class);
}
static void printMethodTable(Class> clazz) {
System.out.println("=== " + clazz.getName() + " 的方法表 ===");
// 获取所有公共方法(包括继承的)
Method[] methods = clazz.getMethods();
System.out.println("总计方法数:" + methods.length);
for (int i = 0; i < methods.length; i++) {
Method m = methods[i];
System.out.printf("[%2d] %s.%s%n",
i,
m.getDeclaringClass().getSimpleName(),
m.getName()
);
}
// 获取声明的方法(不包括继承的)
Method[] declared = clazz.getDeclaredMethods();
System.out.println("\n本类声明的方法数:" + declared.length);
}
}
/* 输出示例:
=== Dog 的方法表 ===
总计方法数:17
[ 0] Dog.bark
[ 1] Dog.eat
[ 2] Mammal.nurse
[ 3] Animal.sleep
[ 4] Mammal.move
[ 5] Object.wait
[ 6] Object.wait
[ 7] Object.wait
[ 8] Object.equals
[ 9] Object.toString
[10] Object.hashCode
[11] Object.getClass
[12] Object.notify
[13] Object.notifyAll
...
本类声明的方法数:2 (bark, eat)
*/
public class VirtualCallBenchmark {
static class ShallowClass {
public void method() { }
}
static class DeepClass1 extends ShallowClass { }
static class DeepClass2 extends DeepClass1 { }
static class DeepClass3 extends DeepClass2 { }
static class DeepClass4 extends DeepClass3 { }
static class DeepClass5 extends DeepClass4 { }
public static void main(String[] args) {
ShallowClass shallow = new ShallowClass();
ShallowClass deep = new DeepClass5();
// 预热 JIT
for (int i = 0; i < 100000; i++) {
shallow.method();
deep.method();
}
// 测试
long start = System.nanoTime();
for (int i = 0; i < 10000000; i++) {
shallow.method();
}
long time1 = System.nanoTime() - start;
start = System.nanoTime();
for (int i = 0; i < 10000000; i++) {
deep.method();
}
long time2 = System.nanoTime() - start;
System.out.println("浅继承耗时:" + time1 + " ns");
System.out.println("深继承耗时:" + time2 + " ns");
System.out.println("差异:" + Math.abs(time2 - time1) + " ns");
}
}
/* 典型输出(JIT 优化后):
浅继承耗时:2500000 ns
深继承耗时:2500000 ns
差异:0 ns
结论:JIT 优化后,继承深度对性能几乎无影响!
都被内联或去虚化了。
*/
| 特性 | Java | C++ | 说明 |
|---|---|---|---|
| 方法表内容 | 所有可继承方法 | 仅虚方法 | Java 更完整 |
| 继承的方法 | 复制到子类表中 | 不复制 | Java 空间换时间 |
| 查找复杂度 | O(1) 直接索引 | O(1) 直接索引 | 都是常数时间 |
| 接口方法 | 单独 itable | 无(抽象类实现) | Java 支持多接口 |
| 内存开销 | 较大(完整表) | 较小(仅虚方法) | 每类一份,可接受 |
| 运行时优化 | JIT 去虚化 | 编译器优化有限 | Java 运行时更智能 |