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对象类型:
DogJVM 怎么知道:
应该执行:
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 面向对象的设计,
并不是为了炫技。
而是为了让程序的结构更加合理,让错误尽可能提前暴露,让扩展变得更加自然。
真正的学习,不是记住一个知识点,而是把一个知识点和整个知识体系连接起来。
如果一门语言的每一个设计都能回答 “为什么”,那你离真正理解它就更近了一步。