Java 底层思考系列:从 JVM 栈帧开始,我真正理解了Java 为什么设计抽象类
从 JVM 栈帧开始,我真正理解了Java 为什么设计抽象类
关键词: JVM、Stack Frame、动态链接、抽象类、多态、方法调用、Java底层原理
最近背 Java 八股时,看到了 JVM 的 栈帧(Stack Frame)。
起初觉得这只是 JVM 的一个知识点:
- 局部变量表
- 操作数栈
- 动态链接
- 返回地址
后来越想越觉得:
这些东西竟然和 Java 的面向对象设计全部串起来了。
尤其是:
为什么 Java 要设计抽象类?
以前我的回答一直都是:
“为了规范子类。”
今天终于理解:
真正的原因,要从 JVM 如何调用一个方法开始。
一、一个方法调用,到底发生了什么?
例如:
public static void main(String[] args) {
Animal animal = new Dog();
animal.eat();
}
以前:
我脑子里的理解只有:
调用 eat()
↓
输出结果
现在:
真正发生的是:
Java代码
↓
编译成 .class
↓
JVM加载类
↓
调用方法
↓
创建栈帧(Stack Frame)
↓
执行字节码
↓
方法结束
↓
栈帧销毁
也就是说:
每调用一个方法,JVM 都会创建一个新的栈帧。
方法结束以后:
栈帧立即弹出。
二、栈帧里面到底有什么?
一个栈帧主要包含四部分。
Stack Frame
├── 局部变量表(Local Variable Table)
├── 操作数栈(Operand Stack)
├── 动态链接(Dynamic Linking)
└── 返回地址(Return Address)
① 局部变量表
负责保存局部变量。
例如:
public static int add(int a,int b){
int c = a+b;
return c;
}
那么:
局部变量表
a
b
c
全部保存在这里。
② 操作数栈
负责计算。
例如:
int c = a+b;
JVM 实际会执行:
push a
push b
↓
+
↓
结果压回栈
所以:
所有:
+
-
*
/
%
都依赖操作数栈。
③ 动态链接(Dynamic Linking)
这里看着唬人,其实就一句话秒了:
帮助 JVM 找到真正要执行的方法。
什么意思?
例如:
Animal animal = new Dog();
animal.eat();
变量类型:
Animal
对象类型:
Dog
JVM 怎么知道:
应该执行:
Animal.eat()
还是:
Dog.eat()
答案就是:
动态链接。
运行期间:
JVM 根据对象真正的类型:
找到:
Dog.eat()
然后执行。
这一瞬间,
我突然意识到:
这就是多态工作的底层基础。
④ 返回地址
方法执行结束以后,
CPU 怎么知道回哪里?
例如:
main(){
add();
System.out.println("结束");
}
add()
执行完成以后:
并不是程序结束。
而是:
继续执行:
System.out.println("结束");
所以:
栈帧里面还保存着:
返回地址。
三、动态链接,让我重新理解了多态
以前:
觉得多态只是:
Animal animal = new Dog();
今天终于知道:
真正发生的是:
animal.eat()
↓
JVM
↓
动态链接
↓
Dog.eat()
这也是:
为什么:
Java 可以:
父类引用
↓
子类对象
↓
调用子类方法
四、然后我突然想到一个问题
既然:
JVM 会找到真正的方法。
那:
为什么 Java 还需要抽象类?
例如:
class Shape{
public double area(){
return 0;
}
}
这个:
真的合理吗?
圆形:
怎么算面积?
矩形:
怎么算面积?
三角形:
怎么算面积?
父类其实根本不知道。
如果:
写一个:
return 0;
只是:
为了让代码能够编译。
那这个方法:
其实毫无意义。
五、这就是抽象类存在的意义
于是:
Java 提供了:
abstract class Shape{
public abstract double area();
}
这里:
父类直接告诉所有子类:
我不知道怎么实现。
但是:
你必须实现。
于是:
class Circle extends Shape{
@Override
public double area(){
return Math.PI*r*r;
}
}
如果:
子类没有实现:
编译直接失败。
这就是:
抽象类真正解决的问题。
它不是:
帮助 JVM。
而是:
帮助程序员。
六、我曾经有一个错误理解
刚开始学习的时候,
我甚至认为:
抽象类是不是为了节省 JVM 内存?
后来查阅 JVM 规范以后,
发现这是错误的。
原因很简单。
抽象方法:
依然存在。
例如:
abstract void eat();
JVM 依然知道:
方法名
参数
返回值
访问权限
只是:
没有方法体(Code Attribute)。
所以:
抽象类几乎不是为了节省内存。
真正的价值是:
表达设计意图。
它告诉所有开发者:
这个行为一定存在。
但是:
父类不知道如何实现。
必须交给子类。
这是:
面向对象设计。
而不是:
JVM 内存优化。
七、整个知识终于串起来了
以前:
这些知识:
在我脑子里都是独立的。
JVM
抽象类
继承
多态
Override
今天终于变成了一张图。
Java代码
↓
编译成字节码
↓
JVM加载类
↓
方法调用
↓
创建栈帧
↓
动态链接
↓
找到真正的方法
↓
为什么需要真正的方法?
↓
因为存在 Override
↓
为什么允许 Override?
↓
因为存在继承
↓
为什么父类没有实现?
↓
因为父类无法提供通用实现
↓
于是:
抽象类诞生
这一刻:
JVM 和 Java 面向对象设计,
终于连接到了一起。
八、总结
以前学习 Java:
总是在背:
- 什么是继承
- 什么是多态
- 什么是抽象类
今天第一次站在 JVM 的角度去理解:
发现这些设计,
并不是孤立存在的。
动态链接让 JVM 能够在运行时找到真正的方法。
方法重写让不同子类拥有不同实现。
抽象类则告诉编译器:
父类不知道怎么做,
所以必须由子类完成。
原来,
Java 面向对象的设计,
并不是为了炫技。
而是为了让程序的结构更加合理,让错误尽可能提前暴露,让扩展变得更加自然。
真正的学习,不是记住一个知识点,而是把一个知识点和整个知识体系连接起来。
如果一门语言的每一个设计都能回答 “为什么”,那你离真正理解它就更近了一步。
更多推荐



所有评论(0)