从 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 面向对象的设计,

并不是为了炫技。

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


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

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

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐