五分钟看懂Java字节码:极简手册

news2024/11/6 7:27:55

字节码新手很容易被厚厚的 JVM 书籍劝退,即使我看过相关书籍,工作真正用到时也全忘了,还得现学。

等我有了一定的字节码阅读经验,才发现字节码其实非常简单,只需要三步就能快速学会:

  1. 先了解 JVM 的基本结构,只需要了解字节码相关的结构即可
  2. 然后将指令分成几类,学习每一类是用来操作 Jvm 哪个组件的
  3. 最后学习字节码的常见模式,比如 new 一般会被翻译成哪几指令,等等

之后按需查询 JVM指令官方文档,就可以出师了。

相比官方文档,本文更加适合顺序阅读,读完后再按需查阅官方文档。

JVM 的基本结构


只需要了解三个和字节码相关的结构。

栈帧

JVM 每调用一个方法就会压入一个新的栈帧到栈上,比如下面的例子:

public static void main() {
    a();
}

public static void a() {
    b();
}

public static void b() {
    //...此处的栈如图所示...
}

栈帧

虽然例子中举的是静态方法,但是对象方法也是一样。

重点:栈帧代表一个 Java 方法,里面装的是 Java 方法的运行时数据

操作数栈

每个栈帧中会有一个操作数栈,这其实就是一个用来执行各种计算的工作区,比如加法的执行过程:

  • 将 1 压栈
  • 将 2 压栈
  • 栈顶两个元素相加,将结果 3 压栈

局部变量表

栈帧中除了用于运算的操作数栈,还有用来临时存储变量的 “局部变量表”,比如下面的变量 a:

public static void main() {
    // a 会被存入 “局部变量表”
    int a = 10;
    //...此处省略1w行代码...
    // 将 a 从局部变量表中取出
    int b = a + 1;
}

操作数栈是栈帧中的存储,所以其生命周期仅限于一个 Java 方法。

而堆就是能够跨越 Java 方法的存储。一般用 new 新建的对象都是在堆里,然后在栈帧中只保留一个引用指针。

比如 Object a = new Object();

堆

指令分类学习


我们可以根据操作的 JVM 组件,将指令分类。每一类我们只要认识常见的几个,剩下的都非常相似。

指令的结构

JVM 指令的结构和 linux 命令差不多:指令 参数1 参数2 ...;其中每个部分占一个字节,所以叫做 “字节码”。

栈操作

  • dup: 复制栈顶元素
  • add: 将栈顶两个元素出栈相加,然后将结果入栈

JVM 并不存在 add 指令,而是各种专门数据类型的相加指令,比如 iadd 是整型相加,dadd 是浮点类型相加等等,其他指令也是类似的规律

  • ldc/bipush/const: 将常量压栈
    • ldc index: 将常量池中第 index 个元素入栈
    • bipush byte:此时常量不在常量池中,而是作为一个 byte 被编码在了指令中
    • const 则是一系列指令,比如 iconst_0 表示将整型 0 入栈,iconst_1 表示将整型 1 入栈,dconst_0 表示将浮点型 0 入栈等等,以此类推。可见 const 相比前两个是更加压缩的表示,一个 iconst_1 同时表达了指令(iconst)和常量值(1),这么做的好处是可以节约一个字节。

addconst 指令中可以看出 JVM 指令会用前缀表示指令操作的数据类型,比如 i 表示整型,d 表示浮点型等等

局部变量表操作

局部变量表是一个数组,通过下标访问。

  • store i: 将栈顶的值存储到局部变量表的 i 位置
  • load i:将局部变量表 i 位置的值加载到栈顶

store 和 load 也都是一系列指令,比如 istore i, iload i, dstore i, dload i 等等,甚至

虽然我们看到代码里的变量都是有具体的名字的,比如 int a = 1,b = 2 中的变量 a, b。其实在运行时,这些变量都会按照方法中出现的顺序被翻译成局部变量表的一个下标,int a = 1,b = 2 对应的字节码就是:

// 常量 1 入栈
0: iconst_1
// 将常量 1 存储到局部变量表的 1 位置(其实就是变量 a)
1: istore_1
// 常量 2 入栈
2: iconst_2
// 将常量 2 存储到局部变量表的 2 位置(其实就变量 b)
3: istore_2

如图所示:

赋值变量a

赋值变量b

局部变量表的 0 号位置一般是用来保存 this 引用的,所以图中没有使用。

堆操作

堆主要还是依靠保存在栈中的引用来操作的。

通过 new 指令在堆中新建一个对象,然后将引用保存在栈顶。

常见模式


静态方法调用

  • 将参数入栈
  • 然后使用 invokestatic 指令调用静态方法

如下代码:

static void a() {
    b(1,2,3);
}

static void b(int a, int b, int c) {
   //...
}

此时 a 方法的字节码就是:

// 将参数入栈
0: iconst 1
1: iconst_2
2: iconst3
// 调用静态方法
3: invokestatic b:(III)V

成员方法调用

  • 将调用对象入栈
  • 将参数入栈
  • 调用成员方法

如下代码:

class Test {

    void a() {
        b(1,2,3);
    }

    void b(int a, int b, int c) {

    }
}

此时 a 方法的字节码:

// 将调用对象入栈
// 回忆前文, this 引用存储在局部变量表的 0 号位置, 所以可以从局部变量表的 0 号位置加载到
0: aload_0
// 调用参数入栈
1: iconst_1
2: iconst_2
3: iconst_3
// 调用成员方法
4: invokevirtual b:(III)V

成员方法的调用,本质上是将对象作为隐藏的第 0 个参数,比如当调用 object.a(1) 时,在 JVM 层面相当于 a(object, 1)

另外,针对不同的情况,invoke 有一些 invokeXxx 方法:

  • invokeinterface:调用接口方法
  • invokevirtual: 调用对象的成员方法,是最常见的
  • invokespecial: 调用构造器等特殊方法。比如下文中的构造函数就是使用这个指令调用的

构造对象

总体步骤如下:

  1. new 指令新建一个空对象
  2. dup 复制栈顶
  3. 像 “方法调用” 中一样将构造函数的参数逐个入栈
  4. invokespecial <init> 调用对应的构造函数完成初始化

比如下面的代码:

Integer a = new Integer(10);

对应的指令就是:

// 步骤 1.
0: new java/lang/Integer
// 步骤 2.
3: dup
// 步骤 3.
4: bipush 10
// 步骤 4.
6: invokespecial java/lang/Integer."<init>":(I)V

前面编号代表的是指令位于第几个字节,从上面可以看出,除了 dup 以外,其他指令都需要占用多个字节

在编码阶段,对象看起来是构造好才返回的,但在字节码层面,其实对象新建和初始化是两个步骤,先新建一个空对象,然后才调用的初始化方法。

为什么需要一个 dup,因为构造方法在 JVM 层面其实就是一个普通的成员方法, invokespecial 需要将对象和参数一起从栈中弹出,而构造方法执行完后还需要在栈上保留一个对象,因此需要 dup 保留一个副本。

分支判断

分支判断主要使用 if_xxx index 系列指令,如果满足条件就跳转到 index 位置的指令执行。

如下代码:

public void b(int a, int b) {
    int c;
    if (a > b) {
        c = 1;
    } else {
        c = 2;
    }
    return;
}

编译后的字节码是:

// 加载局部变量 a
0: iload_1
// 加载局部变量 b
1: iload_2
// 如果 a <= b, 则跳转到位置 10 执行(其实就是 else 分支的位置)
2: if_icmple 10
5: iconst_1
6: istore_3
// 跳转到位置 12 执行(其实就是跳过 else 分支的指令)
7: goto 12
10: iconst_2
11: istore_3
12: return

大体逻辑如图:

分支判断

做字节码实验时的注意点

当我们编写好测试的 Java 代码 Test.java,可以用下面两条命令查看它的字节码:

javac Test.java
javap -c -v -l Test.class

需要注意的是,javac 中会做一些优化,导致字节码和源码对不上,比如下面的代码:

public static void main(String[] args) {
    if (2 < 1) {
        int a = 2;
    } else {
        int a = 1;
    }
}

编译后字节码是:

0: iconst_1
1: istore_1
2: return

可以看出,字节码其实只编译了 int a = 1 这一行代码,没有编译 if ... else ... 逻辑。这是因为 2 < 1 肯定是 false,于是编译器就把这段逻辑优化掉了。

如何阅读字节码官方文档


这篇文章只是带大家入个门,重点还是要能在遇到不认识的字节码时自主查阅 Java字节码官方文档。

比如下面的 iadd 字节码

文档示例

  • Operation:可看出是做 int 型加法操作
  • Operand Stack:表示指令执行会导致栈变化,iadd 会将栈顶两个元素出栈,然后将结果入栈

End


作者:元青

微信公众号 「技乐书香」

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/338275.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【设计模式之美 设计原则与思想:面向对象】14 | 实战二(下):如何利用面向对象设计和编程开发接口鉴权功能?

在上一节课中&#xff0c;针对接口鉴权功能的开发&#xff0c;我们讲了如何进行面向对象分析&#xff08;OOA&#xff09;&#xff0c;也就是需求分析。实际上&#xff0c;需求定义清楚之后&#xff0c;这个问题就已经解决了一大半&#xff0c;这也是为什么我花了那么多篇幅来讲…

创建Django项目

创建Django项目 步骤 创建Django项目 django-admin startproject name 创建子应用 python manager.py startapp name创建工程 在使用Flask框架时&#xff0c;项目工程目录的组织与创建是需要我们自己手动创建完成的。 在django中&#xff0c;项目工程目录可以借助django提供…

嵌软工程师要掌握的硬件知识2:一文看懂什么开漏和推挽电路(open-drain / push-pull)

想了解开漏和推挽,就要先了解一下三极管和场效应管是什么,在其他章节有详细介绍,本文就不再进行赘述。 1 推挽(push pull)电路 1.1 理解什么是推挽电路 - 详细介绍 如图所示,Q3是个NPN型三极管,Q4是个PNP型三极管。 1)当Vin电压为正时,上面的N型三极管控制端有电…

ccc-Classification-李宏毅(4)

文章目录Classification 概念Example ApplicationHow to do ClassificationWhy not RegesssionProbability from Class - FeatureProbability from ClassHow’s the results?Modifying ModelThree StepsProbability DistributionClassification 概念 本质是找一个函数&#x…

电商导购CPS,淘宝联盟如何跟单实现用户和订单绑定

前言 大家好&#xff0c;我是小悟 做过自媒体的小伙伴都知道&#xff0c;不管是发图文还是发短视频&#xff0c;直播也好&#xff0c;可以带货。在你的内容里面挂上商品&#xff0c;你自己都不需要囤货&#xff0c;如果用户通过这个商品下单成交了&#xff0c;自媒体平台就会…

基于 MySQL 排它锁实现分布式可重入锁解决方案

一、MySQL 排它锁和共享锁 在进行实验前&#xff0c;先来了解下MySQL 的排它锁和共享锁&#xff0c;在 MySQL 中的锁分为表锁和行锁&#xff0c;在行锁中锁又分成了排它锁和共享锁两种类型。 1. 排它锁 排他锁又称为写锁&#xff0c;简称X锁&#xff0c;是一种悲观锁&#x…

【C++】模板初阶STL简介

今天&#xff0c;你内卷了吗&#xff1f; 文章目录一、泛型编程二、函数模板&#xff08;显示实例化和隐式实例化&#xff09;1.函数模板格式2.单参数模板3.多参数模板4.模板参数的匹配原则三、类模板&#xff08;没有推演的时机&#xff0c;统一显示实例化&#xff09;1.类模…

RTOS之二环境搭建初识RTOS

参考&#xff1a;https://blog.csdn.net/kouxi1/article/details/123650688RTOS本质就是切换线程栈&#xff0c;栈换了环境就换了&#xff0c;一个重要的结构tcb&#xff08;linux叫PCB或thread_info&#xff09;&#xff1a;struct tcb{int32_t *sp; // 重要的sp指针&#xff…

seata【SAGA模式】代码实践(细节未必完全符合saga的配置,仅参考)

seata SAGA模式&#xff1a; 代码仍然是上一篇AT模式的代码&#xff1a;AT模式 不需要undo_log表 下面开始&#xff1a; 首先&#xff0c;saga模式依靠状态机的json文件来执行整个流程&#xff0c;其中的开始节点的服务即TM&#xff0c;然后状态机需要依靠三张表&#xff0…

【大数据】Hadoop-HA-Federation-3.3.1集群高可用联邦安装部署文档(建议收藏哦)

背景概述 单 NameNode 的架构使得 HDFS 在集群扩展性和性能上都有潜在的问题&#xff0c;当集群大到一定程度后&#xff0c;NameNode 进程使用的内存可能会达到上百 G&#xff0c;NameNode 成为了性能的瓶颈。因而提出了 namenode 水平扩展方案-- Federation。 Federation 中…

C语言---字符串函数总结

&#x1f680;write in front&#x1f680; &#x1f4dd;个人主页&#xff1a;认真写博客的夏目浅石. &#x1f381;欢迎各位→点赞&#x1f44d; 收藏⭐️ 留言&#x1f4dd; &#x1f4e3;系列专栏&#xff1a;夏目的C语言宝藏 &#x1f4ac;总结&#xff1a;希望你看完之…

ChatGPT国内使用方法全攻略(完整图文教程)

你好呀&#xff0c;我是月亮&#xff0c;一个90后的老程序员啦~ 最近ChatGPT完全火出圈了。 相关教程很多&#xff0c;我整理了一份网盘汇总&#xff0c;包含注册、谷歌浏览器插件使用、国内面注册平台&#xff0c;需要的小伙伴自取~ 网盘地址&#xff1a;使用方式汇总文档 …

数据库实践LAB大纲 06 INDEX

索引 索引是一个列表 —— 若干列集合和这些值的记录在数据表存储位置的物理地址 作用 加快检索速度唯一性索引 —— 保障数据唯一性加速表的连接分组和排序进行检索的时候 —— 减少时间消耗 一般建立原则 经常查询的数据主键外键连接字段排序字段少涉及、重复值多的字段…

分享114个JS菜单导航,总有一款适合您

分享114个JS菜单导航&#xff0c;总有一款适合您 114个JS菜单导航下载链接&#xff1a;https://pan.baidu.com/s/1t4_v0PipMjw3ULTLqkEiDQ?pwdgoi2 提取码&#xff1a;goi2 Python采集代码下载链接&#xff1a;https://wwgn.lanzoul.com/iKGwb0kye3wj $.ajax({type: &quo…

“万字“ Java I/O流讲解

Java I/O流讲解 每博一文案 谁让你读了这么多书&#xff0c;又知道了双水村以外还有一个大世界&#xff0c;如果从小你就在这个天地里&#xff0c;日出而作&#xff0c;日落而息。 那你现在就会和众乡亲抱同一理想&#xff1a;经过几年的辛劳&#xff0c;像大哥一样娶个满意的…

2023年中国各大城市薪酬报告出炉

全国地区&#xff1a;https://download.csdn.net/download/std86021/87322224北京&#xff1a;https://download.csdn.net/download/std86021/87273488上海&#xff1a;https://download.csdn.net/download/std86021/87322226广州&#xff1a;https://download.csdn.net/downlo…

Linux之文本搜索命令

文本搜索命令学习目标能够知道文本搜索使用的命令1. grep命令的使用命令说明grep文本搜索grep命令效果图:2. grep命令选项的使用命令选项说明-i忽略大小写-n显示匹配行号-v显示不包含匹配文本的所有行-i命令选项效果图:-n命令选项效果图:-v命令选项效果图:3. grep命令结合正则表…

linux基本功系列之hostname实战

文章目录前言一. hostname命令介绍二. 语法格式及常用选项三. 参考案例3.1 显示本机的主机名3.2 临时修改主机名3.3 显示短格式的主机名3.4 显示主机的ip地址四. 永久修改主机名4.1 centos6 修改主机名的方式4.2 centos7中修改主机名永久生效总结前言 大家好&#xff0c;又见面…

Java、JSP企业快信系统的设计与实现

技术&#xff1a;Java、JSP等摘要&#xff1a;计算机网络的出现到现在已经经历了翻天覆地的重大改变。因特网也从最早的供科学家交流心得的简单的文本浏览器发展成为了商务和信息的中心。到了今天&#xff0c;互联网已经成为了大量应用的首选平台&#xff0c;人们已经渐渐习惯了…

02- 天池工业蒸汽量项目实战 (项目二)

忽略警告: warnings.filterwarnings("ignore") import warnings warnings.filterwarnings("ignore") 读取文件格式: pd.read_csv(train_data_file, sep\t) # 注意sep 是 , , 还是\ttrain_data.info() # 查看是否存在空数据及数据类型train_data.desc…