当前位置: 首页 > news >正文

Java 字节码进阶3:面向对象多态在字节码层面的原理?

大家好,我是此林。

Java 字节码进阶1:深入解析 class 文件结构-CSDN博客

Java 字节码进阶2:为什么 switch 比 if-else 高效(阿里代码规约)?-CSDN博客

前两章,我们深入分析了 class 字节码文件的十大组成,

介绍了阿里代码规约中 switch-case 比 if-else 高效的字节码原理。

今天,我们来深入分析 面向对象三大特性(封装、继承、多态)之一:多态 在字节码层面的原理。

1. new 创建一个对象的时候发生了什么?

在 Java 中,我们通常使用 new 关键字去创建一个对象,例如:

public class Hello {public void test() {Hello h = new Hello();}
}

我们使用 javac Hello.java 编译成 Hello.class

然后使用 javap -v Hello 去查看字节码。

这里,贴出了 test 函数的字节码。

我们发现,一个对象的创建需要三条指令:newdupinvokespecial 调用 <init> 方法

问:什么是 <init> 方法?

这个 <init> 方法,实际上就是 JVM 中类的实例初始化方法,类的构造方法、非静态变量的初始化、对象初始化代码块都会被编译进这个方法中。比如:

public class Hello {// 初始化非静态变量private int a = 10;// 构造器方法public Hello() {}// 对象初始化代码块{int b = 9;}
}

在上面 Hello 这个类中,初始化变量、构造方法、非静态代码块在最终编译后都会统一编译进 <init> 方法。

再看这个图:

创建一个对象时,先调用 new 指令,只是创建了一个类实例的引用,并将这个引用压入操作数栈顶。

接着,使用 invokespecial 调用 <init> 方法才真正调用了构造方法。

问:那中间指令 dup 的作用是什么?

答:dup 其实就是 duplicate 的缩写,意思是复制。

invokespecial 指令调用 <init> 后,会消耗 操作数栈顶 的类实例引用。

那如果想要在 invokespecial 调用后栈顶还有指向新建类实例的引用,就需要 事先在 invokespecial 之前复制一份类对象实例的引用;

否则,调用完 <init> 之后就再也找不到刚刚创建的对象的引用了。

刚刚我们介绍了用 new 关键字去创建一个对象,底层字节码的流程。

我们提到了 <init> 方法是类实例的初始化方法,与之对应的还有个 <clinit> 方法

<clinit> 方法是类的静态初始化方法,类的静态代码块、静态变量初始化都会被编译进这个方法中,比如:

public class Hello {// 初始化静态变量private static int a = 10;// 静态代码块static {int b = 9;}
}

需要注意的是,<clinit> 的调用在四个字节码指令会触发:

        new

        getstatic

        putstatic

        invokestatic

比如下面的场景:

        创建类对象的实例

        访问类的静态方法或静态字段

        初始化某个类的子类

2. 方法调用字节码指令

JVM 的方法调用指令都以 invoke 开头,比如:

invokestatic:调用静态方法

invokevirtual:调用实例对象的非私有方法(支持多态的关键

invokeinterface:调用接口方法(支持多态的关键

invokespecial:调用实例对象的私有方法、构造器方法、super关键字调用父类的实例方法

invokedynamic:调用动态方法

2.1. invokestatic 指令

invokestatic 用于调用静态方法,也就是 static 修饰的方法。

它要调用的方法在 javac 编译器已经确定,运行期不会改变,属于静态绑定。所以调用 invokestatic 不需要将对象加载到操作数栈,只需要方法参数入栈即可。

这个我们在 Java 字节码进阶2:为什么 switch 比 if-else 高效(阿里代码规约)?-CSDN博客 已经说过:

非静态方法,比如:

public void test(int name) {// ...
}

它方法参数里看起来只有一个参数 name,实际上还会传入一个隐藏的 this 参数(当前实例)。

2.2. invokevirtual 指令 (多态在字节码层面的原理)

invokevirtual 指令用于调用普通实例方法,它调用的目标方法要在运行时才能更具对象实际的类型确定,在编译期无法知道,类似 C++ 中的虚方法。

在调用 invokevirtual 之前,会把 对象引用(this)、方法参数压入操作数栈

调用结束 对象引用(this)、方法参数出栈,

如果方法有返回值,返回值会进入操作数栈顶。

我们来看一个案例:

class Animal {public void speak() {System.out.println("Animal speaks");}
}class Dog extends Animal {@Overridepublic void speak() {System.out.println("Dog barks");}
}public class Main {public static void main(String[] args) {Animal a = new Dog();  // 向上转型a.speak();             // 多态调用}
}

我们使用 javac Main.java 编译后,使用 javap -c Main 反编译字节码。

可以看到,编译器只知道变量 a 的静态类型是 Animal,所以字节码里写的是调用 Animal.speak()

运行时 JVM 会查 a 的真实类型(这里是 Dog),通过 虚方法表 vtable 跳转到 Dog.speak()

结论:

  • 源代码层面:我们写的是 a.speak()

  • 字节码层面:表现为 invokevirtual 指令。

  • 运行时层面:依靠虚方法表完成 动态分派,最终执行 Dog.speak()

2.3. invokeinterface 指令 (多态在字节码层面的原理)

vtable 虚方法表用于支持类的继承的多态,而 itable 用于支持多接口的实现。

问:vtable 和 itable 的区别?

答:

  • vtable(虚方法表)
    用于 类的继承体系,存储类及其父类的虚方法实现。invokevirtual 就是通过 vtable 做动态分派。

  • itable(接口方法表)
    用于 接口方法的实现查找,当字节码里遇到 invokeinterface 指令时,会通过 itable 来找到具体的实现。

问: 为什么需要 itable?

:因为一个类可能实现 多个接口,而接口的方法签名可能相同,但来源不同。
如果只用 vtable,无法区分同一个方法属于哪个接口。
因此 JVM 在类加载时,会为每个接口建立一个 itable entry,里面保存该接口的方法到类中具体实现方法的映射。

案例:

interface Speakable {void speak();
}class Dog implements Speakable {@Overridepublic void speak() {System.out.println("Dog barks");}
}public class Main {public static void main(String[] args) {Speakable s = new Dog();s.speak();  // 接口多态调用}
}

javap -c Main 结果:

  public static void main(java.lang.String[]);Code:0: new           #2                  // class Dog3: dup4: invokespecial #3                  // Method Dog."<init>":()V7: astore_18: aload_19: invokeinterface #4,  1            // InterfaceMethod Speakable.speak:()V14: return

注意这里不是 invokevirtual,而是 invokeinterface

Dog 加载后,JVM 内部大致结构:

Dog.klass├── vtable│    [0] -> Dog.speak()   (覆盖 Object 的 toString() 等也会在这)│├── itable│    [Speakable 接口]│         method[0] -> Dog.speak()

当执行:

Speakable s = new Dog();
s.speak();
  • 字节码是:invokeinterface #4, 1

  • JVM 执程:

    1. 找到 s 的实际类 Dog

    2. DogitableSpeakable 这个接口的 entry

    3. 在 entry 里查 speak() 的实现

    4. 跳转执行 Dog.speak()

所以总的来说,itable 会比 vtable 多一层,主要是因为在 Java 里,只支持单继承,但支持实现多个接口。

当然,感兴趣的同学还可以使用 HotSopt Debugger 去查看更深入的内容。

2.4. invokedynamic 指令

invokedynamic 出现的背景:

JDK 7 (JSR 292) 中引入,它的设计目标是:

  • 给 JVM 加入一个 动态调用机制,让 动态语言(Groovy、JRuby、Jython 等)能更高效运行在 JVM 上。

那什么是动态语言呢?它主要有两个特点:

  • 弱类型 / duck typing:方法调用时不依赖静态类型检查,而是运行时决定。

  • 方法、属性可以在运行时动态改变。

传统的 invokevirtual / invokeinterface 在编译期就要决定目标方法签名,而 invokedynamic 则允许 方法绑定延迟到运行时(动态绑定),由引导方法 bootstrap method 决定

所以, invokedynamic 指令初衷是为了支持动态语言(弱类型为主)而设计的

Java 本身是强类型语言,但从 Java 8 开始,invokedynamic 被大量用在 语法糖的底层实现,而不是仅仅是 “弱类型”,比如:

Lambda 表达式。通过 invokedynamic 调用 LambdaMetafactory.metafactory 生成 Function 对象。

今天的分享就到这里了,相信你一定有所收获!

我是此林,关注我吧,带你看不一样的世界!

http://www.dtcms.com/a/388804.html

相关文章:

  • Tensor :核心概念、常用函数与避坑指南
  • 机器学习实战·第四章 训练模型(1)
  • 一次因表单默认提交导致的白屏排查记录
  • Linux:io_uring
  • 《第九课——C语言判断:从Java的“文明裁决“到C的“原始决斗“——if/else的生死擂台与switch的轮盘赌局》
  • 学习日报|Spring 全局异常与自定义异常拦截器执行顺序问题及解决
  • Spring Boot 参数处理
  • Debian系统基本介绍:新手入门指南
  • Spring Security 框架
  • Qt QPercentBarSeries详解
  • RTT操作系统(3)
  • DNS服务管理
  • IDA Pro配置与笔记
  • 虚函数表在单继承与多继承中的实现机制
  • 矿石生成(1)
  • Linux 线程的概念
  • Unity学习之资源管理(Resources、AssetDatabase、AssetBundle、Addressable)
  • LG P5138 fibonacci Solution
  • 删除UCPD监控服务或者监控驱动
  • 日语学习-日语知识点小记-构建基础-JLPT-N3阶段(33):文法運用第10回1+(考え方14)
  • 向量技术研究报告:从数学基础到AI革命的支柱
  • 802.1x和802.1Q之间关联和作用
  • 基于大模型多模态的人体体型评估:从“尺码测量”到“视觉-感受”范式
  • 更符合人类偏好的具身导航!HALO:面向机器人导航的人类偏好对齐离线奖励学习
  • Transformer多头注意力机制
  • git 分支 error: src refspec sit does not match any`
  • VN1640 CH5 I/O通道终极指南:【VN1630 I/O功能在电源电压时间精确度测试中的深度应用】
  • qt QHorizontalBarSeries详解
  • 半导体制造的芯片可靠性测试的全类别
  • MySQL 索引详解:原理、类型与优化实践