JVM之字节码与类的加载

51 阅读10分钟

前端编译器: 前端编译器的主要任务就是负责将符合java语法规范的java代码转换为符合jvm规范的字节码

Javac是一种能够将Java源码编译为字节码的前端编译器

Javac编译器在将Java源码编译为一个有效的字节吗文件过程中经历了4个步骤,分别是 词法解 析、语法解析、语义解析以及生成字节码。

JIT编译器:在程序运行时,发现某段代码反复地执行(热点代码),把字节码转换为可以在硬件上执行的机器指令

AOT编译器:在程序运行之前,直接将字节码文件翻译成机器指令

Class文件结构

class字节码文件结构

类型名称说明长度数量
u4magic魔数,识别Class文件格式4个字节1
u2minor_version副版本号(小版本)2个字节1
u2major_version主版本号(大版本)2个字节1
u2constant_pool_count常量池计数器2个字节1
cp_infoconstant_pool常量池表n个字节constant_pool_count-1
u2access_flags访问标识2个字节1
u2this_class类索引2个字节1
u2super_class父类索引2个字节1
u2interfaces_count接口计数器2个字节1
u2interfaces接口索引集合2个字节interfaces_count
u2fields_count字段计数器2个字节1
field_infofields字段表n个字节fields_count
u2methods_count方法计数器2个字节1
method_infomethods方法表n个字节methods_count
u2attributes_count属性计数器2个字节1
attribute_infoattributes属性表n个字节attributes_count
  • 文件开头的4个字节("cafe babe")称之为 魔数,唯有以"cafe babe"开头的class文件方可被虚拟机所接受,这4个字节就是字节码文件的身份识别。
  • 0000是编译器jdk版本的次版本号0,0034转化为十进制是52,是主版本号,java的版本号从45开始,除1.0和1.1都是使用45.x外,以后每升一个大版本,版本号加一。也就是说,编译生成该class文件的jdk版本为1.8.0。

Class文件版本号和平台的对应

主版本(十进制)副版本(十进制)编译器版本
4531.1
4601.2
4701.3
4801.4
4901.5
5001.6
5101.7
5201.8
5301.9
5401.10
5501.11

class文件数据类型

数据类型定义说明
无符号数无符号数可以用来描述数字、索引引用、数量值或按照utf-8编码构成的字符串值。其中无符号数属于基本的数据类型。 以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节和8个字节
表是由多个无符号数或其他表构成的复合数据结构。所有的表都以“_info”结尾。 由于表没有固定长度,所以通常会在其前面加上个数说明。

类型描述符

描述符的作用就是来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值。根据描述符规则,基本数据类型以及代表无返回值的void类型都用一个大写字母表示,而对象类型则用字符L加对象的全限定名来表示

标志符含义
B基本数据类型byte
C基本数据类型char
D基本数据类型double
F基本数据类型float
I基本数据类型int
J基本数据类型long
S基本数据类型short
Z基本数据类型boolean
V代表void类型
L对象类型,比如:Ljava/lang/Object;
[数组类型,代表一维数组。比如:double[][][] is [[[D

常量类型和结构

类型标志(或标识)描述
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表示一个动态方法调用点

常量池:存放所有常量

  • 在版本号之后,紧跟着的是常量池的数量,以及若干个常量池表项。
  • 常量池中常量的数量是不固定的,所以在常量池的入口需要放置一项u2类型的无符号数,代表常量池容量计数值(constant_pool_count)。与java中语言习惯不一样的是,这个容量计数是从1而不是从0开始的。
  • 常量池表项中,用于存放编译时期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。

常量池计数器 (constant_pool_count)

  • 由于常量池的数量不固定,时长时短,所以需要放置两个字节来表示常量池容量计数值。
  • 常量池容量计数值(u2类型):从1开始,表示常量池中有多少项常量。即constant_pool_count =1 表示常量池中有0个常量项。

常量池表(constant_pool [])

  • constant_pool是一种表结构,以1 ~ constant_pool_count -1为索引。表明了后面有多少个常量项。
  • 常量池主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References)
  • 他包含了class文件结构及其子结构中引用的所有字符串常量、类或接口名、字段名和其他常量。常量池中的每一项都具备相同的特征。第一个字节作为类型标记,用于确定该项的格式,这个字节称为tag byte (标记字节、标签字节)

字面量和符号引用

字面量:文本字符串 声明为final的常量值

符号引用: 类和接口的全限定名、字段的名称和描述符、方法的名称和描述符

虚拟机在加载Class文件时才会进行动态链接,也就是说,Class文件中不会保存各个方法和字段的最终内存布局信息,因此 这些字段和方法的符号引用不经过转换是无法直接被虚拟机使用的类。当虚拟机运行时,需要从常量池中获得对应的符号引用,再在类加载过程中的解析阶段将其替换为直接引用,并翻译到具体的内存地址。

符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任意形式的字面量,只要使用时能无歧义的定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到了内存中。

直接引用:直接引用可以是直接指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄。直接引用是与虚拟机实现的内存布局相关的,同一个符号引用在不同的虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那就说明引用的目标必定已经在内存之中了。

访问标志

标志名称标志值含义
ACC_PUBLIC0x0001标志为public类型
ACC_FINAL0x0010标志被声明为final,只有类可以设置
ACC_SUPER0x0020标志允许使用invokespecial字节码指令的新语义,JDK1.0.2之后编译出来的类的这个标志默认为真。(使用增强的方法调用父类方法)
ACC_INTERFACE0x0200标志这是一个接口
ACC_ABSTRACT0x0400是否为abstract类型,对于接口或者抽象类来说,次标志值为真,其他类型为假
ACC_SYNTHETIC0x1000标志此类并非由用户代码产生(即:由编译器产生的类,没有源码对应)
ACC_ANNOTATION0x2000标志这是一个注解
ACC_ENUM0x4000标志这是一个枚举

类索引、父类索引、接口索引

长度含义
u2this_class
u2super_class
u2interfaces_count
u2interfaces[interfaces_count]

属性的通用格式

类型名称数量含义
u2attribute_name_index1属性名索引
u4attribute_length1属性长度
u1infoattribute_length属性表

数据类型和默认初始值对应

类型默认初始值
byte(byte)0
short(short)0
int0
long0L
float0.0f
double0.0
char\u0000
booleanfalse
referencenull

字节码指令集

  1. Java字节码对于虚拟机,就好像汇编语言对于计算机,属于基本执行指令。
  2. Java虚拟机的指令由一个字节长度的、代表着某种特定操作意义的数字(成为操作码,opcode),以及跟随其后的零至多个代表此操作所需参数(称为操作数,operands)构成
  3. 由于限制了java虚拟机操作码的长度为一个字节(0-255),就是说指令集的操作码总数不能超过256条。

加载与存储指令

常用指令:

1、【局部变量压栈指令】将一个局部变量加载到操作数栈:xload、xload_(其中x为i、l、f、d、a, n为0到3)

2、【常量入栈指令】将一个常量加载草操作数栈:bipush 、sipush ldc 、ldc_w ldc2_w 、aconst_null iconst_m1 、iconst_ 、 lconst_ 、lconst_、 dconst_

3、【出栈装入局部变量表指令】将一个数值从操作数栈存储到局部变量表:xstore、xstore_(其中x为i、l、f、d、a,n为0到3);xastore(其中x为i、l、f、d、a、b、c、s)

4、扩充局部变量表的访问索引的指令:wide。

复习操作数栈和局部变量表:

操作数栈:执行每一条指令之前,Java虚拟机要求该指令的操作数已经被压入操作数栈中。,在执行指令时。Java虚拟机会将该指令所需的操作数弹出,并且将指令的结果重新压入栈中。(用来存放操作数,以及返回结果)

局部变量表:字节码程序可以将计算的结果缓存在局部变量表中,Java虚拟机将局部变量表当成一个数组,一次存放this指针(仅非静态方法),所传入的参数,以及字节码中的局部变量。和操作数栈一样,long类型以及double类型的值占据两个单元,其余类型占据一个单元。

栈桢中,与性能调优关系最密切的部份就是局部变量表,局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都不会被回收。

在方法执行时,虚拟机使用局部变量表完成方法的传递。

常量入栈指令

const系列 :

push系列:主要包括bipush 和 sipush 他们的区别在于,bipush接收8位整数作为参数,sipush接收16位整数,他们都将参数压入栈。

ldc系列:

算数运算指令

类型转换指令

对象的创建与访问指令

数组操作指令

类型检查指令

方法调用与返回指令

操作数栈管理指令

条件跳转指令

类的生命周期