跳到主要内容

02、JVM 实战 - 字节码指令篇

一、Class文件结构

Class文件的结构并不是一成不变的,随着Java虚拟机的不断发展,总是不可避免地会对Class文件结构做出一些调整,但是其基本结构和框架是非常稳定的。

  • 魔数
  • Class文件版本
  • 常量池
  • 访问标识(或标志)
  • 类索引,父类索引,接口索引集合
  • 字段表集合
  • 方法表集合
  • 属性表集合

 

1、魔数

  • 每个 Class 文件的头 4 个字节称为“魔数(Magic Number)”。
  • 它的唯一作用是确定这个文件是否为一个能被虚拟机所接受的有效、合法的class文件。
  • class文件的魔数值固定为OxCAFEBABE,不会改变。

2、class文件版本

排列在magic后的第5个和第6个字节所代表的含义就是编译的副版本号minor_version,而第7个和第8个字节就是编译的主版本号major_version。

验证字节码文件的主版本号和次版本号同样也是格式验证的任务之一,因为如果是高版本的JDK编译的字节码文件,自然不能在低版本的JVM中运行,否则JVM会抛出java.lang.UnsupportedClassVersionError异常。

3、常量池

1、 constant_pool_count(常量池计数器);

constant_pool_count的值等于常量池表中的成员数加1。常量池表的索引值只有在大于0且小于constant_pool_count时才会认为是有效的,对于long和 double类型有例外情况。

2、 constant_pool[](常量池);

constant_pool是一种表结构,以 1 ~ constant_pool_count - 1为索引。

它包含class文件结构及其子结构中引用的所有字符串常量、类或接口名、字段名和其他常量。常量池中的每一项都具备相同的特征,第1个字节作为类型标记,用于确定该项的格式,这个字节称为tag byte (标记字节、标签字节)。

4、访问标志

常量池后面就是访问标志,用两个字节来表示,其标识了类或者接口的访问信息,比如:该Class文件是类还是接口,是否被定义成public,是否是abstract,如果是类,是否被声明成final等等。各种访问标志如下所示:

 

5、类索引、父类索引、接口索引集合

  • 访问标志后的两个字节就是类索引;
  • 类索引后的两个字节就是父类索引;
  • 父类索引后的两个字节则是接口索引计数器。
  • 通过这三项,就可以确定了这个类的继承关系了。

6、字段表集合

字段表用来描述类或者接口中声明的变量。这里的字段包含了类级别变量以及实例变量,但是不包括方法内部声明的局部变量。

fields:指向常量池索引集合,它完整描述了每个字段。

fields_count (字段计数器)

fields_count的值表示当前class文件fields表的成员个数。fields表中每个成员都是一个field_info结构,用于表示该类或接口所声明的类字段或者实例字段。

fields [](字段表)

fields表中的每个成员都必须是一个fields_info结构的数据项,用 于表示当前类或接口中某个字段的完整描述。fields表描述当前类或接口声明的所有字段,但不包括从父类或父接口继承的那些字段。

字段表访问标志:

 

7、方法表集合

methods:指向常量池索引集合,它完整描述了每个方法的签名,如果这个方法不是抽象的或者不是native的,那么字节码中会体现出来。

  • methods_count (方法计数器)
    methods_count的值表示当前class文件methods表的成员个数。methods 表中每个成员都是一个method_info结构。
  • methods [](方法表)
    methods表中的每个成员都必须是一个method_info结构,用于表示当前类或接口中某个方法的完整描述。如果某个method_info结构的access_ flags项既没有设置 ACC_NATIVE 标志也没有设置ACC_ABSTRACT标志,那么该 结构中也应包含实现这个方法所用的Java虚拟机指令。
    method_info 结构可以表示类和接口中定义的所有方法,包括实例方法、类方法、 实例初始化方法和类或接口初始化方法。methods表只描述当前类或接口中声明的方法,不包括从父类或父接口继承的方法。

方法表访问标志:

 

8、属性表集合

属性表不仅在方法表有用到,字段表和Class文件中也会用得到。

attributes:不同值的集合,它提供了额外的关于这个类的信息,包括任何带有RetentionPolicy.CLASS 或者RetentionPolicy.RUNTIME的注解。

  • attributes_count (属性计数器)
    attributes_count的值表示当前class文件属性表的成员个数。属性表中每一项都是一个attribute_info结构。
  • attributes [](属性表)
    属性表的每个项的值必须是attribute_info结构。

二、字节码指令集

JVM中字节码指令集按用途大致可分为9类:

1、 加载与存储指令;
2、 算术指令;
3、 类型转换指令;
4、 对象的创建与访问指令;
5、 方法调用与返回指令;
6、 操作数栈管理指令;
7、 控制转移指令;
8、 异常处理指令;
9、 同步控制指令;

在做值相关操作时

一个指令,可以从局部变量表、常量池、堆中对象、方法调用、系统调用中等取得数据,这些数据(可能是值,可能是对象的引用)被压入操作数栈。

一个指令,也可以从操作数栈中取出一到多个值(pop多次),完成赋值、加减乘除、方法传参、系统调用等等操作。

1、方法调用指令

  • invokevirtual 指令:用于调用对象的实例方法,根据对象的实际类型进行分派(虚方法分派),支持多态。这也是Java语言中最常见的方法分派方式。
  • invokeinterface 指令:用于调用接口方法,它会在运行时搜索由特定对象所实现的这个接口方法,并找出适合的方法进行调用。
  • invokespecial 指令:用于调用一些需要特殊处理的实例方法,包括实例初始化方法(构造器)、私有方法和父类方法。这些方法都是静态类型绑定的,不会在调用时进行动态派发。
  • invokestatic 指令:用于调用命名类中的类方法(static方法)。这是静态绑定的。
  • invokedynamic 指令:调用动态绑定的方法,这个是JDK 1.7后新加入的指令。用于在运行时动态解析出调用点限定符所引用的方法,并执行该方法。前面4条调用指令的分派逻辑都固化在java 虚拟机内部,而invokedynamic指令的分派逻辑是由用户所设定的引导方法决定的。l