一夜搞懂 | JVM 字节码执行引擎
前言
本文已经收录到我的 Github 个人博客,欢迎大佬们光临寒舍:
学习导图

一.为什么要学习字节码执行引擎?
代码编译的结果从本地机器码转变为字节码,是存储格式发展的一小步,却是编程语言发展的一大步
首先,抛出灵魂三问:
- 虚拟机在执行代码的时候,如何找到正确的方法呢?
- 如何执行方法内的字节码呢?
- 执行代码时涉及的内存结构有哪些呢?
如果你对上述问题理解得还不是特别透彻的话,可以看下这篇文章;如果理解了,你可以关闭网页,打开游戏放松了hhh
下面,笔者将带你探究 JVM 核心的组成部分之一——执行引擎。
二.核心知识点归纳
2.1 概述
Q1:虚拟机与物理机的异同
- 相同点:都有代码执行能力
- 不同点:
- 物理机的执行引擎是直接建立在处理器、硬件、指令集和操作系统层面上的
- 虚拟机的执行引擎是由自定义的,可自行制定指令集与执行引擎的结构体系,且能够执行不被硬件直接支持的指令集格式
Q2:有关 JVM 字节码执行引擎的概念模型
- 外观上:所有
JVM的执行引擎都是一致的。输入的是字节码文件,处理的是字节码解析的等效过程,输出的是执行结果

- 从实现上,执行引擎有多种执行
Java代码的选择
- 解释执行:通过解释器执行
- 编译执行:通过即时编译器产生本地代码执行
- 两者兼备,甚至还会包含几个不同级别的编译器执行引擎
2.2 运行时栈帧结构
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计数器的值已超出了某个变量的作用域,则该变量对应的变量槽可交给其他变量使用
访问方式:通过索引定位。索引值的范围是从 0 开始至局部变量表最大的变量槽数量
局部变量表第一项是名为
this的一个当前类引用,它指向堆中当前对象的引用(由反编译得到的局部变量表可知)
2.2.3 操作数栈
操作数栈是一个后入先出栈
作用:在方法执行过程中,写入(进栈)和提取(出栈)各种字节码指令
分配时期:同上,在编译时会在方法的
Code属性的max_stacks数据项中确定操作数栈的最大深度栈容量:操作数栈的每一个元素可以是任意的
Java数据类型 ——32位数据类型所占的栈容量为1,64位数据类型所占的栈容量为2注意:操作数栈中元素的数据类型必须与字节码指令的序列严格匹配,在编译时编译器需要验证一次、在类校验阶段的数据流分析中还要再次验证
2.2.4 动态连接
- 定义:每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接
- 静态解析和动态连接区别:
Class文件的常量池中存有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用作为参数,这些符号引用:
- 一部分会在类加载阶段或者第一次使用的时候就转化为直接引用(静态解析)
- 另一部分会在每一次运行期间转化为直接引用(动态连接)
2.2.5 方法返回地址
- 方法退出的两种方式:
- 正常退出:执行中遇到任意一个方法返回的字节码指令
- 异常退出:执行中遇到异常、且在本方法的异常表中没有搜索到匹配的异常处理器区处理
- 作用:在方法返回时都可能在栈帧中保存一些信息,用于恢复上层方法调用者的执行状态
- 正常退出时,调用者的
PC计数器的值可以作为返回地址- 异常退出时,通过异常处理器表来确定返回地址
- 方法退出的执行操作:
- 恢复上层方法的局部变量表和操作数栈
- 若有返回值把它压入调用者栈帧的操作数栈中
- 调整
PC计数器的值以指向方法调用指令后面的一条指令等
在实际开发中,一般会把动态连接、方法返回地址与其他附加信息全部一起称为栈帧信息
2.3 方法调用
- 方法调用是最普遍且频繁的操作
- 任务:确定被调用方法的版本,即调用哪一个方法,不涉及方法内部的具体运行过程
下面笔者将为大家详细讲解方法调用的类型
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字节码执行引擎已经有一定深度的了解!你可以稍微放松奖励自己一下,可以睡一个美美的觉,明天起来继续冲冲冲!!!
如果文章对您有一点帮助的话,希望您能点一下赞,您的点赞,是我前进的动力
本文参考链接:
- 《深入理解Java虚拟机》第3版
- Java 中的静态单多分派与动态单分派
- 要点提炼 | 理解 JVM 之字节码执行引擎
- 虚拟机字节码执行引擎
一夜搞懂 | JVM 字节码执行引擎的更多相关文章
- JVM总结(五):JVM字节码执行引擎
JVM字节码执行引擎 运行时栈帧结构 局部变量表 操作数栈 动态连接 方法返回地址 附加信息 方法调用 解析 分派 –“重载”和“重写”的实现 静态分派 动态分派 单分派和多分派 JVM动态分派的实现 ...
- 深入理解JVM—字节码执行引擎
原文地址:http://yhjhappy234.blog.163.com/blog/static/3163283220122204355694/ 前面我们不止一次的提到,Java是一种跨平台的语言,为 ...
- JVM字节码执行引擎和动态绑定原理
1.执行引擎 所有Java虚拟机的执行引擎都是一致的: 输入的是字节码文件,处理过程就是解析过程,最后输出执行结果. 在整个过程不同的数据在不同的结构中进行处理. 2.栈帧 jvm进行方法调用和方法执 ...
- JVM字节码执行引擎
一.概述 在不同的虚拟机实现里面,执行引擎在执行Java代码的时候可能会有解释执行(通过解释器执行)和编译器执行(通过即时编译器产生本地代码执行)两种选择,所有的Java虚拟机的执行引擎都是一致的:输 ...
- 图解JVM字节码执行引擎之栈帧结构
一.执行引擎 “虚拟机”的概念是相对于“物理机”而言的,这两种“机器”都有执行代码的能力.物理机的执行引擎是直接建立在硬件处理器.物理寄存器.指令集和操作系统层面的:而“虚拟机”的执行引擎是 ...
- 深入理解JVM虚拟机5:虚拟机字节码执行引擎
虚拟机字节码执行引擎 转自https://juejin.im/post/5abc97ff518825556a727e66 所谓的「虚拟机字节码执行引擎」其实就是 JVM 根据 Class 文件中给 ...
- JVM学习笔记:字节码执行引擎
JVM学习笔记:字节码执行引擎 移步大神贴:http://rednaxelafx.iteye.com/blog/492667
- 《深入理解Java虚拟机》-----第8章 虚拟机字节码执行引擎——Java高级开发必须懂的
概述 执行引擎是Java虚拟机最核心的组成部分之一.“虚拟机”是一个相对于“物理机”的概念 ,这两种机器都有代码执行能力,其区别是物理机的执行引擎是直接建立在处理器.硬件.指令集和操作系统层面上的,而 ...
- JVM基础结构与字节码执行引擎
JVM基础结构 JVM内部结构如下:栈.堆. 栈 JVM中的栈主要是指线程里面的栈,里面有方法栈.native方法栈.PC寄存器等等:每个方法栈是由栈帧组成的:每个栈帧是由局部变量表.操作数栈等组成. ...
随机推荐
- 前端性能优化之Lazyload
前端性能优化之Lazyload @(Mob前端-冬晨)[JavaScript|技术分享|懒加载] [TOC] Lazyload 简介 前端工作中,界面和效果正在变得越来越狂拽炫酷,与此同时性能也是不得 ...
- LeetCode:两数之和、三数之和、四数之和
LeetCode:两数之和.三数之和.四数之和 多数之和问题,利用哈希集合减少时间复杂度以及多指针收缩窗口的巧妙解法 No.1 两数之和 给定一个整数数组 nums 和一个目标值 target,请你在 ...
- Spring配置cache(concurrentHashMap,guava cache、redis实现)附源码
在应用程序中,数据一般是存在数据库中(磁盘介质),对于某些被频繁访问的数据,如果每次都访问数据库,不仅涉及到网络io,还受到数据库查询的影响:而目前通常会将频繁使用,并且不经常改变的数据放入缓存中,从 ...
- vijos 1449 字符串还原
背景 小K是一位蔚蓝教主的崇拜者(Orz教主er),有一天,他收到了一封匿名信,信告诉了小K由于他表现出色,得到了一次当面Orz教主的机会,但是要当面Orz教主可不那么容易,不是每个人都有资格Orz教 ...
- ant tree 展开key的集合
这次有个功能 ant的tree 展开 点击子节点 新增节点之后 数据能够照常展开 有几种方法 我能想到的 因为ant 有个expanded 只要设置为true就能展开了,但是这边有个陷阱,就是仅仅设置 ...
- 前端解决跨域问题的终极武器——Nginx反向代理
提到代理,分为:正向代理和反向代理. 正向代理:就是你访问不了Google,但是国外有个VPN可以访问Google,你访问VPN后叫它访问Google,然后把数据传给你. 正向代理隐藏了真实的客户端. ...
- Mac上Burpsuite 拦截不到HTTPS流量怎么设置
在百度了一堆以及修修改改下终于拦截到HTTPS流量了. 安装步骤就大致讲一下吧 网上下载burp的安装包,然后Mac上直接打开这个burpUnlimited.jar包就可以了 我直接选择的第一个 ...
- web测试喜事连连--草稿箱功能
“草稿箱”功能很常见吧,编辑内容后,不想发布的话,就先存为草稿.啥时候想公开了,发布即可. 今天发生个啥事呢,让作为Tester的我,哭笑不得. 开发部经理老F,反馈一个客户需求,发到群里让大家讨论. ...
- 基于Linux Lite 4.8制作“Windows 12 Lite”正式发布
值得注意的是,这款Windows 12 Lite的宣传标语将矛头直指Windows 10,声称不会迫使用户更新.不会存在病毒或恶意软件.启动速度比Win 10快3倍.提供8万多款优质软件.允许与Win ...
- 数据结构 - Collection接口
简介 Collection继承自Iterable,Collection接口是Java集合两大分支中的一支,Queue.List.Set都是Collection的扩展:集合大类分为了Collection ...