上一章 咱们讲解了类的加载,后面咱重新写了代码,咱打算用 未来可能很火的Rust 来完成这个项目。
JAVA中每个class 文件就是一个类,类名和文件名相同, 按照Java虚拟机规范其中对类名有了严格的规定。Java虚拟机 对类的加载方式则较为宽松 类文件可以是从.JAR .ZIP 文件中读取加载class文件,甚至可以从网络上加载。
按照《Java虚拟机规范 JavaSE7版》的描述来看,任何编程语言的编译结果满足并包含Java虚拟机的内部指令集、符号表以及一些其他的辅助信息,它就是一个有效的字节码文件,就能够被虚拟机所识别并装载运行。
字节码是指平常所了解的 .class 文件,Java 代码通过 javac 命令编译成字节码
机器码和本地代码都是指机器可以直接识别运行的代码,也就是机器指令
字节码是不能直接运行的,需要经过 JVM 解释或编译成机器码才能运行
此时你要问了,为什么 Java 不直接编译成机器码,这样不是更快吗?
机器码是与平台相关的,也就是操作系统相关,不同操作系统能识别的机器码不同,如果编译成机器码那岂不是和 C、C++差不多了,不能跨平台,Java 就没有那响亮的口号 “一次编译,到处运行”;
之所以不一次性全部编译,是因为有一些代码只运行一次,没必要编译,直接解释运行就可以。而那些“热点”代码,反复解释执行肯定很慢,JVM在运行程序的过程中不断优化,用JIT编译器编译那些热点代码,让他们不用每次都逐句解释执行;
还有一方面的原因是后文讲解的解释器与编译器共存的原因。
为了提高热点代码的执行效率,在运行时,虚拟机将会把这些代码编译成与本地平台相关的机器码,并进行各种层次的优化,完成这个任务的编译器称为即时编译器(Just In Time Compiler),简称 JIT 编译器
编译器:把源程序的每一条语句都编译成机器语言,并保存成二进制文件,这样运行时计算机可以直接以机器语言来运行此程序,速度很快;
解释器:只在执行程序时,才一条一条的解释成机器语言给计算机来执行,所以运行速度是不如编译后的程序运行的快的;
通过javac命令将 Java 程序的源代码编译成 Java 字节码,即我们常说的 class 文件。这是我们通常意义上理解的编译。
字节码并不是机器语言,要想让机器能够执行,还需要把字节码翻译成机器指令。这个过程是Java 虚拟机做的,这个过程也叫编译。是更深层次的编译。(实际上就是解释,引入 JIT 之后也存在编译)
此时又有疑惑了,Java不是解释执行的吗?
没错,Java 需要将字节码逐条翻译成对应的机器指令并且执行,这就是传统的 JVM 的解释器的功能,正是由于解释器逐条翻译并执行这个过程的效率低,引入了 JIT 即时编译技术。
必须指出的是,不管是解释执行,还是编译执行,最终执行的代码单元都是可直接在真实机器上运行的机器码,或称为本地代码
.Class字节码组成有不通长度的字节码(Byte)为单位通过拼凑而成,Java虚拟机规范定义了u1、u2和u4三种数据类型来表示1、 2和4字节无符号整数, 并且以大端(big-endian)方式存储。
类型 | 名称 | 数量 |
---|---|---|
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 (无符号整数)
魔数很简单,其实就是你给你的名字象征的七个表示在前几个字节,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 |
紧跟在 主版本号,后面的是 常量池的数量,值的注意的一点是 常量池的索引是从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个字节 知道 确定后面需要读取的数据的结构,按对应的读取方法再做读取。
全限定名和简单名称其实解释 类的名字 比如 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表示。
/**
- 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>,
}
指向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 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,
}
System.out.println() 在常量池里 表示为 CONSTANT_Fieldref_info 结构。2各部分 组成 类名 和 方法的非限定名。
//方法引用
/**
- 方法引用信息
- 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 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,
}
CONSTANT_String_info 结构表示String类型的常量对象。
/**
- tag 8
- string_index u2(指向utf8的索引)
*/
CONSTStringInfo {
string_index: u16,
}
这个结构最后会转初始化为一个String 对象。
/**
- tag 3
- bytes u2(大端直接存储整形值,占用4个字节)
- *sample: public static final int a =50;
-> 注意:只有被final 修饰的才会在 编译时就加入常量池。
*/
CONSTANTIntegerInfo {
i: i32,
}
图上我们可以看到,short 其实 也是使用 CONSTANT_Integer_info 这个结构来存储的。
/**
- tag 4
- bytes u2(大端直接存储浮点值,占用4个字节)
- *sample: public static final float b = 0.1f;
*/
CONSTANTFloatInfo {
f: f32,
}
CONSTANT_Long_info、CONSTANT_Double_info 各自占用 2个常量池索引。
/**
- 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 6
- bytes u8(按照大端存储一个占8个字节的long长整型数,其实可以分为 高四位 和低四位 通过 ((long)high_bytes << 32) + low_bytes 计算出实际数)
- *sample: public static final double d = 111111.00;
-> 注意:一个Double类型 会在常量池占2个索引。
*/
CONSTANTDoubleInfo {
f: f64,
}
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 15
- reference_kind 值在1~9之间,它决定了后续 reference_index项中的方法句柄类型,方法句柄的值表示方法句柄的字节码行为。
- reference_index 指向常量值列表的有效索引
-> 注意:同样只有被final 修饰的才会在 编译时存入常量池,并且一个Double类型 会在常量池占2个索引。
*/
ConstantMethodHandleInfo {
reference_kind: u8,
reference_index: u16,
}
/**
- tag 16
- descriptor_index 指向CONSTANT_Utf8_info结构,表示方法的类型。
*/
ConstantMethodTypeInfo {
descriptor_index: u16,
}
/**
- 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,那么后面接口表就没有实际数据。
字段表(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个 方法表属性 后面将会提到。
在Java 中是不允许存在带有同一签名(描述符)并且方法名也相同的,这么做完全是为了避免 如果方法签名(描述符)都一致 编译器将无法判断实际要调用哪个方法, 但如果想要 声明多个具有相同方法名的方法,在面向对象中的多态特性中,方法重载 可以满足这个特性,只要实现了具有相同方法名的方法但具有不同的方法签名(描述符)即可。
/**
方法表
- 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>,
}
属性名 | 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 |
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,
}
/**
定长属性,位于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,
}
#[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!”); 抛出异常时 时 就会产生一个一个关于宜昌的属性。
在日常生活中,我们经常会根据 日志文件中给出的 错误行号和所属的文件名来分析和解决错误,如果 程序出现错误 我们不想 输入错误行号以及错误所属文件名称,可以在编译的时候 使用 “-g:none”,使用了这个选项后,堆栈信息中将再也不输出任何错误行号语句错误代码所属的文件名称。
#[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,
}
#[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,
}
/**
局部变量表 存放方法的局部变量信息
*/
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,
}
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,
}
— 周志华:《深入理解 Java 虚拟机》
— Tim Lindholm 、Frank Yellin、Gilad Bracha 、Alex Buckley 《 java虚拟机规范 Java Se8》
— 高翔龙《java 虚拟机精讲》
— 张秀宏 《自己动手写 java虚拟机》