一夜搞懂 | JVM 字节码执行引擎

前言

本文已经收录到我的 Github 个人博客,欢迎大佬们光临寒舍:

我的 GIthub 博客

学习导图

学习导图

一.为什么要学习字节码执行引擎?

代码编译的结果从本地机器码转变为字节码,是存储格式发展的一小步,却是编程语言发展的一大步

首先,抛出灵魂三问:

  • 虚拟机在执行代码的时候,如何找到正确的方法呢?
  • 如何执行方法内的字节码呢?
  • 执行代码时涉及的内存结构有哪些呢?

如果你对上述问题理解得还不是特别透彻的话,可以看下这篇文章;如果理解了,你可以关闭网页,打开游戏放松了hhh

下面,笔者将带你探究 JVM 核心的组成部分之一——执行引擎。

二.核心知识点归纳

2.1 概述

Q1:虚拟机与物理机的异同

  • 相同点:都有代码执行能力
  • 不同点:
  • 物理机的执行引擎是直接建立在处理器、硬件、指令集和操作系统层面上的
  • 虚拟机的执行引擎是由自定义的,可自行制定指令集与执行引擎的结构体系,且能够执行不被硬件直接支持的指令集格式

Q2:有关 JVM 字节码执行引擎的概念模型

  • 外观上:所有 JVM 的执行引擎都是一致的。输入的是字节码文件,处理的是字节码解析的等效过程,输出的是执行结果
执行引擎的外观
  • 从实现上,执行引擎有多种执行 Java 代码的选择
  • 解释执行:通过解释器执行
  • 编译执行:通过即时编译器产生本地代码执行
  • 两者兼备,甚至还会包含几个不同级别的编译器执行引擎

2.2 运行时栈帧结构

2.2.1 基本概念

笔者之前在 一文洞悉 JVM 内存管理机制 中就谈到过虚拟机栈,相信看过的读者都有印象

  • 栈帧:用于支持虚拟机进行方法调用和方法执行的数据结构,是虚拟机栈的栈元素
  • 存储内容:方法的局部变量表、操作数栈、动态连接、方法返回地址和一些额外的附加信息
  • 每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程
  • 一个栈帧需要分配多少内存在程序编译期就已确定,而不会受到程序运行期变量数据的影响
  • 对于执行引擎来说,只有位于栈顶的栈帧(当前栈帧)才是有效的,即所有字节码指令只对当前栈帧进行操作,与当前栈帧相关联的方法称为当前方法
栈帧结构

2.2.2 局部变量表

  • 定义:局部变量表是一组变量值存储空间
  • 作用:存放方法参数和方法内部定义的局部变量
  • 分配时期:Java 程序编译Class 文件时,会在方法的 Code 属性的 max_locals 数据项中确定了该方法所需要分配的局部变量表的最大容量
  • 最小单位:变量槽
  • 大小:虚拟机规范中没有明确指明一个变量槽占用的内存空间大小,允许变量槽长度随着处理器、操作系统或虚拟机的不同而发生变化
    1. 对于 32 位以内的数据类型(booleanbytecharshortintfloatreferencereturnAddress ),虚拟机会为其分配一个变量槽空间
    2. 对于 64 位的数据类型(longdouble ),虚拟机会以高位对齐的方式为其分配两个连续的变量槽空间
  • 特点:可重用。为了尽可能节省栈帧空间,若当前字节码 PC 计数器的值已超出了某个变量的作用域,则该变量对应的变量槽可交给其他变量使用
  • 访问方式:通过索引定位。索引值的范围是从 0 开始至局部变量表最大的变量槽数量

  • 局部变量表第一项是名为 this 的一个当前类引用,它指向堆中当前对象的引用(由反编译得到的局部变量表可知)

    局部变量表

2.2.3 操作数栈

  • 操作数栈是一个后入先出栈

  • 作用:在方法执行过程中,写入(进栈)和提取(出栈)各种字节码指令

  • 分配时期:同上,在编译时会在方法的 Code 属性的 max_stacks 数据项中确定操作数栈的最大深度

  • 栈容量:操作数栈的每一个元素可以是任意的 Java 数据类型 ——32 位数据类型所占的栈容量为 164 位数据类型所占的栈容量为 2

  • 注意:操作数栈中元素的数据类型必须与字节码指令的序列严格匹配,在编译时编译器需要验证一次、在类校验阶段的数据流分析中还要再次验证

2.2.4 动态连接

  • 定义:每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接
  • 静态解析和动态连接区别:

Class 文件的常量池中存有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用作为参数,这些符号引用:

  • 一部分会在类加载阶段或者第一次使用的时候就转化为直接引用(静态解析
  • 另一部分会在每一次运行期间转化为直接引用(动态连接

2.2.5 方法返回地址

  • 方法退出的两种方式:
  • 正常退出:执行中遇到任意一个方法返回的字节码指令
  • 异常退出:执行中遇到异常、且在本方法的异常表中没有搜索到匹配的异常处理器区处理
  • 作用:在方法返回时都可能在栈帧中保存一些信息,用于恢复上层方法调用者的执行状态
  • 正常退出时,调用者的 PC 计数器的值可以作为返回地址
  • 异常退出时,通过异常处理器表来确定返回地址
  • 方法退出的执行操作:
  • 恢复上层方法的局部变量表和操作数栈
  • 若有返回值把它压入调用者栈帧的操作数栈中
  • 调整 PC 计数器的值以指向方法调用指令后面的一条指令等

在实际开发中,一般会把动态连接、方法返回地址与其他附加信息全部一起称为栈帧信息

2.3 方法调用

  • 方法调用是最普遍且频繁的操作
  • 任务:确定被调用方法的版本,即调用哪一个方法,不涉及方法内部的具体运行过程

下面笔者将为大家详细讲解方法调用的类型

2.3.1 解析调用

笔者之前在 一夜搞懂 | JVM 类加载机制中就谈到过解析,感觉有点混淆的,可以回去看下

  • 特点:
    1. 是静态过程
    2. 在编译期间就完全确定,在类装载解析阶段就会把涉及的符号引用全部转变为可确定的直接引用,而不会延迟到运行期再去完成,即编译期可知、运行期不变
  • 适用对象: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 指令需要完成以下几个步骤才能实现对一个方法的调用:

image

因此,疑惑释然

正解
3.单分派
  • 含义:根据一个宗量对目标方法进行选择(方法的接受者与方法的参数统称为方法的宗量)
4.多分派
  • 含义:根据多于一个宗量对目标方法进行选择

想了解 静态多分派,动态单分派 的可以看下这篇文章:Java 中的静态单多分派与动态单分派

三.碎碎念

恭喜你!已经看完了前面的文章,相信你对JVM字节码执行引擎已经有一定深度的了解!你可以稍微放松奖励自己一下,可以睡一个美美的觉,明天起来继续冲冲冲!!!

睡觉不香吗

如果文章对您有一点帮助的话,希望您能点一下赞,您的点赞,是我前进的动力

本文参考链接:

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,053评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,527评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,779评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,685评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,699评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,609评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,989评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,654评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,890评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,634评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,716评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,394评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,976评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,950评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,191评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,849评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,458评论 2 342

推荐阅读更多精彩内容