Java 底层思考系列:从 JVM 栈帧开始,我真正理解了Java 为什么设计抽象类

📅 2026/7/8 10:04:50 👁️ 阅读次数 📝 编程学习
Java 底层思考系列:从 JVM 栈帧开始,我真正理解了Java 为什么设计抽象类

从 JVM 栈帧开始,我真正理解了Java 为什么设计抽象类

关键词:JVM、Stack Frame、动态链接、抽象类、多态、方法调用、Java底层原理

最近背 Java 八股时,看到了 JVM 的栈帧(Stack Frame)

起初觉得这只是 JVM 的一个知识点:

  • 局部变量表
  • 操作数栈
  • 动态链接
  • 返回地址

后来越想越觉得:

这些东西竟然和 Java 的面向对象设计全部串起来了。

尤其是:

为什么 Java 要设计抽象类?

以前我的回答一直都是:

“为了规范子类。”

今天终于理解:

真正的原因,要从 JVM 如何调用一个方法开始。


一、一个方法调用,到底发生了什么?

例如:

publicstaticvoidmain(String[]args){Animalanimal=newDog();animal.eat();}

以前:

我脑子里的理解只有:

调用 eat() ↓ 输出结果

现在:

真正发生的是:

Java代码 ↓ 编译成 .class ↓ JVM加载类 ↓ 调用方法 ↓ 创建栈帧(Stack Frame) ↓ 执行字节码 ↓ 方法结束 ↓ 栈帧销毁

也就是说:

每调用一个方法,JVM 都会创建一个新的栈帧。

方法结束以后:

栈帧立即弹出。


二、栈帧里面到底有什么?

一个栈帧主要包含四部分。

Stack Frame ├── 局部变量表(Local Variable Table) ├── 操作数栈(Operand Stack) ├── 动态链接(Dynamic Linking) └── 返回地址(Return Address)

① 局部变量表

负责保存局部变量。

例如:

publicstaticintadd(inta,intb){intc=a+b;returnc;}

那么:

局部变量表 a b c

全部保存在这里。


② 操作数栈

负责计算。

例如:

intc=a+b;

JVM 实际会执行:

push a push b ↓ + ↓ 结果压回栈

所以:

所有:

+ - * / %

都依赖操作数栈。


③ 动态链接(Dynamic Linking)

这里看着唬人,其实就一句话秒了:

帮助 JVM 找到真正要执行的方法。

什么意思?

例如:

Animalanimal=newDog();animal.eat();

变量类型:

Animal

对象类型:

Dog

JVM 怎么知道:

应该执行:

Animal.eat()

还是:

Dog.eat()

答案就是:

动态链接。

运行期间:

JVM 根据对象真正的类型:

找到:

Dog.eat()

然后执行。

这一瞬间,

我突然意识到:

这就是多态工作的底层基础。


④ 返回地址

方法执行结束以后,

CPU 怎么知道回哪里?

例如:

main(){add();System.out.println("结束");}

add()

执行完成以后:

并不是程序结束。

而是:

继续执行:

System.out.println("结束");

所以:

栈帧里面还保存着:

返回地址。


三、动态链接,让我重新理解了多态

以前:

觉得多态只是:

Animalanimal=newDog();

今天终于知道:

真正发生的是:

animal.eat() ↓ JVM ↓ 动态链接 ↓ Dog.eat()

这也是:

为什么:

Java 可以:

父类引用 ↓ 子类对象 ↓ 调用子类方法

四、然后我突然想到一个问题

既然:

JVM 会找到真正的方法。

那:

为什么 Java 还需要抽象类?

例如:

classShape{publicdoublearea(){return0;}}

这个:

真的合理吗?

圆形:

怎么算面积?

矩形:

怎么算面积?

三角形:

怎么算面积?

父类其实根本不知道。

如果:

写一个:

return0;

只是:

为了让代码能够编译。

那这个方法:

其实毫无意义。


五、这就是抽象类存在的意义

于是:

Java 提供了:

abstractclassShape{publicabstractdoublearea();}

这里:

父类直接告诉所有子类:

我不知道怎么实现。

但是:

你必须实现。

于是:

classCircleextendsShape{@Overridepublicdoublearea(){returnMath.PI*r*r;}}

如果:

子类没有实现:

编译直接失败。

这就是:

抽象类真正解决的问题。

它不是:

帮助 JVM。

而是:

帮助程序员。


六、我曾经有一个错误理解

刚开始学习的时候,

我甚至认为:

抽象类是不是为了节省 JVM 内存?

后来查阅 JVM 规范以后,

发现这是错误的。

原因很简单。

抽象方法:

依然存在。

例如:

abstractvoideat();

JVM 依然知道:

方法名 参数 返回值 访问权限

只是:

没有方法体(Code Attribute)。

所以:

抽象类几乎不是为了节省内存。

真正的价值是:

表达设计意图。

它告诉所有开发者:

这个行为一定存在。 但是: 父类不知道如何实现。 必须交给子类。

这是:

面向对象设计。

而不是:

JVM 内存优化。


七、整个知识终于串起来了

以前:

这些知识:

在我脑子里都是独立的。

JVM 抽象类 继承 多态 Override

今天终于变成了一张图。

Java代码 ↓ 编译成字节码 ↓ JVM加载类 ↓ 方法调用 ↓ 创建栈帧 ↓ 动态链接 ↓ 找到真正的方法 ↓ 为什么需要真正的方法? ↓ 因为存在 Override ↓ 为什么允许 Override? ↓ 因为存在继承 ↓ 为什么父类没有实现? ↓ 因为父类无法提供通用实现 ↓ 于是: 抽象类诞生

这一刻:

JVM 和 Java 面向对象设计,

终于连接到了一起。


八、总结

以前学习 Java:

总是在背:

  • 什么是继承
  • 什么是多态
  • 什么是抽象类

今天第一次站在 JVM 的角度去理解:

发现这些设计,

并不是孤立存在的。

动态链接让 JVM 能够在运行时找到真正的方法。

方法重写让不同子类拥有不同实现。

抽象类则告诉编译器:

父类不知道怎么做,

所以必须由子类完成。

原来,

Java 面向对象的设计,

并不是为了炫技。

而是为了让程序的结构更加合理,让错误尽可能提前暴露,让扩展变得更加自然。


真正的学习,不是记住一个知识点,而是把一个知识点和整个知识体系连接起来。

如果一门语言的每一个设计都能回答 “为什么”,那你离真正理解它就更近了一步。