从零开始用 Rust 打造一个玩具级别 Java 虚拟机 (二) Class字节码解析

上一章 咱们讲解了类的加载,后面咱重新写了代码,咱打算用 未来可能很火的Rust 来完成这个项目。

.Class文件介绍

JAVA中每个class 文件就是一个类,类名和文件名相同, 按照Java虚拟机规范其中对类名有了严格的规定。Java虚拟机 对类的加载方式则较为宽松 类文件可以是从.JAR .ZIP 文件中读取加载class文件,甚至可以从网络上加载。

Java 加载Class 流程:

Created with Raphaël 2.2.0 前端编译器编译成.Class字节码文件 虚拟机装载.Class文件 虚拟机,解释编译为 对应平台上的机器指令运行
什么是Java 字节码文件 .Class?

按照《Java虚拟机规范 JavaSE7版》的描述来看,任何编程语言的编译结果满足并包含Java虚拟机的内部指令集、符号表以及一些其他的辅助信息,它就是一个有效的字节码文件,就能够被虚拟机所识别并装载运行

什么是字节码、机器码、本地代码?

字节码是指平常所了解的 .class 文件,Java 代码通过 javac 命令编译成字节码

机器码和本地代码都是指机器可以直接识别运行的代码,也就是机器指令

字节码是不能直接运行的,需要经过 JVM 解释或编译成机器码才能运行

此时你要问了,为什么 Java 不直接编译成机器码,这样不是更快吗?

  1. 机器码是与平台相关的,也就是操作系统相关,不同操作系统能识别的机器码不同,如果编译成机器码那岂不是和 C、C++差不多了,不能跨平台,Java 就没有那响亮的口号 “一次编译,到处运行”;

  2. 之所以不一次性全部编译,是因为有一些代码只运行一次,没必要编译,直接解释运行就可以。而那些“热点”代码,反复解释执行肯定很慢,JVM在运行程序的过程中不断优化,用JIT编译器编译那些热点代码,让他们不用每次都逐句解释执行;

  3. 还有一方面的原因是后文讲解的解释器与编译器共存的原因。

什么是 JIT ?

为了提高热点代码的执行效率,在运行时,虚拟机将会把这些代码编译成与本地平台相关的机器码,并进行各种层次的优化,完成这个任务的编译器称为即时编译器(Just In Time Compiler),简称 JIT 编译器

什么是编译和解释?

编译器:把源程序的每一条语句都编译成机器语言,并保存成二进制文件,这样运行时计算机可以直接以机器语言来运行此程序,速度很快;

解释器:只在执行程序时,才一条一条的解释成机器语言给计算机来执行,所以运行速度是不如编译后的程序运行的快的;

通过javac命令将 Java 程序的源代码编译成 Java 字节码,即我们常说的 class 文件。这是我们通常意义上理解的编译。

字节码并不是机器语言,要想让机器能够执行,还需要把字节码翻译成机器指令。这个过程是Java 虚拟机做的,这个过程也叫编译。是更深层次的编译。(实际上就是解释,引入 JIT 之后也存在编译)

此时又有疑惑了,Java不是解释执行的吗?

没错,Java 需要将字节码逐条翻译成对应的机器指令并且执行,这就是传统的 JVM 的解释器的功能,正是由于解释器逐条翻译并执行这个过程的效率低,引入了 JIT 即时编译技术。

必须指出的是,不管是解释执行,还是编译执行,最终执行的代码单元都是可直接在真实机器上运行的机器码,或称为本地代码

从零开始用 Rust 打造一个玩具级别 Java 虚拟机 (二) Class字节码解析_第1张图片

字节码结构组成?

.Class字节码组成有不通长度的字节码(Byte)为单位通过拼凑而成,Java虚拟机规范定义了u1、u2和u4三种数据类型来表示1、 2和4字节无符号整数, 并且以大端(big-endian)方式存储。
从零开始用 Rust 打造一个玩具级别 Java 虚拟机 (二) Class字节码解析_第2张图片

类型 名称 数量
u4 magic(魔数) 用来标识让虚拟机识别这个是Class文件 固定0xCAFEBABE 1
u2 minor_version 编译当前Class的jdk次版本号,高版本jdk编译文件不能运行在低版本JVM上 反之可以 1
u2 major_version 编译当前Class的jdk主版本号 1
u2 constant_pool_count 标识常量池 包含常量个数 1
cp_info constant_pool 常量池 constant_pool_count -1
u2 access_flags 访问标志,某个类或者接口的访问权限 public final abstract 等都是访问标志 1
u2 this_class 通过索引指向常量池中类的全限定名 1
u2 suprer_class 所有类默认超类为Object,并且一个类只能有一个超类 1
u2 interfaces_count 继承接口的个数,从0开始 1
u2 interfaces 接口表,接口名全限定名在常量池中的索引 interfaces_count
u2 fields_count 类变量和实例变量的总和 1
field_info fields_count个field_info结构体组成的 记录实例变量和类变量的一些信息 比如 是否被 public final 修饰等 fields_count
u2 methods_count 记录当前类的方法有多少个 1
method_info methods_count个method_info结构体组成的 方法表 就是记录一些 方法的 修饰符 public final 信息 methods_count
u2 attribute_count 表示attribute_info个数 1
attribute_info attribute_count 个 attribute_info组成指向 attribute_info包含索引指向常量池的CONSTANT_Utf8_info 也就是一个utf8 字符串 attribute_count

ps:u2 = 2byte ,u4 = 4byte ,u8 = 8byte (无符号整数)

魔数

从零开始用 Rust 打造一个玩具级别 Java 虚拟机 (二) Class字节码解析_第3张图片
魔数很简单,其实就是你给你的名字象征的七个表示在前几个字节,java 文件字节码以 0xCAFEBABE 开头 那么 虚拟机就判断 这时个.Class 文件 能被虚拟机加载,其实计算机很多格式的文件 比如.AVI .JPG .MOV 这样的文件开头前几个字节都是固定魔数。而 0xCAFEBABE 对应ASCII码 则是 cafe babe,关于Java 起名的由来是 取自某座盛产咖啡的岛为命名的。

主版本号次版本号

主版本号 就你编译是编译JDK的版本号, 主要是让虚拟机能判断 如果是低版本的虚拟机,没法 运行高版本的 JDK编译的 .Class文件的。

JDK版本号 Class版本号 16进制
1.1 45.0 00 00 00 2D
1.2 46.0 00 00 00 2E
1.3 47.0 00 00 00 2F
1.4 48.0 00 00 00 30
1.5 49.0 00 00 00 31
1.6 50.0 00 00 00 32
1.7 51.0 00 00 00 33
1.8 52.0 00 00 00 34

从零开始用 Rust 打造一个玩具级别 Java 虚拟机 (二) Class字节码解析_第4张图片
可以看到 00 34 对应JDK版本号是1.8。

常量池的数据结构:

紧跟在 主版本号,后面的是 常量池的数量,值的注意的一点是 常量池的索引是从1开始的,所以实际上 是常量数 constant_pool_count - 1, 当某个 属性指向常量池0号索引 表示不引用当前常量池的内容.

常量池里面包含了很多的数据结构,是这些数据结构的集合,可以用来存储 字符串常量值 类名 接口名 字段和方法名等等。常量池按结构可以分为两大常量:字面量(Literal) 和 符号引用(Symbolic References)。 字面量 如文本字符串、整数浮点数值等。
而符号引用 属于编译原理的概念、包括三类常量:

  • 类和接口的全限定名
  • 字段的名称和描述符
  • 方法的名称和描述符

//class 读取后的完整结构
#[derive(Debug, Clone)]
pub struct ClassFile {
    pub magic: u32, //class文件的魔数
    pub minor_version: u16, //编译此Class文件JDK的次版本号
    pub major_version: u16, //编译此Class文件JDK的主版本号
    pub constant_pool_count: u16,//常量池数量 从 1开始 0保留
    pub constant_pool: Vec<Constant>,//常量池 包含14中结构的数据
    pub access_flags: u16, //访问标志 记录 接口类 方法的 访问标志
    pub this_class: u16,//当前类名的索引 指向常量池一个utf-8字符串
    pub super_class: u16,//父类的索引 除了 java.lang.Object 每个类都应该有对应的父类,指向常量池一个utf-8字符串
    pub interfaces_count: u16,//接口的个数
    pub interfaces: Vec<Constant>,//常量池 utf-8结构 存放 继承接口的类名 从左到右
    pub fields_count: u16,//字段数
    pub fields: Vec<FieldInfo>,//字段表信息 存放变量的 名字描述符和 属性
    pub methods_count: u16,//方法树
    pub methods: Vec<MethodInfo>,//存放方法的名 描述符合属性
    pub attributes_count: u16,//属性数
    pub attributes: Vec<AttributeInfo>,//记录属性表
}

在Java Se7 中包含 14中常量池的的格式,但是 它们之间格式之间 有相似性,都会有一个 tag 表示 常量池的数据类型。和 info 存放实际数据。

type cp_info struct {
	tag uint8
	info[] uint8
}

tag 用来表示14中结构体的编号。
对照上面的表,每次你读常量池时先读1个字节 知道 确定后面需要读取的数据的结构,按对应的读取方法再做读取。

常量池14种结构

全限定名

全限定名和简单名称其实解释 类的名字 比如 java.lang.Object 这个 所有类的父类, 它的全限定名称 就是 java/lang.Object; 仅仅是把 “.” 替换成了 “/” 并且最后加入了一个 “;” 符号。

描述符、简单名称:

什么是描述符呢?简单来说 就是 起了个简化名来表示 方法的参数,类型等,描述符存在常量池里,用的是 改良版的utf-8格式,那么很多小伙伴要有疑问了 为什么好好的名字不用 非要缩略呢,其实就是为了 压缩文件大小,读取更快吗,原来一个 byte 用B 来表述 计算机还是能识别,但是只用了一个字节 .

自段描述符:


使用描述符 一个Object对象被标示成了 Ljava/lang/Object ,一个 一维数组可以表示成[,二维数组被表示成[[ ,java 规定了 数组最大维度 255。

方法描述符

方法描述符(method descriptor)包含0个或多个参数描述符(parameter descriptor) 以及1个返回值描述符(return descriptor)。参数描述符表示表示该方法锁接受的参数类型,如果该方法有返回值的话,那么该返回值描述符则表示该方法的返回值的类型。

举个例子:
Objcet m(int I,double d,Thread t){…}

它 有三个参数, 分别对应的描述符为 int ->I, double ->D, Thread -> Ljava/lang/Thread 返回值为 Objcet -> Ljava/lang/Object

那么 最后转化为描述符为 (IDLjava/lang/Thread;) Ljava/lang/Object

一个有效的 方法描述符,参数数量 控制在 255个以内,对于 实例方法 来说 就是 对象的方法 this.xxx()
实际上 当你调用 对象.xxx() 的时候本质上 就是 将 对象this传入了 这个方法。
所以 实例方法 本身 this 也作为一个参数,传递 this 不是由 方法描述符 来记录的,也就是说 参数 this 的传递是由调用实例方法的 指令来传递的,还有 要注意的一点是 每个double 或 long 占用 2个参数长度。

总结:类型描述符。
①基本类型byte、short、char、int、long、float和double的描述符是单个字母,分别对应B、S、C、I、J、F和D。注意,long的描述符是J而不是L。
②引用类型的描述符是L+类的完全限定名+分号。
③数组类型的描述符是[+数组元素类型描述符。
2)字段描述符就是字段类型的描述符。
3)方法描述符是(分号分隔的参数类型描述符)+返回值类型描述符,其中void返回值由单个字母V表示。

CONSTANT_Utf8_info结构

/**
    -  tag 1
    -  length u2(UTF-8改良版字符串的长度 最长65535)
    -  bytes length(存储的UTF8字符串 用来被存储 方法名、类的全限定名、字段名、方法参数描述符等)
    -  *sample:   | Ljava/lang/String; | args | ([Ljava.lang.String;)V | [[C (二维char数组)
     */
    CONSTANTUtf8Info {
        length:u16,
        bytes:Vec<u8>,
    }
  • tag:1
  • length:用来指明bytes的长度,length 最长 是 65535 所以 变量名、String类型长度、方法名 参数名等 最长 是 65535byte = 64KB
    bytes[] :由length个长度组成的,使用改进的UTF-8 编码个是来存储,与传统的UTF-8编码区别在于,改进过的UTF-8 编码格式从’\u0001’ ~ ‘\u007f’ 之间的字符使用1个字符来表示,从’\u0080’ ~’\u07ff’ 之间的字符使用2个字节来表示;而从’\u0800’ ~’\ufff’之间的字符则与传统的 UTF-8 编码格式一样使用 3个字节来表示。

从零开始用 Rust 打造一个玩具级别 Java 虚拟机 (二) Class字节码解析_第5张图片

CONSTANT_Class_info 结构

指向Utf8_info 表示接口

/**
     -  tag 7
     -  name_index u2(名字,指向utf8)
     -  descriptor_index u2(描述符类型,指向utf8)
     -  *sample: new String();
     -> name_index -> utf8(#name_index)  -> 指向的 是一个类或接口的全限定名 java/lang/String
     */
    CONSTANTClassInfo {
        name_index: u16,
    }
  • tag :7
  • name_index 为对常量池表的一个索引,其索引的对象为
    CONSTANT_Utf8_info结构,是一个 使用改进的UTF-8格式存储的字符串,在这里是用来存放类 或接口的全限定名字符串。
    在 JVM中 包含三种引用类型,分别是:类类型(class type)、数组类型(array type)和 接口类型(interface type),CONSTANT_Class_info 也可用来表示数组类型 、一个char数组类型描述符用 [C 表示

CONSTANT_Fieldref_info、 CONSTANT_Methodref_info 和、CONSTANT_InterfaceMethodref_Info 结构

字段、方法和接口方法结构如下

//字段引用

/**
    -  字段引用信息
    -  tag  9
    -  class_index u2(指向CONSTANT_Class 当前字段所属到的所属的类或接口)
    -  name_and_type_index u2 (指向 CONSTANT_NameAndType 获得当前字段的简单名称和自段描述符)

    - *sample :public class HelloClass {
                private static String hellocclass;//变量名、字段名= hellocclass 变量类型:String 描述符形式就是 Ljava/lang/String;
                public static void main(String[] args) {
                    hellocclass =".CLASS";
                    }
                }
     - > class_index - > CONSTANT_Class(#class_index) -> utf8(CONSTANT_Class.name_index) -> HelloClass 当前字段所在类的类名
        - > name_and_type_index -> CONSTANT_NameAndType(#name_and_type_index) ->  utf8(#CONSTANT_NameAndType.name_index)   ->  hellocclass  ↓
                                                                              ->   utf8(#CONSTANT_NameAndType.#descriptor_index) -> Ljava/lang/String;  ↓
                                                                                                                              ----->  hellocclass:Ljava/lang/String; 字段名:字段的类型
    */
    CONSTFieldrefInfo {
        class_index: u16,
        name_and_type_index: u16,
    }
  • tag: 9
  • class_index : class_index 指向常量池的 CONSTANT_Class_info 结构, 被指向的CONSTANT_Class_info 即可以表示为类、也可以表示为接口,就是最终指向常量池的一个字符串可以是接口名,或者是类名。
  • name_and_type_index:指向常量池的CONSTANT_NameAndType_info 结构,它表示当前字段描述符。

System.out.println() 在常量池里 表示为 CONSTANT_Fieldref_info 结构。2各部分 组成 类名 和 方法的非限定名。
从零开始用 Rust 打造一个玩具级别 Java 虚拟机 (二) Class字节码解析_第6张图片

//方法引用

 /**
       -  方法引用信息
       -  tag  10
       -  class_index u2(指向CONSTANT_Class 一个类的全限定名)
       -  name_and_type_index u2 (指向 CONSTANT_NameAndType 方法的名字、参数的描述符)
       -  *sample: new String();
       - > class_index - > CONSTANT_Class(#class_index) -> utf8(CONSTANT_Class.name_index) -> java/lang/String 类的全限定名
       - > name_and_type_index -> CONSTANT_NameAndType(#name_and_type_index) ->  utf8(#CONSTANT_NameAndType.name_index)   ->      ↓
                                                                             ->   utf8(#CONSTANT_NameAndType.#descriptor_index) -> ()V  ↓
                                                                                                                   ----->  ()V 调用init方法 无参、返回值void
       */
        CONSTMethodrefInfo {
            class_index: u16,
            name_and_type_index: u16,
        }
  • tag: 10
  • class_index : class_index 指向常量池的 CONSTANT_Class_info 结构, 被指向的CONSTANT_Class_info 表示为类,说白了就是最终指向常量池里的字符串,是个类名。
  • name_and_type_index:指向常量池的CONSTANT_NameAndType_info 结构,它表示当前方法的描述符。
    从零开始用 Rust 打造一个玩具级别 Java 虚拟机 (二) Class字节码解析_第7张图片
    上图可以看到,当我们 new 一个 String 类型时 使用的 CONSTANT_Methodref_info 它 class_index 指向了一个 Class_info 指定了类的全限定名 java/lang/String , name_and_type_index : 指定了 方法的名字 和 方法修饰符 包含 参数和返回值 ()V ,
    () 表示没有参数 V代表 Void 没有返回值。

//接口引用

从零开始用 Rust 打造一个玩具级别 Java 虚拟机 (二) Class字节码解析_第8张图片

/**
     -  接口引用信息
     -  tag  11
     -  class_index u2(指向CONSTANT_Class 当前方法所属的接口)
     -  name_and_type_index u2 (指向 CONSTANT_NameAndType 获得当前接口方法的简单名称和自段描述符)
     - *sample :public class HelloClass {
            //接口
            private static aaa myinferface;
            public static void main(String[] args) {}
            public String method1() {
                //调用结构方法的引用
                return myinferface.method1();
            }
        }
     - > class_index - > CONSTANT_Class(#class_index) -> utf8(CONSTANT_Class.name_index) -> aaa 引用方法所在的接口
     - > name_and_type_index -> CONSTANT_NameAndType(#name_and_type_index) ->  utf8(#CONSTANT_NameAndType.name_index)   ->  method1  ↓
                                                                           ->   utf8(#CONSTANT_NameAndType.#descriptor_index) -> ()Ljava/lang/String;  ↓
                                                                                               ----->  method1:()Ljava/lang/String; 接口方法名:接口方法的参数返回值描述符
                                                                                                                                返回 String 无参数
    */
    CONSTInterfaceMethodrefInfo {
        class_index: u16,
        name_and_type_index: u16,
    }
  • tag: 11
  • class_index : class_index 指向常量池的 CONSTANT_Class_info 结构, 被指向的CONSTANT_Class_info 表示为 一个接口,最终指向一个字符串 表示为 接口名。
  • name_and_type_index:指向常量池的CONSTANT_NameAndType_info 结构,它表示当前方法的描述符。

CONSTANT_String_info 结构

CONSTANT_String_info 结构表示String类型的常量对象。

/**
    -  tag  8
    -  string_index u2(指向utf8的索引)
    */
    CONSTStringInfo {
        string_index: u16,
    }

从零开始用 Rust 打造一个玩具级别 Java 虚拟机 (二) Class字节码解析_第9张图片
从零开始用 Rust 打造一个玩具级别 Java 虚拟机 (二) Class字节码解析_第10张图片

  • u1:8
  • string_index:对应一个 CONSTANT_Utf8_info 结构在常量池中索引。

这个结构最后会转初始化为一个String 对象。

CONSTANT_Integer_info 和 CONSTANT_Float_info 结构

/**
    - tag 3
    - bytes u2(大端直接存储整形值,占用4个字节)
    - *sample:  public static final int a =50;
    -> 注意:只有被final 修饰的才会在 编译时就加入常量池。
    */
    CONSTANTIntegerInfo {
        i: i32,
    }

从零开始用 Rust 打造一个玩具级别 Java 虚拟机 (二) Class字节码解析_第11张图片

图上我们可以看到,short 其实 也是使用 CONSTANT_Integer_info 这个结构来存储的。

  • tag : 3
  • bytes:表示4个字节的整数,按大端存储。
/**
    - tag 4
    - bytes u2(大端直接存储浮点值,占用4个字节)
    - *sample:  public static final float b =  0.1f;
    */
    CONSTANTFloatInfo {
        f: f32,
    }
  • tag : 4
  • bytes:按照IEEE 754单精度浮点格式来表示 float常量的值,同样也是大端存储(Big-endian)。
    从零开始用 Rust 打造一个玩具级别 Java 虚拟机 (二) Class字节码解析_第12张图片
    总结:从这里 我们就可以看到 在java里 int 占用的 是 4个字节

CONSTANT_Long_info 和 CONSTANT_Double_info 结构

CONSTANT_Long_info、CONSTANT_Double_info 各自占用 2个常量池索引。
从零开始用 Rust 打造一个玩具级别 Java 虚拟机 (二) Class字节码解析_第13张图片

 /**
    - tag 5
    - bytes u8(按照大端存储一个占8个字节的long长整型数,其实可以分为 高四位 和低四位 通过 ((long)high_bytes << 32)  +  low_bytes 计算出实际数)
    - *sample:   public static final long c =  111111;
    -> 注意 一个Long类型 会在常量池占2个索引。
    */
    CONSTANTLongInfo {
        i: i64
    }
  • tag:5
  • high_bytes、low_bytes:将高32位 左移 32位 加上 低 32位 ((long)high_bytes << 32) + low_bytes 合起来 就是一个 64位的long类型。
 /**
   - tag 6
   - bytes u8(按照大端存储一个占8个字节的long长整型数,其实可以分为 高四位 和低四位 通过 ((long)high_bytes << 32)  +  low_bytes 计算出实际数)
   - *sample:   public static final double d =  111111.00;
   -> 注意:一个Double类型 会在常量池占2个索引。
   */
    CONSTANTDoubleInfo {
        f: f64,
    }
  • tag:6
  • high_bytes、low_bytes 按照Ieee 754 双精度浮点格式来表示 double常量的值。- - high_bytes 和 low_bytes 都按照大端(big-endian)顺序存储.
    ((long) high_bytes << 32 ) + low_bytes
    从零开始用 Rust 打造一个玩具级别 Java 虚拟机 (二) Class字节码解析_第14张图片

CONSTANT_NameAndType_info 结构

CONSTANT_NameAndType_info 结构用于表示字段或方法。

/**
    -  tag 12
    -  name_index u2(名字,指向utf8)
    -  descriptor_index u2(描述符类型,指向utf8)
    -  *sample: new String();
    -> name_index -> utf8(#name_index)  ->  指向一个utf8 的方法名
    -> descriptor_index -> utf8(#descriptor_index) -> ()V 指向方法的参数和返回值的描述符 () = 没有参数 V = Void
    */
    CONSTANTNameAndTypeInfo {
        name_index: u16,
        descriptor_index: u16,
    }
  • tag:12
  • name_index:对常量池的中CONSTANT_Utf8_info结构的有效索引,表示为 或者有效的字段或方法的非限定名。
  • descriptor_index:对常量池的中CONSTANT_Utf8_info结构的有效索引,表示为 字段描述符或方法 描述符。
    从零开始用 Rust 打造一个玩具级别 Java 虚拟机 (二) Class字节码解析_第15张图片
    从零开始用 Rust 打造一个玩具级别 Java 虚拟机 (二) Class字节码解析_第16张图片

CONSTANT_MethodHandle_info结构

用于表示方法的类型。

/**
   - tag 15
   - reference_kind 值在1~9之间,它决定了后续 reference_index项中的方法句柄类型,方法句柄的值表示方法句柄的字节码行为。
   - reference_index 指向常量值列表的有效索引
   -> 注意:同样只有被final 修饰的才会在 编译时存入常量池,并且一个Double类型 会在常量池占2个索引。
   */
    ConstantMethodHandleInfo {
        reference_kind: u8,
        reference_index: u16,
    }
  • tag 15
  • reference_kind 项的值必须在范围1 ~ 9(包括1和9)之内,它表示方法句柄的类型(kind). 方法句柄类型决定句柄的字节码行为(bytercode behavior)
  • reference_index 项的值必须是对常量池表的有效索引.该位置上的常量池表项。

CONSTANT_MethodType_info结构

/**
   - tag 16
   - descriptor_index 指向CONSTANT_Utf8_info结构,表示方法的类型。
   
   */
    ConstantMethodTypeInfo {
        descriptor_index: u16,
    }
   
  • tag 16
  • descriptor_index 指向的是CONSTANT_Utf8_info,表示方法的类型。

CONSTANT_InvokeDynamicInfo

/**
    - tag 18
    - bootstrap_method_attr_index 对当前字节码文件中引导方法的boostrap_method 数组的有效索引
    - name_and_type_index name_and_type_index 项的值则是一个指向常量池列表中CONSTANT_NameAndType_info常量项的有效索引,用于表示方法得的简单名称和方法描述符。
    */
    ConstantInvokeDynamicInfo {
        bootstrap_method_attr_index: u16,
        name_and_type_index: u16,
    }

访问标志

常量池结束后,接着是 访问标志(access flag)用来表示识别一些类或接口的访问信息,比如 这个类是否接口类,是否定义为public类型;是否定义为abstract类型; 类是否被final修饰等。

#[rustfmt::skip]
#[allow(dead_code)]
pub mod access_flags {
    pub const ACC_PUBLIC:            u16 = 0x0001; //声明为public,可以从包外访问
    pub const ACC_PACC_PRIVATE:      u16 = 0x0002; //声明为private,自己能从定义该方法的类中访问
    pub const ACC_PACC_PROTECTED:    u16 = 0x0004; //声明为protected,子类可以访问
    pub const ACC_PACC_STATIC:       u16 = 0x0008; //声明为static
    pub const ACC_PACC_FINAL:        u16 = 0x0010; //声明为 final,不能被覆盖
    pub const ACC_PACC_SYNCHRONIZED: u16 = 0x0020; //声明为synchronized,对该方法的调用,将包装在同步锁(monitor)里
    pub const ACC_PACC_BRIDGE:       u16 = 0x0040; //声明为bridge方法,由编译器产生
    pub const ACC_PACC_VARARGS:      u16 = 0x0080; //表示方法带有边长参数
    pub const ACC_PACC_NATIVE:       u16 = 0x0100; //声明为 native,该方法不是用Java语言实现的
    pub const ACC_PACC_ABSTRACT:     u16 = 0x0400; //声明为abstract,该方法没有实现代码
    pub const ACC_PACC_STRICT:       u16 = 0x0800; //声明为strictfp,使用FP-strict浮点模式
    pub const ACC_PACC_SYNTHETIC:    u16 = 0x10;  //该方法是由编译器合成的,而不是有源代码编译出来的
}

access_flags 一共16个标志可以使用,为什么是 16位可以使用呢?
因为 这个标志 2字节 =16 bit 每一位上置1 就代表 那个标志位, 00000000000000001 = ACC_PUBLIC
00000000000000010 + 00000000000000001 = ACC_PACC_PRIVATE + ACC_PUBLIC
当然 类不能 即是private 又是 pubilc 我们这里只是打个比方。

当给我们 m = 0x0000000000001010 我们只需要 直到 倒数第二位 对应 哪那个access标志,倒数第四位对应那个access标志。

获取倒数第二位 m & 0x10 倒数第四位 m & 1000

,其他的属于符合的标志 可以通过上面8个计算出来,具体的 如果 我想表达 一个 public interface 只需要 进行或运算 0x0001 | 0x0200 = 0x0201

拆解access_flag代码:

		
   let flag_access = 0x0012;
    let mut tag:u16 = 0b0001;
    for i in 0..16{
        let is = flag_access & tag;
        println!("{:x}",is);
        tag = tag << 1;
    }

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

类索引、父类索引 都是u2 类型、接口索引集合是多个u2类型的接口名索引的集合。类索引指向常量池中中的索引的一个类的全限定名称,父类索引 由于Java 中没有多重索引所以只能有一个父类并且一般 除了 java.lang.Object 外其他类都有父类,一般 普通类都继承了java.lang.Object,所以类的索引都不为0 ,接口索引 用来描述 这个类实现了哪些接口 ,顺序从左到右 的顺序为索引,如果接口数为0,那么后面接口表就没有实际数据。
从零开始用 Rust 打造一个玩具级别 Java 虚拟机 (二) Class字节码解析_第17张图片

字段表集合

字段表(field_info) 用于描述接口或者类中声明的变量。字段(field)包括类级变量以及实例级变量,在字节码文件中,每一个field项都对应这一个类或者接口中的字段(变量)信息,用于表示一个字段的完整信息,比如字段的标识符,访问 修饰符(public、private 或 protected)、是类变量还是实例变量(static 修饰符)、是否是常量(final修饰符)等,值的注意的一点是 由于局部变量并不包含在字段表中 所以 java的字段的作用域都是一样的 那么就是说在java中 一个字段(变量) 是无法重载的换句话说 只要变量名字一样 就是不可以的,不管类型是不是不一样。然而在字节码文件中 相同字段名 但是不同修饰符的变量是可以存在的。

/**

字段表结构
access_flags : 访问标志  基本8种 总共16种
ACC_PUBLIC 0x0001 | ACC_FINAL 0x0010
ACC_SUPER 0x0020 | ACC_INTERFACE 0x0200
ACC_ABSTRACT 0x0400 | ACC_SYNTHETIC 0x1000
ACC_ANNOTATION 0x2000 | ACC_ENUM 0x4000

name_index : 字段名(变量名)索引 引用常量池一个utf字符串
descriptor_index : 字段描述索引 引用常量池一个utf字符串

attributes_count 当前字段附加属性的数量。
attributes 属性表attribute_info,一个字段可以关联多个属性表。


*/
#[derive(Clone, Debug)]
pub struct FieldInfo {
    pub access_flags: u16,
    pub name_index: u16,
    pub descriptor_index: u16,
    pub attributes_count: u16,
    pub attributes: Vec<AttributeInfo>,
}

我们 可以看到字段表就是 对字段 做出描述的表,包括了 字段名 、字段的 类型 、字段访问表示等,关于最后 2个 方法表属性 后面将会提到。

从零开始用 Rust 打造一个玩具级别 Java 虚拟机 (二) Class字节码解析_第18张图片

方法表

在Java 中是不允许存在带有同一签名(描述符)并且方法名也相同的,这么做完全是为了避免 如果方法签名(描述符)都一致 编译器将无法判断实际要调用哪个方法, 但如果想要 声明多个具有相同方法名的方法,在面向对象中的多态特性中,方法重载 可以满足这个特性,只要实现了具有相同方法名的方法但具有不同的方法签名(描述符)即可。

  • access_flags 类或接口的初始化方法有Java虚拟机隐式调用,类在初始化的时候, 默认调用类的构造器 () 方法,并且() 方法只允许使用 ACC_PUBLIC、ACC_PRIVATE以及ACC_PROTECTED标志,
  • name_index name_index的值必须是对常量池表的一个有效索引。常量池表在该处索引处的成员必须是CONSTANT_Utf8_info结构,它要么表示一个特殊的方法的名字( 或) ,要么表示一个方法的有效非限定名。
  • descriptor_index descriptor_index的值必须是对常量池表的一个有效索引。常量池表示在该索引处的成员必须是CONSTANT_Utf8_info 结构,此结构表示一个有效的方法的描述符。简单来说 就是 一个索引 可以引用常量池 里一个字符串 这个字符串 是一个该函数参数和返回值的描述符 。
  • attribute_info 可以关联多个属性表
/**
方法表
- access_flags : 访问标志
- name_index : 方法名索引,引用常量池一个utf-8方法名字符串
- descriptor_index : 方法描述符索引,引用常量池一个utf-8描述符字符串
- attribute_info : 属性表
- samples : public static void main(String[] args) {}
          access_flags  0x0001 |
          name_index -> utf8(name_index)  -> main
          descriptor_index -> utf8(descriptor_index)  -> ([Ljava/lang/String;)V 参数 string数组 返回值 Void

*/
pub struct MethodInfo{
    pub access_flags: u16,
    pub name_index: u16,
    pub descriptor_index: u16,
    pub attribute_info:Vec<attributeInfo>,
}

属性表

属性(attribute) 表的通用结构

pub struct  AttributeInfo{
    pub attribute_name_index:u16,
    pub Vec<info>,
}
  • attribute_name_index 是一个对当前文件常量池的16位无符号索引,引用了一个CONSTANT_Utf8_info结构,用以表示当前属性的名字。
  • info 是一个自定义的数据结构,用于存储属性的数据信息。
属性名 class文件 出现位置 JavaSE
ConstantValue field_info 45.3 1.0.2
Code method_info 45.3 1.0.2
Execptions method_info 45.3 1.0.2
SourceFile ClassFile 45.3 1.0.2
LineNumberTable Code 45.3 1.0.2
LocalVariableTable Code 45.3 1.0.2
InnerClasses ClassFile 45.3 1.1
Synthetic ClassFile、field_info 、 method_info 45.3 1.1
Deprecated ClassFile、field_info 、 method_info 45.3 1.1
EnclosingMethod ClassFile 49.0 5.0
Signature ClassFile、field_info 、 method_info 49.0 5.0
SoureceDebugExtension ClassFile 49.0 5.0
LocalVariableTypeTable Code 49.0 5.0
RuntimeVisibleAnntations ClassFile、field_info 、 method_info 49.0 5.0
RuntimeInvisibleAnnotations ClassFile、field_info 、 method_info 49.0 5.0
RuntimeVisibleParameterAnnotations method_info 49.0 5.0
RuntimeInVisibleParameterAnnotations method_info 49.0 5.0
AnnotationDefault method_info 45.3 5.0
StackMapTable Code 50.0 6
BootstrapMethods ClassFile 45.3 7
RuntimeVisibleTypeAnnotations ClassFile、field_info 、 method_info 、 Code 52.0 8
RuntimeInVisibleTypeAnnotations ClassFile、field_info 、 method_info 、 Code 52.0 8
MethodParameters method_info 45.3 8
不同的虚拟机可能会有自己额外的属性,但是对于一个Java虚拟机能正确解读Class 文件就以下5个属性起到了关键作用。
  • ConstantValue
  • Cod
  • StackMapTable
  • Exceptions
  • BootstrapMethods
对于Java se平台的类库正确解读class文件其关键作用的12个属性。
  • InnerClasses
  • EnclosingMethod
  • Synthetic
  • Signature
  • RuntimeVisibleAnntations
  • RuntimeInvisibleAnnotations
  • RuntimeVisibleParameterAnnotations
  • RuntimeInVisibleParameterAnnotations
  • RuntimeVisibleTypeAnnotations
  • RuntimeInVisibleTypeAnnotations
  • AnnotationDefault
  • MethodParameters
自定义属性的规范
  1. 编译器可以 自定义属性表里的属性,这不影响Class文件的正常执行,所以JVM必须能跳过 不能识别的自定义属性。
  2. JVM规范明确规定Java虚拟机实现仅仅因为class文件包含新属性而抛出异常或以其他形式拒绝使用class文件。
  3. 自定义属性名不能重复,否则会引起冲突。

Code属性

code 属性 会记录 方法内部的虚拟机指令信息 ,和异常信息表,并且 Code 属性 内部还可以放其他 属性表。

  • 位于method_info 中,是为了识别一个Class 文件基础的属性3个属性之一
  • Code属性用于存储方法字节码指令以及一些其他辅助信息
  • 除了 abstract、native 关键字声明的抽象方法、本地方法 不含COde属性,其他都含Code属性。
#[derive(Debug, Clone)]
pub struct CodeAttribute {
    pub attributes_name_index:u16, /** 对常量池表的一个有效索引,常量池表在该索引处的成员闭学式CONSTANT_Utf8_info结构,用以表示字符串"Code" */
    pub attributes_length:u32, /** 给出了当前属性的长度,不包括初始 6个字节  */
    pub max_stack: u16, /** 给出了当前放大的操作数占在方法执行的任何时间点的最大深度  */
    pub max_locals: u16, /** 给出了分配在当前方法引用的局部变量表中的局部变量个数,其中也包括调试此方法时用于传递参数的局部变量。  */
    pub code_length: u32, /** 给出了当前方法 code[] 数组的字节数。  */
    pub code: *mut Vec<u8>, /** code[] 数组给出了实现当前方法的Java虚拟机代码的实际字节内容.  */
    pub exception_table_length: u16, /** 给出了 exception_table 表的成员个数  */
    pub exception_table: Vec<Exception>, /** 每个Exception 都是 code[] 数组中的一个异常处理器.  */
    pub attributes_count: u16, /** 给出了Code属性中attributes[] 成员的个数  */
    pub attributes_info: Vec<AttributeInfo>,  /** 一个AttributeInfo 的结构体,可以放入其他类型的属性表   */

}

#[derive(Debug, Clone)]
pub struct Exception {
    /**  start_pc 和 end_pc 的值必须是当前code[]中某一指令操作码的有效索引。 */
    pub start_pc: u16,

    /** end_pc 是 code[] 中某一指令操作码的有效索引,end_pc 另一种取值是 code_length 的值 ,即code[]的长度. start_pc < end_pc
    当程序计数器 处于 x条指令 处于 start_pc <= x < end_pc  也就是说 2个字节 65535 start_pc 从0 开始 但是 有个设计缺陷 end_pc 最大 也是 65535 但是 < end_pc
    把 end_pc 65535 排除在外了,这样导致如果 Code 属性如果长度刚好是 65535个字节 最后一条指令 不能被异常处理器 所处理。
    */
    pub end_pc: u16,

    /** handler_pc项的值表示一个异常处理器的起点。handler_pc 的值必须是同时使对当前code[] 和其中某一指令操作码的有效索引。简单来说 就是catch 处理指令的 code号 */
    pub handler_pc: u16,

    /**
      值不为0,对常量池的一个有效索引。常量池表在该索引处的成员必须是CONSTANT_Class_info 结构,用以表示当前异常处理器需要捕捉的异常类型。
      只有当抛出的异常是制度的类或其自类的实例时,才会调用异常处理器。 验证器(verifier) 会检查这个类是不是 Thorwable 或 Throwable的子类
      如果 catch_type 为 0,所有异常抛出是都会调用这个异常处理器。
    */
    pub catch_type: u16,

}

从零开始用 Rust 打造一个玩具级别 Java 虚拟机 (二) Class字节码解析_第19张图片

从零开始用 Rust 打造一个玩具级别 Java 虚拟机 (二) Class字节码解析_第20张图片

ConstantValue属性

  • Java虚拟机必须实现的3个属性之一,ConstantValue 属性是定长属性,位于field_info结构属性表中。
  • 一个field_info最多只能包含一个 ConstantValue属性,该属性主要用于通知Java虚拟机对代码中类变量(不包括实例变量)执行初始化操作。
  • 类变量初始化,两种方式
    1. (接口)初始化方法()完成
    2. ConstantValue属性完成,如果一个类变量 final、static 修饰,并且是原始数据(int long double …)类型 或String 类型 这个类能够被ConstantValue初始化,反之 如果不是 原始数据 、String类型 没有 被final 修饰的话只能 由 () 方法完成初始化操作。
/**
    定长属性,位于field_info结构的属性表中。
*/
#[derive(Debug, Clone)]
pub struct ConstantValue{
    /** 常量池列表中CONSTANT_Utf8_info常量项的有效索引,通过这个索引即可成功获取当前属性的简单名称, 即"ConstantValue"*/
    pub attribute_name_index:u16,
    /** 固定位2 ,*/
    pub attribute_length:u32,
    /**  指向常量池的CONSTANT_Long、CONSTANT_Float 、CONSTANT_Double 、
    CONSTANT_Integer 、CONSTANT_String 中的一种常量池结构体    */
    pub constantvalue_index:u16,
}

在这里插入图片描述

从零开始用 Rust 打造一个玩具级别 Java 虚拟机 (二) Class字节码解析_第21张图片

Exceptions属性

  • 和前面2个属性一样,属于Java 虚拟机必须识别的3个属性之一,位于method_info项属性表中。
  • 一个method_info 项属性表中最多包含一个Exception属性,用于例举出当前方法通过关键字 thorws 可能抛出的异常信息。
#[derive(Debug, Clone)]
pub struct ExceptionsAttribute{
    /** 常量池列表中CONSTANT_Utf8_info常量项的有效索引,可以获取当前属性的简单名称,即 字符串 "Exceptions"。 */
    pub attribute_name_index:u16,
    /** 指明了Exception属性值的长度 (不包括 attribute_lenght 和  attribute_name_index)*/
    pub attribute_lenght:u32,
    /** 指明了后序exception_index_table[] 项的数组的长度,其中每一个成员必须是一个指向常量池列表中Constant_Class_info */
    pub number_of_exceptions:u16,
    /** 常量项的有效索引通过这个索引值,即可成功获取当前方法通过thorws 可能抛出的异常信息 */
    pub exception_index_table:Vec<u16>
}

当 throw new Exception(“xxxx!”); 抛出异常时 时 就会产生一个一个关于宜昌的属性。

从零开始用 Rust 打造一个玩具级别 Java 虚拟机 (二) Class字节码解析_第22张图片
从零开始用 Rust 打造一个玩具级别 Java 虚拟机 (二) Class字节码解析_第23张图片

LineNumberTable 属性

在日常生活中,我们经常会根据 日志文件中给出的 错误行号和所属的文件名来分析和解决错误,如果 程序出现错误 我们不想 输入错误行号以及错误所属文件名称,可以在编译的时候 使用 “-g:none”,使用了这个选项后,堆栈信息中将再也不输出任何错误行号语句错误代码所属的文件名称。

  • LineNumberTable 其实就是用来描述 Java代码中的行号与字节码文件中的字节码行号之间的对应关系。
  • LineNumberTable是一个可选属性,它位于Code_atribute 项的属性表中。
#[derive(Debug, Clone)]
pub struct LineNumberTable{
    /** 常量池列表中CONSTANT_Utf8_info常量项的有效索引,可以获取当前属性的简单名称,即 字符串 "LineNumberTable"。 */
    attribute_name_index:u16,
    /** 指明了后序2个属性属性的长度 (不包括 attribute_name_index attribute_length ) */
    attribute_length:u32,
    /** 指明了 line_number_tabel 项数组的长度,*/
    line_number_table_length:u16,
    line_number_info:Vec<LineNumber>,
}

#[derive(Debug, Clone)]
pub struct LineNumber {
    /**表示字节码文件中的字节码行号*/
    pub start_pc: u16,
    /**表示Java代码中的行号*/
    pub line_number: u16,
}

从零开始用 Rust 打造一个玩具级别 Java 虚拟机 (二) Class字节码解析_第24张图片

SourceFile 属性

  • 可选属性,位于ClassFile的属性表中,用于记录生成字节码文件的源文件名称。如果在编译是设定有选项 “-g:none” 时,那么程序中一旦出现异常时,堆栈信息中将再也不会输出任何错误行号以及错误代码所属的文件名称。
#[derive(Debug, Clone)]
pub struct SourceFile_attribute {
    /** 常量池列表中CONSTANT_Utf8_info常量项的有效索引,可以获取当前属性的简单名称,即 字符串 "SourceFile"。 */
    attribute_name_index:u16,
    /** 值固定位 2*/
    attribute_length:u32,
    /** 常量池列表中CONSTANT_Utf8_info常量项的有效索引,通过这个索引既可以获取源文件的名称 */
    sourcefile_index:u16,
}

从零开始用 Rust 打造一个玩具级别 Java 虚拟机 (二) Class字节码解析_第25张图片

LocalVariableTable 属性

  • 可选属性,用于调试器在执行方法过程中可以用它来确定某个局部变量的值,此属性描述的是局部变量表中的局部变量与Java代码中定义变量之间的对应关系。
  • 位于Code_attribute 项的属性表中
/**
   局部变量表  存放方法的局部变量信息

*/
pub struct InnerClassesAttribute{
    /** 常量池列表中CONSTANT_Utf8_info常量项的有效索引,可以获取当前属性的简单名称,即  "InnerClasses"。 */
    pub attribute_name_index:u16,
    /** 指明了后序2个属性属性的长度 (不包括 attribute_name_index attribute_length )  */
    pub attribute_legth:u32,
    /** 指明了后序inner_classes[] 项的数组长度,也就是一个类中究竟含有多少个内部类*/
    pub number_of_classe:u16,

    classes:Vec<InnerClassesInfo>,

}

pub struct  InnerClassesInfo{
    /**常量池列表中CONSTANT_Class_info常量项的有效索引,用以表示类或接口*/
    inner_class_info_index:u16,
    /**如果Class不是类或接口的成员(也就是Class为顶层类或接口)、局部类或匿名类、
    那么outer_class_info_index项的值为0,否则这个项的值必须是对象常量池表的一个有效索引,
    常量池表在该索引处的成员必须是Constant_class_INFO结构,代表一个类或接口,Class
    为这个类或接口的成员*/
    outer_class_info_index:u16,
    /** 如果 C 是匿名类,则为0 ,否则是对常量池表UTF8结构的一个有效索引,表示由与C的class
    文件相对的源文件所定义的C的原始简单名称*/
    inner_name_index:u16,
    /** C的访问标志 */
    inner_class_access_flags:u16,
}

从零开始用 Rust 打造一个玩具级别 Java 虚拟机 (二) Class字节码解析_第26张图片

InnerClasses 属性

  • 用于描述内部类 与宿主之间的关联关系,该属性位于 ClassFile 的属性表中。 如果一个类 含有内部类,那么编译器就会生成InnerClasses属性。
pub struct InnerClassesAttribute{
    /** 常量池列表中CONSTANT_Utf8_info常量项的有效索引,可以获取当前属性的简单名称,即  "InnerClasses"。 */
    pub attribute_name_index:u16,
    /** 指明了后序2个属性属性的长度 (不包括 attribute_name_index attribute_length )  */
    pub attribute_legth:u32,
    /** 指明了后序inner_classes[] 项的数组长度,也就是一个类中究竟含有多少个内部类*/
    pub number_of_classe:u16,

    classes:Vec<InnerClassesInfo>,

}

pub struct  InnerClassesInfo{
    /**常量池列表中CONSTANT_Class_info常量项的有效索引,用以表示类或接口*/
    inner_class_info_index:u16,
    
    outer_class_info_index:u16,
    inner_name_index:u16,
    inner_class_access_flags:u16,
}

从零开始用 Rust 打造一个玩具级别 Java 虚拟机 (二) Class字节码解析_第27张图片

StackMapTable 属性

  • 变长属性,位于Code 属性表中,用于虚拟机的类型检查验证阶段。
  • Code 属性表 只能包含一个 StackMapTable 属性。
    stackMap table 是为了 Class文件 在加载时做类型检查用的,它描述了 有不同块 组成的一个方法内部的 局部面量表 的里面变量的类型和上一个状态之间的关系,默认 第一个StackMapTable是隐藏的, 一个 条件分支 goto 的开头都可以 会产生一个StackMapTable 属性,当有新的 属性 的时候 会通过append 来表示。

从零开始用 Rust 打造一个玩具级别 Java 虚拟机 (二) Class字节码解析_第28张图片

参考资料:

— 周志华:《深入理解 Java 虚拟机》
— Tim Lindholm 、Frank Yellin、Gilad Bracha 、Alex Buckley 《 java虚拟机规范 Java Se8》
— 高翔龙《java 虚拟机精讲》
— 张秀宏 《自己动手写 java虚拟机》

你可能感兴趣的:(jvm,java,rust,jvm,虚拟机,rust)