字节码/class文件
什么是字节码?JVM可以理解的代码(即扩展名为 .class
的文件),源代码通过编译器编译为字节码 ,再通过类加载子系统加载到JVM中运行。比如 Kotlin也是基于JVM的编程语言。class文件就是字节码文件。
为什么要学习字节码文件
学习字节码文件可以帮助你更好地理解类加载的整个过程,区分符号引用与直接引用,弄明白方法调用的过程,多态的实现原理。字节码文件也与注解,异常的实现原理息息相关。字节码文件是基础的基础。
class文件特点
class文件/字节码文件本质上是一个以8个字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑的排列在class文件中,中间没有添加任何分隔符。jvm根据其特定的规则解析该二进制数据,从而得到相关信息。
RednaxelaFX:
用字节码来解释性能问题很抱歉也是比较不靠谱的。
字节码用于解释 "语义问题" 很靠谱,但看字节码是看不出性能问题的------超过它的抽象层次了
class文件的两种数据类型
只有两种数据类型:"无符号数"和"表"
- 无符号数:以u1/u2/u4/u8代表1/2/4/8个字节数的无符号数,可以用来描述数字、索引引用、数量值、字符串值
- 表:由无符号数和其它表构成,命名以"_info"结尾。
整个class文件也是一张表。
class文件结构
0、魔数:CAFEBABE
文件开头的4个字节 ("cafe babe")称之为 魔数 ,唯有以"cafe babe"
开头的class文件方可被虚拟机所接受,这4个字节就是字节码文件的身份识别。
1、两个版本号
minor:次要版本;major:主要版本
它们各占两个字节
2、常量池(重要)
常量池占一个Class文件的很大一部分空间
常量池的入口有一个u2,代表个数(constant_pool_count)
Class文件结构中只有常量池的容量计数是从1开始
常量池用于存储字面量和符号引用 ,字面量包括字符串"abd"
,和用final修饰的字段。
符号引用包括:
- 类和接口的全限定名
- 字段的简单名称和描述符
- 方法的简单名称和描述符
比如:一个方法在方法表,存储的是索引,指向常量池内的方法名称/描述符
以及可能不太眼熟的:
- 被模块导出或者开放的包
- 动态调用点和动态常量
- 方法句柄和方法类型
截至JDK13,常量表中分别有17种不同类型的常量。
类的符号引用:CONSTANT_Class_info
CONSTANT_Class_info还是一个索引,它的结构是:
- 一个tag,u1,标识当前常量类型为
CONSTANT_Class_info
- 一个name_index,u2,指向常量池中一个
CONSTANT_Utf8_info
类型常量,此常量代表了这个类(或者接口)的全限定名
笼统的符号引用是:本身可以是任何形式的字面量,它的实现取决于JVM,只要能定位到目标即可。
所以,类的符号引用在HotSpot的实现就是全类名
Utf8字符串:CONSTANT_Utf8_info
这就是存储字符串字面值的。它的结构是:
- 一个tag,u1,所有常量池的类型都有一个tag用于标识自己的类型,后续不再赘述
- 一个length,u2,代表当前字符串的长度
- bytes,u1的集合,个数取决于length
3、类访问标记
包括访问修饰符,是否接口,是否抽象,是否枚举
4、当前类,父类
java
u2 this_class; // 当前类
u2 super_class; // 父类
它们都指向一个类型为CONSTANT_Class_info的类描述符常量,从而找到定义在CONSTANT_Utf8_info类型的常量中的全限定名字符串。
可以理解为:this_class -> CONSTANT_Class_info -> CONSTANT_Utf8_info -> 全类名
有这个链条关系,就可以知道当前类和父类了,「5、接口索引集合」同理
如果没看懂为什么就再去看一遍「2、常量池」吧
5、接口索引集合
java
u2的集合 interfaces; // 接口索引集合
接口索引集合存放着当前类实现的所有接口,如果当前是接口,则是所有继承的接口
它的查找过程和类/父类一样。
6、字段表集合
字段包括了类变量(static修饰)和普通变量(非static修饰),类变量由access_flags的ACC_STATIC标识。
字段表结构:
1、access_flags:类似「3、类访问标记」,以及一些字段独有的比如:volatile,transient
2、name_index和descriptor_index,是对常量池项的引用 ,分别代表着字段的简单名称 以及字段的描述符
再强调一下,这两个是索引,真正的符号引用/字面量在常量池,因此是u2是定长的
字段的简单名称:纯的字段名。int m字段的简单名称就是"m"
字段描述符:描述字段的数据类型
如一个定义为"java.lang.String"类型的二维数组将被记录成"[[Ljava/lang/String;" 一个整型数组"int[]"将被记录成"[I"。
3、attribute_info:额外信息,详见『8、属性表集合』
字段表集合中不会列出从父类或者父接口中继承而来的字段,但有可能出现原本Java代码之中不存在的字段,譬如在内部类中为了保持对外部类的访问性,编译器就会自动添加指向外部类实例的字段。另外,在Java语言中字段是无法重载的,两个字段的数据类型、修饰符不管是否相同,都必须使用不一样的名称,但是对于Class文件格式来讲,只要两个字段的描述符不是完全相同,那字段重名就是合法的
7、方法表集合
method_info和field_info几乎完全一致。
仅仅是access_flag
略有不同。比如:
- 方法独有:是否synchronized,native;
- 而字段独有:volatile,transient
name_index和descriptor_index的引用也分别代表了方法的简单名称 以及方法的描述符
方法的简单名称:纯的方法名。inc()方法就是"inc"
方法描述符:包括 方法的参数列表(包括数量、类型以及顺序)和返回值
方法int indexOf(char[]source,int sourceOffset,int sourceCount,char[]target,int targetOffset,int targetCount,int fromIndex)的描述符为 "([CII[CIII)I" 。
实际的方法代码存储在哪里?
方法原本也是一段java代码,因此会被编译成字节码指令,存储在: 方法属性表集合中一个名为"Code"的属性里面。
详见『8、属性表集合』
8、属性表集合🚩
属性表集合用于描述某些场景专有的信息
像前文提到的字段,方法都依赖属性表集合
- 方法表依赖Code 存储实际方法的字节码指令
- 字段表依赖ConstantValue 存储final定义的常量值
声明
final static int m=123;
那就可能会存在一项名称为ConstantValue的属性,其值指向常量123。
属性表还包括:
- Exceptions:方法可能抛出的所有异常
- Inner classes:内部类
还有非常多,一共接近30个,详见《深入理解Java虚拟机 第三版》P318,这里暂时只看两个:
ConstantValue:常量值索引
static字段专属。
static字段的初始化有两种方法:
- 类构造器< clinit >()方法中
- 使用ConstantValue属性
目前:被static final修饰,且为基本数据类型或者java.lang.String的,用ConstantValue;其余用< clinit >()
ConstantValue是常量池索引,常量池只有基本数据类型和String,因此这看似是一种ConstantValue对User的束缚,实际ConstantValue自己是被常量池所束缚
Code:方法体
Code用于描述一个方法。
less
// Java源程序编译后生成的字节码指令
u4 code length // 字节码指令的长度
u1 code // 字节码指令 有code length 个 code
// 一个u1 8位,可以表达0~255 共256条指令,目前Java已经定义了两百多条指令。
// 可见 《深入理解Java虚拟机 第三版》附录C"虚拟机字节码指令表
元数据
// 一个Code还包括很多其他元数据 包括栈最大深度 异常表等信息
虽然code length是u4,但《Java虚拟机规范》中明确限制了一个方法不允许超过65535条字节码指令。
Code内部的异常表
异常表是Code的一部分。异常表不是必须的。是实现try catch finally代码块的核心。
在Code中,异常表紧跟code:
java
u2 exception_table_length // 一个u2 异常表长度
exception_info exception_table // 异常表信息
对于一个exception_info是这样的:
arduino
u2 start_pc // 左闭右开的 start_pc ~ end_pc
u2 end_pc
u2 handler_pc // 跳转行
u2 catch_type // 出现了类型为catch_type或者其子类的异常
代表在一个「左闭右开」的 start_pc ~ end_pc 行之间,一旦出现了类型为catch_type或者其子类的异常,就转而到第handler_pc行继续处理。
对于finally来说 catch_type是any
javap反编译出来的异常表结果:
python
Exception table:
from to target type
0 5 10 Class java/lang/Exception
0 5 21 any
10 16 21 any
Exceptions:方法抛出的异常
更确切地说:Exceptions是列举出方法中可能抛出的受查异常(Checked Excepitons),也就是方法描述时在throws关键字后面列举的异常。
Exceptions和异常表是不同的
- Exceptions用于描述方法的throws关键字后面列举的异常
- 异常表是try...代码块
因此,Exceptions与Code属性是平级的。
结构和其他的差不多,就不再列举了
InnerClasses:内部类集合
如果一个类有内部类,就会有这个属性。
用若干个inner_classes_info表示。
Signature:标签
任何类、接口、初始化方法或成员的泛型签名如果包含了类型变量(Type Variable)或参数化类型(ParameterizedType),则Signature属性会为它记录泛型签名信息。
Java的反射API能够获取的泛型类型,最终的数据来源也是这个属性
Annotations:运行时注解相关
RuntimeVisibleAnnotations:运行时可见注解
使用反射API获取注解时,就来自属性表的注解属性
一个注解对应一个annotation属性
一个annotation属性包含注解的全类名,以及它的所有属性,以键值对的形式存储,可以通过反射获取
字节码指令集
Java虚拟机的指令由一个字节长度的Opcode操作码,加若干Operand操作数,Java虚拟机采用面向操作数栈,因此大多数指令都不包含操作数。
基本工作模型:
java
do {
自动计算PC寄存器的值加1;
根据PC寄存器指示的位置,从字节码流中取出操作码;
if (字节码存在操作数) 从字节码流中取出操作数;
执行操作码所定义的操作;
} while (字节码流长度 > 0);
具体的字节码指令就不一一列举了,网上很方便能够查到
javap:查看方法的字节码指令
JDK内置了很多有用的小工具,javap是其中一个。javap可以帮助我们看到字节码指令
字节码指令可以帮助我们更好地理解"语义"。在此简单介绍javap的使用:
javap基本使用
首先在「idea整合javap」,然后只需要右键类,External Tools,javap即可。
也可以在控制台直接拼下面的命令
javap参数
- javap -l :会输出行号和本地变量表信息;
- javap -c :会对当前class字节码进行反编译生成汇编代码;
- javap -v: class字节码文件中除了包-c参数包含的内容外,还会输出行号、局部变量表信息、常量池等信息;
最全的:javap -verbose xxx.class
参考文献
周志明 《深入理解Java虚拟机 第三版》