深入了解 Java 字节码
1.1 什么是字节码?
Java 在刚刚诞生之时曾经提出过一个非常著名的口号: “一次编写,到处运行(write once,run anywhere)”,这句话充分表达了软件开发人员对冲破平台界限的渴求。“与平台无关”的理想最终实现在操作系统的运用层上: 虚拟机提供商开发了许多可以运行在不同平台上的虚拟机,这些虚拟机都可以载入和执行同一种平台无关的字节码,从而实现了程序的“一次编写到处运行”。
各种不同平台的虚拟机与所有平台都统一使用的程序存储格式—字节码(ByteCode),因此,可以看出字节码对 Java 生态的重要性。之所以被称为字节码,是因为字节码是由十六进制组成的,而 JVM(Java Virtual Machine)以两个十六进制为一组,即以字节为单位进行读取。在 Java 中使用 javac 命令把源代码编译成字节码文件,一个 .java 源文件从编译成 .class 字节码文件的示例如图 1 所示:

图 1
对于从事基于 JVM 的语言的开发人员来说,比如: Java,了解字节码可以更准确、更直观的理解 Java 语言中更深层次的东西,比如通过字节码,可以很直观的看到 volatile 关键字如何在字节码上生效。另外,字节码增强技术在各种 ORM 框架、Spring AOP、热部署等一些应用中经常使用,深入理解其原理对于我们来说大有裨益。由于 JVM 规范的存在,只要最终生成了符合 JVM 字节码规范的文件都可以在 JVM 上运行,因此,这个也给其它各种运行在 JVM 上的语言(如: Scala、Groovy、Kotlin)提供了一个机会,可以扩展 Java 没有实现的特性或者实现一些语法糖。
接下来就让我们就一起看看这个字节码文件结构到底是什么样的。
1.2 Java 字节码结构
Java 源文件通过用 javac 命令编译后就会得到 .class 结尾的字节码文件,比如一个简单的 JavaCodeCompilerDemo 类如图 2 所示:

图 2
编译后生成的 .class 字节码文件,打开后是一堆 十六进制 数,如图 3 所示:
图 3
在上节提过,JVM 对于字节码规范是有要求的,打开编译后的字节码文件看似混乱无章,其实它是符合一定的结构规范的,JVM 规范要求每一个字节码文件都要由十部分固定的顺序组成的,接下来我们将一一介绍这部分,整体的组成结构如图 4 所示:
图 4
(1)魔数(Magic Number)
每个字节码文件的头 4 个字节称为 魔数(Magic Number),它的唯一作用是确定这个文件是否为一个能被虚拟机接受的 Class 文件。很多文件存储标准中都使用魔数来进行身份识别,譬如图片格式,如 gif 或者 jpg 等在文件头中都存有魔数。使用魔数而不是扩展名来进行识别主要是基于安全方面的考虑,因为文件扩展名可以随意改动。魔数的固定值为: 0xCAFEBABE,魔数放在文件头,JVM 可以根据文件的开头来判断这个文件是否可能是一个字节码文件,如果是,才会进行之后的操作。
有趣的是,魔数的固定值是 Java 之父 James Gosling 制定的,为 CafeBabe(咖啡宝贝),而 Java 的图标为一杯咖啡。
(2)版本号(Version)
版本号为魔数之后的 4 个字节,前两个字节表示次版本号(Minor Version),后两个字节表示主版本号(Major Version),上图 3 中版本号为: “00 00 00 34”,次版本号转化为十进制为 0,主版本号转化为十进制 52(3 * 16^1 + 4 * 16^0 = 52),在 Oracle 官网中查询序号 52 对应的 JDK 版本为 1.8,所以编译该源代码文件的 Java 版本为 1.8.0。
(3)常量池(Constant Pool)
紧接着主版本号之后的字节是常量池入口。常量池中存储两种类型常量: 字面量和符号运用。字面量为代码中声明为 final 的常量值,符号引用如类和接口的全局限定名、字段的名称和描述符、方法的名称和描述符。常量池整体上分为两部分: 常量池计数器和常量池数据区,如图 5 所示:

图 5
常量池计数器(constant_pool_count): 由于常量池的数量不固定,所以需要先放置两个字节来表示常量池容量计数值,图 2 示例代码的字节码的前十个字节如下图 6 所示,将十六进制的 17 转为十进制的值为 33 (1 * 16^1 + 7 * 16^0 = 33),排除下标 0,也就是说这个类文件有 32 个常量。
图 6
常量池数据区: 数据区是由(constant_pool_count - 1)个 cp_info 结构组成,一个 cp_info 的结构对应一个常量。在字节码中共有 14 种类型的 cp_info ,每种类型的结构都是固定的,如图 7 所示:
图 7
以 CONSTANT_Utf8_info 为例,它的结构如表 1 所示:
| 名称 | 长度 | 值 |
|---|---|---|
| tag | 1 字节 | 01 对应图 7 中 CONSTANT_Utf8_info 的标志栏中的值 |
| length | 2 字节 | 该 utf8 字符串的长度 |
| bytes | length 字节 | length 个字节的具体数据 |
表 1
首先第一个字节 tag,它的取值对应图 7 中的 Tag,由于它的类型是 CONSTANT_Utf8_info,所以值为 01(十六进制)。接下来两个字节标识该字符串的长度 length,然后 length 个字节为这个字符串具体的值。从图 3 的字节码中摘取一个 cp_info 结构,将它翻译过来后,其含义为: 该常量为 utf8 字符串,长度为 7 字节,数据为: numberA,如图 8 所示:

图 8
其它类型的 cp_info 结构在本文不在细说,和 CONSTANT_Utf8_info 的结构大同小异,都是先通过 tag 来标识类型,然后后续的 n 个字节来描述长度和数据。等我们对这些结构比较了解了之后,我们可以通过: javap -verbose JavaCodeCompilerDemo 命令查看 JVM 反编译后的完整常量池,可以看到反编译结果可以将每一个 cp_info 结构的类型和值都很明确的呈现出来,如图 9 所示:
图 9
(4)访问标志(access_flag)
常量池结束之后的两个字节,描述该 Class 是类还是接口,以及是否被 Public、Abstract、Final 等修饰符修饰。JVM 规范规定了如下表 2 所示的 9 种访问标志。需要注意的是,JVM 并没有穷举所有的访问标志,而是使用 按位或 操作来进行描述的,比如某个类的修饰符为 public final,则对应的访问修饰符的值为 ACC_PUBLIC | ACC_FINAL,即 0x0001 | 0x0010 = 0x0011。
| 标志名称 | 标志值 | 含义 |
|---|---|---|
| ACC_PUBLIC | 0x0001 | 字段是否为 public |
| ACC_PRIVATE | 0x0002 | 字段是否为 private |
| ACC_PROTECTED | 0x0004 | 字段是否为 protected |
| ACC_STATIC | 0x0008 | 字段是否为 static |
| ACC_FINAL | 0x0010 | 字段是否为 final |
| ACC_VOLATILE | 0x0040 | 字段是否为 volatile |
| ACC_TRANSIENT | 0x0080 | 字段是否为 transient |
| ACC_SYNCHETIC | 0x1000 | 字段是否为编译器自动产生 |
| ACC_ENUM | 0x4000 | 字段是否为 enum |
表 2
(5)当前类名(this_class)
访问标志后的两个字节,描述的是当前类的全限定名。这两个字节保存的值为常量池中的索引值,根据索引值就能在常量池中找到这个类的全限定名。
(6)父类名称(super_class)
当前类名的后两个字节,描述父类的全限定名。这两个字节保存的值也是在常量池中的索引值,根据索引值就能在常量池中找到这个类的父类的全限定名。
(7)接口信息(interfaces)
父类名称后的两个字节,描述这个类的接口计数器,即: 当前类或父类实现的接口数量。紧接着的 n 个字节是所有的接口名称的字符串常量在常量池的索引值。
(8)字段表(field_table)
字段表用于描述类和接口中声明的变量,包含类级别的变量以及实例变量,但是不包含方法内部声明的 局部变量。字段表也分为两部分,第一部分是两个字节,描述字段个数,第二部分是每个字段的详细信息 field_info。字段表结构如图 10 所示:

图 10
以图 3 中的字节码字段表为例,如下图 11 所示。其中字段的访问标志查表 2,002 对应为 Private,通过索引下标在图 9 中常量池分别得到字段名为: numberA,描述符为: I(在JVM 中的I代表 Java 中的 int)。综上,就可以唯一确定出类 JavaCodeCompilerDemo 中声明的变量为: private int numberA 。
图 11
(9)方法表(method_table)
字段表结束后为方法表,方法表也是由两部分组成,第一部分为两个字节描述方法的个数,第二个部分为每个方法的详细信息。方法的详细信息包括:方法的访问标志、方法名、方法的描述符以及方法的属性,如图 12 所示:

图 12
方法的权限修饰符依然可以通过图 9 的值查询到,方法名和方法的描述符都是常量池的索引值,可以通过索引值在常量池中查询得到。而方法属性这个部分比较复杂,我们可以借助 javap -verbose 将其反编译为人们可读的信息进行解读。如图 13 所示。我们可以看到属性中包含三个部分:
- Code 区: 源代码对应的 JVM 指令操作码,我们在字节码增强的时候重点操作的就是这个部分。
- LineNumberTable: 行号表,将 Code 区的操作码和源代码的行号对应,Debug 时会起到作用(即: 当源代码向下走一行,相应的需要走几个 JVM 指令操作码)。
- LocalVariableTable: 本地变量表,包含 this 和局部变量,之所以可以在每一个非 static 的方法内部都可以调用到 this,是因为 JVM 将 this 作为每个方法的第一个参数隐式进行传入。
图 13
(10)附加属性表(additional_attribute_table)
字节码的最后一部分,存放了在文件中类或接口所定义的属性的基本信息。
1.3 Java 字节码操作集合
在图 13 中,Code 区的编号是 0 ~ 10,就是 .java 源文件的方法源代码编译后让 JVM 真正执行的操作码。为了帮助人们理解,反编译后看到的是十六进制操作码所对应的助记符,十六进制值操作码和助记符的对应关系,以及每个操作码的具体作用可以查看 Oracle 官网,在需要的时候查阅即可。比如上图 13 的助记符为 iconst_2,对应图 3 中的字节码 0x05,作用是将 int 值 2 压入操作数栈中。以此类推,对 0 ~ 10 的助记符理解后就是整个 sum() 方法的操作数码实现。
1.4 查看字节码工具
如果我们每次反编译都要使用 javap 命令的话,确实比较繁琐,这里我推荐大家一个 IDEA 插件: jclasslib。使用效果如图 14 所示: 代码编译后在菜单栏: View -> Show Bytecode With jclasslib,可以很直观地看到当前字节码文件的类信息、常量池、方法区等信息,非常方便。

图 14
1.5 总结
Java 中字节码文件是 JVM 执行引擎的数据入口,也是 Java 技术体系的基础构成之一。了解字节码文件的组成结构对后面进一步了解虚拟机和深入学习 Java 有很重要的意义。本文较为详细的讲解了字节码文件结构的各个组成部分,以及每个部分的定义、数据结构和使用方法。强烈建议自己动手分析一下,会理解得更加深入。
深入了解 Java 字节码的更多相关文章
- 在Eclipse里查看Java字节码
要理解 Java 字节码,比较推荐的方法是自己尝试编写源码对照字节码学习.其中阅读 Java 字节码的工具必不可少.虽然javap可以以可读的形式展示出.class 文件中字节码,但每次改动源码都需调 ...
- JAVA字节码解析
Java字节码指令 Java 字节码指令及javap 使用说明 ### java字节码指令列表 字节码 助记符 指令含义 0x00 nop 什么都不做 0x01 aconst_null 将null推送 ...
- 【转】在Eclipse里查看Java字节码
要理解 Java 字节码,比较推荐的方法是自己尝试编写源码对照字节码学习.其中阅读 Java 字节码的工具必不可少.虽然javap可以以可读的形式展示出.class 文件中字节码,但每次改动源码都需调 ...
- Java字节码(.class文件)格式详解(一)
原文链接:http://www.blogjava.net/DLevin/archive/2011/09/05/358033.html 小介:去年在读<深入解析JVM>的时候写的,记得当时还 ...
- 通过Java字节码发现有趣的内幕之String篇(上)(转)
原文出处: jaffa 很多时候我们在编写Java代码时,判断和猜测代码问题时主要是通过运行结果来得到答案,本博文主要是想通过Java字节码的方式来进一步求证我们已知的东西.这里没有对Java字节码知 ...
- 掌握Java字节码(转)
Java是一门设计为运行于虚拟机之上的编程语言,因此它需要一次编译,处处运行(当然也是一次编写,处处测试).因此,安装到你系统上的JVM是原生的程序,而运行在它之上的代码是平台无关的.Java字节码就 ...
- Java字节码操纵框架ASM小试
本文主要内容: ASM是什么 JVM指令 Java字节码文件 ASM编程模型 ASM示例 参考资料汇总 JVM详细指令 ASM是什么 ASM是一个Java字节码操纵框架,它能被用来动态生成类或者增强既 ...
- Java:从面试题“i++和++i哪个效率高?"开始学习java字节码
今天看到一道面试题,i++和++i的效率谁高谁低. 面试题的答案是++i要高一点. 我在网上搜了一圈儿,发现很多回答也都是同一个结论. 如果早个几年,我也会认同这个看法,但现在我负责任的说,这个结论是 ...
- Java字节码—ASM
前言 ASM 是什么 官方介绍:ASM is an all purpose Java bytecode manipulation and analysis framework. It can be u ...
- 打造一个简单的Java字节码反编译器
简介 本文示范了一种反编译Java字节码的方法,首先通过解析class文件,然后将解析的结果转成java代码.但是本文并没有覆盖所有的class文件的特性和指令,只针对部分规范进行解析. 所有的代码代 ...
随机推荐
- verilog的文件流和项目流
verilog的文件流和项目流 1.写在前面 在学习FPGA时,一般都是从项目流入手的.从一个集成的开发环境创建一个工程.通过一个个组件的编写和设置来实现某个项目.这样的操作固然简单,对于设计者来说只 ...
- 李东山:如何让 OpenHarmony 支持低功耗蓝牙芯片 GR551x
编者按:在 OpenHarmony 生态发展过程中,涌现了大批优秀的代码贡献者,本专题旨在表彰贡献.分享经验,文中内容来自嘉宾访谈,不代表 OpenHarmony 工作委员会观点. 李东山 深圳市汇顶 ...
- 组合数学——Min-Max容斥
Min-Max 容斥,即 $$\max(S)=\sum_{T\in S,T\neq\emptyset}(-1)^{|T|-1}\min(T)$$ 接下来证明上面那个式子是对的.定义 \(S\) 中共有 ...
- C# Lock的用法
当我们使用线程的时候,效率最高的方式当然是异步,即各个线程同时运行,其间不相互依赖和等待.但当不同的线程都需要访问某个资源的时候,就需要同步机制了,也就是说当对同一个资源进行读写的时候,我们要使该资源 ...
- Qt加Opencv实现 梯度矫正 功能
废话: 有时候我们是从物品的斜上方拍摄的图片,看起来不直观,需要把视角拉正,这样的一个操作就叫做 梯度矫正,需要用到的技术是 Opencv 的 透视变换. 这个只是一个简单的演示demo,如果完善一下 ...
- Qt 实现涂鸦板三:实现鼠标绘制矩形
.h 文件 #pragma once #include <QtWidgets/QWidget> #include "ui_xuexi.h" #include " ...
- 一键部署openGauss2.0.1 CentOS 7.6
一键部署 openGauss2.0.1[CentOS 7.6] 本文档目的是为了帮助高校学生提供基于 CentOS7.6 操作系统,实现 openGauss 数据库一键式安装的脚本. 该脚本执行成功后 ...
- Prometheus Operator与kube-prometheus之二-如何监控1.23+ kubeadm集群
简介 系列文章: 标签 - Prometheus - 东风微鸣技术博客 (ewhisper.cn) Prometheus Operator 的上一篇: Prometheus Operator 与 ku ...
- 实例讲解昇腾 CANN YOLOV8 和 YOLOV9 适配
本文分享自华为云社区<昇腾 CANN YOLOV8 和 YOLOV9 适配>,作者:jackwangcumt. 1 概述 华为昇腾 CANN YOLOV8 推理示例 C++样例 , 是基于 ...
- HDC2021技术分论坛:盘点分布式软总线数据传输技术中的黑科技
作者:houweibo,软总线首席技术专家:lidonghua,软总线技术专家 随着万物互联时代的到来,特别是大量媒体资源的涌入和使用,用户对传输的要求不断提高,怎样的传输技术才能满足未来的用户需求呢 ...