字节码

2022/07/13

字节码

什么是字节码?

字节码(Byte code),是 Java 代码编译后的中间代码格式。JVM需要读取并解析字节码,校验之后通过JIT编译器转换为本地机器代码执行。

字节码是由十六进制组成的,而JVM以两个十六进制为一组,即以字节为单位进行读取。

字节码结构

Java源文件通过用javac命令编译后就会得到 .class 结尾的字节码文件,比如一个简单的 JavaCodeCompilerDemo类及编译后生成的 .class 字节码文件如下图:

image-20230313153458207

JVM规范要求每一个字节码文件都要由十部分按照固定的顺序组成,整体结构如下图所示:

image-20230313155705519

根据《Java虚拟机规范》的规定,Class文件采用一种类似于C语言结构体的伪结构来存储数据,这种伪结构中只有两种数据类型:“无符号数”和“”。后面的解析都以这两种数据类型为基础

无符号数属于基本的数据类型,以 u1、u2、u4、u8 来分别代表1个字节、2个字节、4个字节和8个字节的无符号数,无符号数可以用来描述数字索引引用数量值或者按照UTF-8编码构成字符串值。

表是由多个无符号数或者其他表作为数据项构成的复合数据类型,为了便于区分,所有表的命名都习惯性地以“_info”结尾。表用于描述有层次关系的复合结构的数据,整个Class文件本质上也可以视作是一张表。

上图是官方文档中给出的结构,下图是合并之后的图,往下看就很容易理解:

image-20230313153712617

(1)魔数(Magic Number) 每个字节码文件的头4个字节称为 魔数(Magic Number),它的唯一作用是确定这个文件是否为一个能被虚拟机接受的 Class 文件。很多文件存储标准中都使用魔数来进行身份识别,譬如图片格式,如 gif 或者 jpg 等在文件中都存有魔数。使用魔数而不是扩展名来进行识别主要是基于安全方面的考虑,因为文件扩展名可以随意改动。

魔数的固定值为: 0xCAFEBABE,魔数放在文件头,JVM 可以根据文件的开头来判断这个文件是否可能是一个字节码文件,如果是,才会进行之后的操

**(2)版本号(Version)**版本号为魔数之后的4个字节,前两个字节表示次版本号(Minor Version),后两个字节表示主版本号(Major Version),上图中版本号为: “00 00 00 34”,次版本号转化为十进制为0,主版本号转化为十进制52,在 Oracle 官网中查询序号52对应的JDK版本为1.8,所以编译该源代码文件的 Java 版本为1.8.0

(3)常量池(Constant Pool接着主版本号之后的字节是常量池入口。常量池中存储两种类型常量: 字面量和符号运用。字面量接近于 Java 语言层面的常量概念,如文本字符串、被声明为 final 的常量值等,符号引用则属于编译原理方面的概念:包括类和接口的全局限定名、字段的名称和描述符、方法的名称和描述符。常量池整体上分为两部分: 常量池计数器常量池数据区

image-20230313155110042

image-20230313160316456

常量池中每一项常量都是一个表,最初常量表中共有 11 种结构各不相同的表结构数据,截至JDK13,常量表中分别有 17 种不同类型的常量。17 种常量类型所代表的具体含义如下图

image-20230313162057731

这 17 类表都有一个共同的特点,表结构起始的第一位是个u1类型的标志位),代表着当前常量属于哪种常量类型。

具体的结构以CONSTANT_Utf8_info为例,它的结构如下所示:

首先第一个字节tag,由于它的类型是CONSTANT_Utf8_info也就是字符串,所以值为 01(十六进制,接下来两个字节标识该字符串的长度length,然后length个字节为这个字符串具体的值。

image-20230313163114283

其他的cp_info结构与上面的大同小异,使用javap -verbose命令进行查看:

image-20230313163749313


(4)访问标志(access_flag) 常量池结束之后的两个字节,描述了该 Class 是类还是接口,以及是否被 PublicAbstractFinal 等修饰符修饰,VM并没有穷举所有的访问标志,而是使用按位或操作来进行描述的,比如某个类的修饰符为Public Final,则对应的访问修饰符的值为ACC_PUBLIC | ACC_FINAL,即0x0001|0x0010=0x0011

image-20230313164005017


(5)类索引(this_class):u2类型的数据,描述的是当前类的全限定名。这两个字节保存的值为常量池中的索引值,根据索引值就能在常量池中找到这个类的全限定名。

(6)父类索引(super_class):u2类型的数据,描述这个类的父类的全限定名。Java 语言不允许多重继承,所以父类索引只有一个,这两个字节保存的值也是在常量池中的索引值,根据索引值就能在常量池中找到这个类的父类的全限定名。

(7)接口索引(interfaces):u2 类型的数据的集合,描述这个类的接口计数器,即: 当前类或父类实现的接口数量。紧接着的n个字节是所有的接口名称的字符串常量在常量池的索引值。

(8)字段表(field) 字段表用于描述类和接口中声明的变量,包含类级别的变量以及实例变量,但是不包含方法内部声明的 局部变量。字段表也分为两部分,第一部分是两个字节,描述字段个数,第二部分是每个字段的详细信息 field_info。字段表结构如下所示:

img

其中字段的访问标志查表 2,002 对应为 Private,通过索引下标在常量池分别得到的字段名为: numberA,描述符为: I(在 JVM 中的 I 代表 Java 中的 int)。综上,就可以唯一确定出类别。 JavaCodeCompilerDemo 声明的变量为: private int numberA

(9)方法表(method) 字段表结束后为方法表,方法表也是由两部分组成,第一部分为两个字节描述方法的个数,第二个部分为每个方法的详细信息。方法的详细信息包括:方法的访问标志、方法名、方法的描述符以及方法的属性:

img

最后一个方法属性较为复杂:

  1. Code: 源代码对应的 JVM 指令操作码,我们在字节码增强的时候重点操作的就是这个部分。
  2. LineNumberTable: 型号表,将 Code 区的操作码和源代码的行号对应,Debug 时会起到作用(即: 当源代码向下走一行,相应的需要走几个 JVM 指令操作码)。
  3. LocalVariableTable: 本地变量表,包含 this 和局部变量,之所以可以在每一个非 static 的方法内部都可以调用到 this,是因为 JVM 将 this 作为每个方法的第一个参数隐式进行传入。

img

(10)附加属性表(additional_attribute_table) 字节码的最后一部分,存放了在文件中类或接口所定义的属性的基本信息。

字节码操作

上图中,Code部分的编号是 0 ~ 10,就是 .java 源文件的方法源代码编译后让 JVM 真正执行的操作码。为了帮助人们理解,反编译后看到的是十六进制操作码所对应的助记符,十六进制值操作码和助记符的对应关系,以及每个操作码的具体作用可以查看Oracle官网,在需要的时候查阅即可。比如上图 的助记符为 iconst_2,作用是将 int 值2压入操作数栈中。以此类推,对 0 ~ 10 的助记符理解后就是整个**sum()**方法的操作数码实现。

操作数栈和字节码

JVM的指令集是基于而不是寄存器,基于栈可以具备很好的跨平台性(因为寄存器指令集往往和硬件挂钩),但缺点在于,要完成同样的操作,基于栈的实现需要更多指令才能完成(因为栈只是一个FILO结构,需要频繁压栈出栈)。另外,由于栈是在内存实现的,而寄存器是在CPU的高速缓存区,相较而言,基于栈的速度要慢很多,这也是为了跨平台性而做出的牺牲。

查看字节码工具

每次查看反编译后的字节码都使用javap命令的话,非常繁琐。这里推荐一个Idea插件:jclasslib。代码编译后在菜单栏”view”中选择”Show Bytecode With jclasslib”,可以很直观地看到当前字节码文件的类信息、常量池、方法区等信息。

image-20230313171427014

另外提一嘴,IDEA插件下载加速:https://plugins.jetbrains.com/

image-20230313171556539

安装后重启,先build项目

image-20230313172327943

image-20230313172511913

字节码增强

字节码增强技术就是一类对现有字节码进行修改或者动态生成全新字节码文件的技术。下面介绍以下几个概念

图16 字节码增强技术

ASM

ASM是一个通用的Java字节码操作和分析框架,ASM的应用场景有AOP(CGLIB就是基于ASM)、热部署、修改其他jar包中的类等。ASM是在指令层次上操作字节码的,它可以直接生产 .class字节码文件,也可以在类被加载入JVM之前动态修改类行为。

Javassist

Javassist框架强调从源代码层次操作字节码。其主要优点在于简单快速. 直接使用 java 编码的形式, 而不需要了解虚拟机指令, 就能动态改变类的结构, 或者动态生成类

AspectJ

AspectJ 是一个基于 Java 语言的AOP框架

AOP的实现方式有两种:

  1. AOP框架在编译阶段,就对目标类进行修改,得到的class文件已经是被修改过的。生成静态的AOP代理类(生成*.class文件已经被改掉了,需要使用特定的编译器)。以AspectJ为代表 —— 静态AOP框架。
  2. AOP框架在运行阶段,动态生成AOP代理(在内存中动态地生成AOP代理类),以实现对目标对象的增强。它不需要特殊的编译器。以Spring AOP为代表。—— 动态AOP框架。

动态AOP框架不需要在编译时对目标类进行增强,而是运行时生成目标类的代理类,该代理类要么与目标类实现相同的接口,要么是目标类的子类——总之,代理类的实例可作为目标类的实例来使用。一般来说,编译时增强的 AOP 框架在性能上更有优势——因为运行时动态增强的 AOP 框架需要每次运行时都进行动态增强

Instrumentation(Agent)

开发者可以构建一个独立于应用程序的代理程序(Agent),用来监测和协助运行在 JVM 上的程序,甚至能够替换和修改某些类的定义

小结

字节码增强,可以理解为AOP增强,只不过方式不同而已。