代码编译的结果从本地机器码转变为字节码,是存储格式发展的一小步,却是编程语言发展的一大步
首先,抛出灵魂三问:
如果你对上述问题理解得还不是特别透彻的话,可以看下这篇文章;如果理解了,你可以关闭网页,打开游戏放松了hhh
下面,笔者将带你探究 JVM
核心的组成部分之一——执行引擎。
Q1:虚拟机与物理机的异同
- 物理机的执行引擎是直接建立在处理器、硬件、指令集和操作系统层面上的
- 虚拟机的执行引擎是由自定义的,可自行制定指令集与执行引擎的结构体系,且能够执行不被硬件直接支持的指令集格式
Q2:有关 JVM
字节码执行引擎的概念模型
JVM
的执行引擎都是一致的。输入的是字节码文件,处理的是字节码解析的等效过程,输出的是执行结果Java
代码的选择
- 解释执行:通过解释器执行
- 编译执行:通过即时编译器产生本地代码执行
- 两者兼备,甚至还会包含几个不同级别的编译器执行引擎
2.2.1 基本概念#
笔者之前在 一文洞悉 JVM 内存管理机制 中就谈到过虚拟机栈,相信看过的读者都有印象
2.2.2 局部变量表#
Java
程序编译为 Class
文件时,会在方法的 Code
属性的 max_locals
数据项中确定了该方法所需要分配的局部变量表的最大容量
- 大小:虚拟机规范中没有明确指明一个变量槽占用的内存空间大小,允许变量槽长度随着处理器、操作系统或虚拟机的不同而发生变化
- 对于
32
位以内的数据类型(boolean
、byte
、char
、short
、int
、float
、reference
、returnAddress
),虚拟机会为其分配一个变量槽空间- 对于
64
位的数据类型(long
、double
),虚拟机会以高位对齐的方式为其分配两个连续的变量槽空间- 特点:可重用。为了尽可能节省栈帧空间,若当前字节码
PC
计数器的值已超出了某个变量的作用域,则该变量对应的变量槽可交给其他变量使用
2.2.3 操作数栈#
操作数栈是一个后入先出栈
作用:在方法执行过程中,写入(进栈)和提取(出栈)各种字节码指令
分配时期:同上,在编译时会在方法的 Code
属性的 max_stacks
数据项中确定操作数栈的最大深度
栈容量:操作数栈的每一个元素可以是任意的 Java
数据类型 ——32
位数据类型所占的栈容量为 1
,64
位数据类型所占的栈容量为 2
注意:操作数栈中元素的数据类型必须与字节码指令的序列严格匹配,在编译时编译器需要验证一次、在类校验阶段的数据流分析中还要再次验证
2.2.4 动态连接#
Class
文件的常量池中存有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用作为参数,这些符号引用:
- 一部分会在类加载阶段或者第一次使用的时候就转化为直接引用(静态解析)
- 另一部分会在每一次运行期间转化为直接引用(动态连接)
2.2.5 方法返回地址#
- 正常退出:执行中遇到任意一个方法返回的字节码指令
- 异常退出:执行中遇到异常、且在本方法的异常表中没有搜索到匹配的异常处理器区处理
- 正常退出时,调用者的
PC
计数器的值可以作为返回地址- 异常退出时,通过异常处理器表来确定返回地址
- 恢复上层方法的局部变量表和操作数栈
- 若有返回值把它压入调用者栈帧的操作数栈中
- 调整
PC
计数器的值以指向方法调用指令后面的一条指令等
在实际开发中,一般会把动态连接、方法返回地址与其他附加信息全部一起称为栈帧信息
下面笔者将为大家详细讲解方法调用的类型
2.3.1 解析调用#
笔者之前在 一夜搞懂 | JVM 类加载机制中就谈到过解析,感觉有点混淆的,可以回去看下
private
修饰的私有方法,类静态方法,类实例构造器,父类方法2.3.2 分派调用#
Q1:什么是静态类型?什么是实际类型?
A1:这个用代码来说比较简便, Talk is cheap ! Show me the code !
//父类 public class Human { }
//子类 public class Man extends Human { }
public class Main { public static void main(String[] args) { //这里的 Human 是静态类型,Man 是实际类型 Human man=new Man(); } }
1.静态分派#
- 依赖静态类型来定位方法的执行版本
- 典型应用是方法重载
- 发生在编译阶段,不由
JVM
来执行单纯说未免有些许抽象,所以特地用下面的
DEMO
来帮助了解
public class Father { } public class Son extends Father { } public class Daughter extends Father { }
public class Hello { public void sayHello(Father father){ System.out.println("hello , i am the father"); } public void sayHello(Daughter daughter){ System.out.println("hello i am the daughter"); } public void sayHello(Son son){ System.out.println("hello i am the son"); } }
public static void main(String[] args){ Father son = new Son(); Father daughter = new Daughter(); Hello hello = new Hello(); hello.sayHello(son); hello.sayHello(daughter); }
输出结果如下:
hello , i am the father
hello , i am the father
我们的编译器在生成字节码指令的时候会根据变量的静态类型选择调用合适的方法。就我们上述的例子而言:
2.动态分派#
依赖动态类型来定位方法的执行版本
典型应用是方法重写
发生在运行阶段,由
JVM
来执行单纯说未免有些许抽象,所以特地用下面的
DEMO
来帮助了解
public class Father { public void sayHello(){ System.out.println("hello world ---- father"); } } //继承 + 方法重写 public class Son extends Father { @Override public void sayHello(){ System.out.println("hello world ---- son"); } }
public static void main(String[] args){ Father son = new Son(); son.sayHello(); }
输出结果如下:
hello world ---- son
我们接着来看一下字节码指令调用情况
疑惑来了,我们可以看到,
JVM
选择调用的是静态类型的对应方法,但是为什么最终的结果却调用了是实际类型的对应方法呢?
当我们将要调用某个类型实例的具体方法时,会首先将当前实例压入操作数栈,然后我们的 invokevirtual
指令需要完成以下几个步骤才能实现对一个方法的调用:
因此,疑惑自然解决了
3.单分派#
4.多分派#
想了解 静态多分派,动态单分派 的可以看下这篇文章:Java 中的静态单多分派与动态单分派
恭喜你!已经看完了前面的文章,相信你对
JVM
字节码执行引擎已经有一定深度的了解!你可以稍微放松奖励自己一下,可以睡一个美美的觉,明天起来继续冲冲冲!!!
如果文章对您有一点帮助的话,希望您能点一下赞,您的点赞,是我前进的动力
本文参考链接:
作者:许朋友爱玩
出处:https://www.cnblogs.com/xcynice/p/jvm_zi_jie_ma_zhi_xing_ying_qing.html
版权:本文采用「署名 4.0 国际」知识共享许可协议进行许可。
PS:本人公众号,不定期更新技术文章,各位老铁可以关注一下