JVM 基础 - 类字节码详解

131 阅读11分钟

这是我参与8月更文挑战的第9天,活动详情查看:8月更文挑战

多语言编译为字节码在JVM运行

​ 计算机是不能直接运行java代码的,必须要先运行java虚拟机,再由java虚拟机运行编译后的java代码。这个编译后的java代码,就是本文要介绍的java字节码。

​ 为什么jvm不能直接运行java代码呢,这是因为在cpu层面看来计算机中所有的操作都是一个个指令的运行汇集而成的,java是高级语言,只有人类才能理解其逻辑,计算机是无法识别的,所以java代码必须要先编译成字节码文件,jvm才能正确识别代码转换后的指令并将其运行。

Java代码间接翻译成字节码,储存字节码的文件再交由运行于不同平台上的JVM虚拟机去读取执行,从而实现一次编写,到处运行的目的。

JVM也不再只支持Java,由此衍生出了许多基于JVM的编程语言,如Groovy, Scala, Koltin等等。

java-jvm-class-1.png

Class文件的结构属性

简述

Class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地 排列在Class文件之中,中间没有添加任何分隔符,这使得整个Class文件中存储的内容几乎 全部是程序运行的必要数据,没有空隙存在。当遇到需要占用8位字节以上空间的数据项 时,则会按照高位在前的方式分割成若干个8位字节进行存储。根据Java虚拟机规范的规定,Class文件格式采用一种类似于C语言结构体的伪结构来存储数据,这种伪结构中只有两种数据类型:无符号数和表

无符号数属于基本的数据类型,以u1、u2、u4、u8来分别代表1个字节、2个字节、4个 字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8 编码构成字符串值。

表是由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都习惯性地以“_info”结尾,表用于描述有层次关系的复合结构的数据。

class ClassFile {
 	u4 magic;  //魔数 0xCAFEBABE
 	u2 minor_version;  //次版本号
 	u2 major_version;  //主版本号
 	u2 constant_pool_count; //常量池计数器
 	cp_info constant_pool[constant_pool_count-1];//常量池 从1开始
 	u2 access_flags;//访问标记
 	u2 this_class;  //类索引
 	u2 super_class; //父类索引
 	u2 interfaces_count; //接口计数器
 	u2 interfaces[interfaces_count];//接口表
 	u2 fields_count;//字段计数器
 	field_info fields[fields_count];//字段表
 	u2 methods_count; //方法计数器
 	method_info methods[methods_count];//方法表
 	u2 attributes_count;//属性计数器
 	attribute_info attributes[attributes_count];//属性表
}

魔数

每个Class文件的头4个字节被称为魔数(Magic Number),值为0xCAFEBABE。它的作用在于:当JVM在尝试加载某个文件到内存中来的时候,会首先判断此class文件有没有JVM认为可以接受的“签名”,即JVM会首先读取文件的前4个字节,判断该4个字节是否是“0xCAFEBABE”,如果是,则JVM会认为可以将此文件当作class文件来加载并使用。

版本号

​ 随着Java本身的发展,Java语言特性和JVM虚拟机也会有相应的更新和增强。目前我们能够用到的JDK版本如:1.5,1.6,1.7,还有现如今最新的1.8。发布新版本的目的在于:在原有的版本上增加新特性和相应的JVM虚拟机的优化。而随着主版本发布的次版本,则是修改相应主版本上出现的bug。我们平时只需要关注主版本就可以了。 ​ 主版本号和次版本号在class文件中各占两个字节,副版本号占用第5、6两个字节,而主版本号则占用第7,8两个字节。JDK1.1的主版本号为45,以后的每个新主版本都会在原先版本的基础上加1。若现在使用的是JDK1.7编译出来的class文件,则相应的主版本号应该是51,对应的7,8个字节的十六进制的值应该是 0x33。

一个 JVM实例只能支持特定范围内的主版本号 (Mi 至Mj) 和 0 至特定范围内 (0 至 m) 的副版本号。假设一个 Class 文件的格式版本号为 V, 仅当Mi.0 ≤ v ≤ Mj.m成立时,这个 Class 文件才可以被此 Java 虚拟机支持。不同版本的 Java 虚拟机实现支持的版本号也不同,高版本号的 Java 虚拟机实现可以支持低版本号的 Class 文件,反之则不成立。

常量池

在class文件中的魔数、副版本号、主版本之后,紧接着就是常量池的数据区域了。前面的两个字节占有的位置叫做常量池计数器(constant_pool_count),它记录着常量池的组成元素 常量池项(*cp_info*) 的个数。紧接着会排列着constant_pool_count-1常量池项(*cp_info*)

常量池可以理解成Class文件中的资源仓库。主要存放的是两大类常量:字面量(Literal)和符号引用(Symbolic References)。字面量类似于java中的常量概念,如文本字符串,final常量等,而符号引用则属于编译原理方面的概念,包括以下三种:

​ 类和接口的全限定名(Fully Qualified Name)

​ 字段的名称和描述符号(Descriptor)

​ 方法的名称和描述符

image-20210820094019729.png

cp_info的通用结构

cp_info {
u1 tag;
u1 info[];
}
类型标志(tag 区分类型)描述
CONSTANT_Utf8_info1UTF-8 编码的字符串
CONSTANT_Integer_info3整性字面量
CONSTANT_Float_info4浮点型字面量
CONSTANT_Long_info5长整型字面量
CONSTANT_Double_info6双精度浮点型字面量
CONSTANT_Class_info7类或则接口的符号引用
CONSTANT_String_info8字符串类型字面量
CONSTANT_Fieldref_info9字段的符号引用
CONSTANT_Methodref_info10类中方法的符号引用
CONSTANT_InterfaceMethodref_info11接口中的方法符号引用
CONSTANT_NameAndType_info12字段或者方法的部分符号引用
CONSTANT_MethodHandle_info15表示方法句柄
CONSTANT_MethodType_info16标识方法类型
CONSTANT_InvokeDynamic_info18表示一个动态方法的调用点

CONSTANT_MethodType_info,CONSTANT_MethodHandle_info,CONSTANT_InvokeDynamic_info,这三项主要是为了让Java语言支持动态语言特性而在Java 7 版本中新增的三个常量池项,只会在极其特别的情况能用到它,在class文件中几乎不会生成这三个常量池项。

访问标志

访问标志,access_flags 是一种掩码标志,用于表示某个类或者接口的访问权限及基础属性。

标志名称标志值含义
ACC_PUBLIC0x0001是否为Public类型
ACC_FINAL0x0010是否被声明为final,只有类可以设置
ACC_SUPER0x0020是否允许使用invokespecial字节码指令的新语义.
ACC_INTERFACE0x0200标志这是一个接口
ACC_ABSTRACT0x0400是否为abstract类型,对于接口或者抽象类来说,次标志值为真,其他类型为假
ACC_SYNTHETIC0x1000标志这个类并非由用户代码产生
ACC_ANNOTATION0x2000标志这是一个注解
ACC_ENUM0x4000标志这是一个枚举

类索引(this_class)

类索引,this_class的值必须是对constant_pool表中项目的一个有效索引值。constant_pool表在这个索引处的项必须为CONSTANT_Class_info 类型常量,表示这个 Class 文件所定义的类或接口。

父类索引(super_class)

父类索引,对于类来说,super_class 的值必须为 0 或者是对constant_pool 表中项目的一个有效索引值。如果它的值不为 0,那 constant_pool 表在这个索引处的项必须为CONSTANT_Class_info 类型常量,表示这个 Class 文件所定义的类的直接父类。当前类的直接父类,以及它所有间接父类的access_flag 中都不能带有ACC_FINAL 标记。对于接口来说,它的Class文件的super_class项的值必须是对constant_pool表中项目的一个有效索引值。constant_pool表在这个索引处的项必须为代表 java.lang.Object 的 CONSTANT_Class_info 类型常量 。如果 Class 文件的 super_class的值为 0,那这个Class文件只可能是定义的是java.lang.Object类,只有它是唯一没有父类的类。

接口表(interfaces[interfaces_count])

接口表,interfaces[]数组中的每个成员的值必须是一个对constant_pool表中项目的一个有效索引值, 它的长度为 interfaces_count。每个成员 interfaces[i] 必须为 CONSTANT_Class_info类型常量,其中 0 ≤ i <interfaces_count。在interfaces[]数组中,成员所表示的接口顺序和对应的源代码中给定的接口顺序(从左至右)一样,即interfaces[0]对应的是源代码中最左边的接口。

字段表(fields[fields_count])

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

field_info {
u2 access_flags;//字段被访问权限和基础属性的掩码标志。
u2 name_index;
u2 descriptor_index;
u2 attributes_count;
attribute_info attributes[attributes_count];
}

方法表(methods[methods_count])

方法表,methods[] 数组中的每个成员都必须是一个 method_info 结构的数据项,用于表示当前类或接口中某个方法的完整描述。如果某个method_info 结构的access_flags 项既没有设置 ACC_NATIVE 标志也没有设置ACC_ABSTRACT 标志,那么它所对应的方法体就应当可以被 Java 虚拟机直接从当前类加载,而不需要引用其它类。 method_info结构可以表示类和接口中定义的所有方法,包括实例方法、类方法、实例初始化方法方法和类或接口初始化方法方法 。methods[]数组只描述当前类或接口中声明的方法,不包括从父类或父接口继承的方法。

method_info {
u2 access_flags;
u2 name_index;
u2 descriptor_index;
u2 attributes_count;
attribute_info attributes[attributes_count];
}

属性表(attributes[attributes_count])

属性表,attributes 表的每个项的值必须是attribute_info结构。

在Java 7 规范里,Class文件结构中的attributes表的项包括下列定义的属性: InnerClasses 、 EnclosingMethod 、 Synthetic 、Signature、SourceFile,SourceDebugExtension 、Deprecated、RuntimeVisibleAnnotations 、RuntimeInvisibleAnnotations以及BootstrapMethods属性。

通用格式

attribute_info {
u2 attribute_name_index;
u4 attribute_length;
u1 info[attribute_length];
}
属性名称使用位置含义
Code方法表Java代码编译成的字节码指令
ConstantValue字段表由final关键字定义的常量值
Deprecated类、方法表、字段被声明为deprecated的方法和字段
Exceptions方法表方法抛出的异常列表
EnclosingMethod类文件仅当一个类为局部类或者匿名类是才能拥有这个属性,这个属性用于标识这个类所在的外围方法
InnerClass类文件内部类列表
LineNumberTableCode属性Java源码的行号与字节码指令的对应关系
LocalVariableTableCode属性方法的局部变量描述
StackMapTableCode属性JDK1.6中新增的属性,供新的类型检查检验器检查和处理目标方法的局部变量和操作数有所需要的类是否匹配
Signature类、方法表、字段用于支持泛型情况下的方法签名
SourceFile类文件记录源文件名称
SourceDebugExtension类文件用于存储额外的调试信息
Synthetic类、方法表、字段标志方法或字段为编译器自动生成的
LocalVariableTypeTable使用特征签名代替描述符,是为了引入泛型语法之后能描述泛型参数化类型而添加
RuntimeVisibleAnnotations类、方法表、字段为动态注解提供支持
RuntimeInvisibleAnnotations类、方法表、字段用于指明哪些注解是运行时不可见的
RuntimeVisibleParameterAnnotations方法表作用与RuntimeVisibleAnnotations属性类似,只不过作用对象为方法
RuntimeInvisibleParameterAnnotations方法表作用与RuntimeInvisibleAnnotations属性类似,作用对象哪个为方法参数
AnnotationDefault方法表用于记录注解类元素的默认值
BootstrapMethos类文件用于保存invokeddynamic指令引用的引导方式限定符
RuntimeVisibleTypeAnnotations类、方法表、字段、Code属性指明哪些注解运行时可见的
RuntimeInvisibleTypeAnnotations类、方法表、字段、Code属性指明哪些注解运行时不可见的
MethodParameters方法表用于支持(编译时加上-parameters参数)将方法名称编译进class文件,并在运行中获取
Module用于记录一个Module名称及相关信息
ModulePackages用于记录一个模块中被exports 或则opens的包
ModuleMainClass指定一个模块的指令
NestHost用于支持嵌套类的反射和访问控制API,一个内部类通过该属性得知自己的宿主类
NestMembers用于支持嵌套类的反射和访问控制API,一个宿主类通过该属性得知自己有哪些内部类

案例

juejin.cn/post/699221…