什么是字节码
- .java源码经过javac编译生成的二进制文件,称为字节码文件(.class文件),其中包含了大量的字节码指令。字节码的用途只有一个,就是在运行时通过这些字节码指令告诉jvm该执行什么样的操作。
- jvm通过字节码保证平台无关性。
- java不是唯一生成class文件的语言。
无关性
- 与平台无关:Oracle公司及其他虚拟机发行商发布过许多可以运行在各种不同硬件平台和操作系统上的java虚拟机,这些平台都统一支持一种与平台无关的程序存储格式——字节码,从而实现了“一次编写,到处运行”。字节码正是平台无关性的基石。
- 与语言无关:java虚拟机不与任何程序语言绑定(包括java语言),它只与“Class文件”这种特定的二进制文件有关联。实现语言无关性的基础仍然是虚拟机和字节码存储格式。
不同的语言拥有不同的编译器, 只要这些语言按照编译器所生产的字节码符合JVM的规范,那JVM可以完成对他的加载,执行和处理。 JVM读取字节码,将字节码文件中的一系列指令转换成操作系统能识别的指令,完成各种操作
字节码组成结构
class文件时一组以8个子节为基础单位的二进制流,各个数据项目紧凑排列,中间没有添加任何分隔符。文件中采用一种类似C语言结构体的结构存储数据,只有两类数据类型:无符号数和表。
- 无符号数:基本的数据类型,以u1,u2,u4,u8来代表1个字节,2个字节,4个字节,8个字节的无符号数,可以用来描述数字,索引引用,数量值。
- 表:有多个无符号数或其他表构成的符合数据类型,所有表的命名习惯于“_info”结尾。
class文件的结构
- 魔数
- 文件版本
- 常量池
- 访问标志
- 类索引,父类索引,接口索引集合
- 字段表集合
- 方法表集合
- 属性表
1.魔数
每个Class文件的头四个字节被称为魔数,它唯一的作用是确实能够这个文件是否为一个能被虚拟机接受的Class文件。0xCAFEBABE是java字节码文件的魔数。
2.文件版本
紧接着魔数的四个字节存储的是Class文件的版本号:第5和第6个字节是此版本号,第7和第8字节是主版本号。可以向下兼容。
3.常量池
字节码中所有基础的数据都保存在常量池中,其他信息都是对常量池的引用和关联。
常量池中存放两大类常量:
- 字面量:文本字符串,被声明为final的常量值。
- 符号引用:
- 被模块导出或者开放的包
- 类和接口的全限定名
- 字段的名称和描述符
- 方法的名称和描述符
- 方法句柄和方法类型
- 动态调用点和动态常量
常量池的特点
- 常量池是与其他项目关联最多的数据,也是占用Class文件空间最大的数据项目之一。
- 常量中常量的数目不固定,在常量池的入口放置一项u2类型的数据,表示常量池用量体积。
- 常量池中每一项常量都是一个表。截止JDK13,常量池中分别由17种不用类型的常量。
表结构起始第一位是个u1类型的标志位,代表当前常量属于哪种常量类型。
这17种常量类型各自有着完全独立的数据结构,两两之间没有什么联系。
为什么java程序中的变量或方法名的长度不能超过64KB?
因为Class文件中方法,字段等都需要引用CONSTANT_Utf8_info型常量来描述的,所以CONSTANT_Utf8_info型常量的最大长度也就是java中方法和字段的最大长度,这里的最大长度是2个字节,即最大值是65535。所以这些名字的最大长度是64KB。
4.访问标志
常量池结束后紧跟着的2个字节代表访问标志,用于识别一些类或接口的访问信息。
包括:
- 这个Class是类还是接口
- 是否定义为public类
- 是否定义为abstract类
- 如果是类,是否声明为final
5.类索引,父类索引,接口索引集合
类索引和父类索引都是一个u2类型的数据,而接口索引集合是一组u2类型的数据集合。
Class文件中由这三项数据来确定该类型的继承关系。
- 类索引:用来确实这个类的全限定名
- 父类索引:用于确定这个类的父类的全限定名。所有java类的父类索引都不为0(除了java.lang.Object,因为都会继承它),并且只有一个。
- 接口索引集合:用来描述这个类实现了哪些接口。
类索引是如何找到类的全限定名的?
类索引和父类索引都用两个u2类型的索引值表示,它们各自指向一个CONSTANT_Class_info的类描述常量,再通过CONSTANT_Class_info类的常量中的索引值,找到定义在CONSTANT_Utf8_info类型的常量中的全限定字符串。
(下面会说什么是全限定名)
接口索引是如何找到接口的全限定名的?
接口索引集合入口第一项就是一个u2类型的数据,是一个接口计数器,代表实现接口数量。如果该类型没有实现任何接口,则计数器的值为0。如果实现了n个接口,则后面会有n个u2类型的索引值,寻找方法和上面一样。
6.字段表集合
字段表用于描述接口或类中声明的实例, 包括:
- 类级变量
- 实例级变量
但不包括方法内部声明的变量。
字段表集合特点
- 不会列出从父类或者父接口继承而来的字段。
- 可能会出现原本java代码中不存在的字段,例如,编译器会在内部类中添加外部类实例的字段,为了保持对外部类的访问性。
- Class文件中两个字段可以重名,只要描述符不完全相同。
字段表结构
- access_flags:字段访问标志,表示字段的修饰符(public,private,static,final…)
- name_index:字段的简单名,是对常量池的引用。
- descriptor_index:字段和方法的描述符,也是对常量池的引用。
简单名,全限定名和描述符
- 简单名:没有类型和参数修饰的方法或者字段名称。
- 全限定名:把类名中的“.”,换成“/”,例如:“org/fenixsoft/clazz/TestClass”。使用时一般个“;”表限定名结束。
- 描述符:字段和方法都有自己的描述符
- 字段:描述字段的数据类型。
- 方法:描述方法的参数列表(包括数量,类型,顺序)和返回值。
7.方法表集合
用于存储类中的方法,所有的方法以二维表的形式存储,每张表表示一个方法的定义。方法里的java代码经过javac编译器编译成字节码指令,存放在方法属性表集合中一个名为“Code”的属性里。
方法表的结构和字段表一样。
方法表特点
- 如果父类方法在子类中没有被重写,方法表集合中就不会有来自父类的方法信息。
- 可能会出现有编译器自动添加的方法,例如类构造器。
- 需要重载一个方法时,除了需要与原方法具有相同的简单名称之外,还需要有一个不同的特征签名。特征签名是指这个方法的各个参数在常量池中的字段符号引用的集合,因为返回值不在特征签名之中,所以,Java语言不能仅仅依靠返回值来对一个方法进行重载。而在Class文件格式中就是可以的,如果两个方法有相同的名称和特征签名,但返回值不同,那么也是可以共存于一个Class文件中的。
8.属性表集合
Class文件,字段表,方法表都可以携带自己的属性表集合,用以描述某些场景专有的信息。
Code属性
java程序方法体里的代码经过javac编译器处理后,最终变为字节码指令存储在Code属性内。
Code属性表结构
Code属性是Class文件中最重要的一个属性,一个Java程序中的信息可以分为两部分:
- 元数据:包括类,字段,方法定义等
- 代码:方法体里的java代码
那么,在整个Class文件中,Code属性用于描述代码,所有其他的数据都用于描述元数据。