JVM
一、 概述
计算机只能识别0和1,所以大家编写的程序都需要经过编译器,转换为由0和1组成的二进制本地机器码(Native Code)。随着虚拟机的不断发展,很多程序语言开始选择与操作系统和机器指令集无关的格式作为编译后的存储格式(Class文件),从而实现”Write Once, Run Anywhere”。 Java设计之初,考虑后期能让Java虚拟机运行其他语言,目前有越来越多的其他语言都可以直接需要在Java虚拟机,虚拟机只能识别Class文件,至于是由何种语言编译而来的,虚拟机并不关心,如下图:
可以看出不管是由Java语言,还是JRuby等其他语言,只能能生成.class字节码文件,就都可以运行在Java虚拟机上。故发布规范文档时,Java规范拆分为Java语言规范和Java虚拟机规范。
Java语法中定义各种变量、关键字、运算符的语义最终由多个字节码命令组合而成。因此字节码命令所能提供的语义描述能力必然要比Java语言本身更加强大。
二、Class组成
Class文件是一组以8位字节为单位的二进制流,中间没有任何分隔符,非常紧凑。 当需要占用8位以上的数据时,会按照Big-endian顺序,高位在前,低位在后的方式来分割成多个8位字节来存储。
- 任何一个Class文件都对应着唯一的类或接口的定义信息;
- 类或接口并不一定定义在文件里,也可以通过类加载器直接生成。
Java虚拟机规范规定:Class文件格式采用伪结构来存储数据,伪结构中只有无符号数和表这两种数据类型。
- 无符号数:是基本数据类型,以u1、u2、u4、u8分别代表1个字节、2个字节、4个字节、8个字节的无符号数。无符号数用于描述数字、索引引用、数量值、字符串值。
- 表:是由多个无符号数或者子表作为数据项构成的符合数据类型。用于描述有层次关系的复合结构的数据。整个Class其实就是一张表。
2.1 相关概念
下面介绍几个概述:
全限定名
是指把类全名中的“.”号,用“/”号替换,并且在最后加入一个“;”分号后生成的名称。比如java.lang.Object
对应的全限定名为java/lang/Object;
。
简单名
这个比较好理解,就是直接的方法名或者字段。比如toString()
方法,不需要包名作为前缀了。
字段描述符
用于描述字段的数据类型。
规则如下:
基本类型字符 | 对应类型 |
---|---|
B | byte |
C | char |
D | double |
F | float |
I | int |
S | short |
J | long |
Z | boolean |
V | void |
L+classname +; | 对象类型 |
[ | 数组类型 |
例如:
- 基本类型:int ==> I
- 对象类型:String ==> Ljava/lang/String;
- 数组类型:long[] ==> [J
方法描述符
用来描述方法的参数列表(数量、类型以及顺序)和返回值。
格式:(参数描述符列表)返回值描述符。 例如:Object m(int i, double d, Thread t) {..}
==>IDLjava/lang/Thread;)Ljava/lang/Object;
2.2 ClassFile结构
一个Class类文件是由一个ClassFile结构组成:
ClassFile {
u4 magic; //魔数,固定值0xCAFEBABE
u2 minor_version; //次版本号
u2 major_version; //主版本号
u2 constant_pool_count; //常量的个数
cp_info constant_pool[constant_pool_count-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文件的大小:26 + cp_info[] + u2[] + field_info[] + method_info[] + attribute_info[]
接下来,将具体来介绍ClassFile文件的各个组成部分。
三、ClassFile文件组成
3.1 魔数
每个Class文件头4个字节称为魔数(Magic Number),作用是用于确定这个Class文件是否能被虚拟机所接受,魔数固定值0xCAFEBABE。这是身份识别,比如jpeg等图片文件头也会有魔数。
3.2 版本号
紧跟魔数,也占用4个字节。从第5字节到第8字节存储的分别是 次版本号,主版本号。
3.3 常量池
常量池是Class文件空间最大的数据项之一,长度不固定。
a. 常量池长度 用u2类型代表常量池容量计数值,u2紧跟版本号。u2的大小等于常量池的常量个数+1。对于u2=0的特殊情况,代表没有使用常量池。
b. 常量池内容,格式如下:
cp_info {
u1 tag;
u1 info[];
}
包括两个类常量,字面量和符号引用:
- 字面量:与Java语言层面的常量概念相近,包含文本字符串、声明为final的常量值等。
- 符号引用:编译语言层面的概念,包括以下3类:
- 类和接口的全限定名
- 字段的名称和描述符
- 方法的名称和描述符
常量池中每一项常量都是一个表结构,每个表的开始第一位是u1类型的标志位tag, 代表当前这个常量的类型。在JDK 1.7.中共有14种不同的表结构的类型,如下:
Class文件都是二进制格式,可通过Jdk/bin/javap.exe
工具,分析Class文件字节码。关于javap用法,可通过javap --help
来查看。
3.4 访问标识
2个字节代表,标示用于识别一些类或者接口层次的访问信息.
标识名 | 标识值 | 解释 |
---|---|---|
ACC_PUBLIC | 0x0001 | 声明为public;可以从包外部访问 |
ACC_FINAL | 0x0010 | 被声明为final;不允许子类修改 |
ACC_SUPER | 0x0020 | 当被invokespecial指令调用时,将特殊对待父类的方法 |
ACC_INTERFACE | 0x0200 | 接口标识符 |
ACC_ABSTRACT | 0x0400 | 声明为abstract;不能被实例化 |
ACC_SYNTHETIC | 0x1000 | 声明为synthetic;不存在于源代码,由编译器生成 |
ACC_ANNOTATION | 0x2000 | 声明为注释类型 |
ACC_ENUM | 0x4000 | 声明为枚举类型 |
3.5 类/父类索引
当前类索引和父类索引占用大小都为u2类型,由于一个类智能继承一个父类,故父类索引只有一个。除了java.lang.Object对象的父类索引为0,其他所有类都有父类。
3.6 接口索引
一个类可以实现多个接口,故利用interfaces_count来记录该类所实现的接口个数,interfaces[interfaces_count]来记录所有实现的接口内容。
3.7 字段表
字段表用于描述类或接口中声明的变量,格式如下:
field_info {
u2 access_flags; //访问标识
u2 name_index; //名称索引
u2 descriptor_index; //描述符索引
u2 attributes_count; //属性个数
attribute_info attributes[attributes_count]; //属性表的具体内容
}
字段访问标识如下:(表中加粗项是字段独有的)
标识名 | 标识值 | 解释 |
---|---|---|
ACC_PUBLIC | 0x0001 | 声明为 public; 可以从包外部访问 |
ACC_PRIVATE | 0x0002 | 声明为 private; 只有定义的类可以访问 |
ACC_PROTECTED | 0x0004 | 声明为 protected;只有子类和相同package的类可访问 |
ACC_STATIC | 0x0008 | 声明为 static;属于类变量 |
ACC_FINAL | 0x0010 | 声明为 final; 对象构造后无法直接修改值 |
ACC_VOLATILE | 0x0040 | 声明为 volatile; 不会被缓存,直接刷新到主屏幕 |
ACC_TRANSIENT | 0x0080 | 声明为 transient; 不能被序列化 |
ACC_SYNTHETIC | 0x1000 | 声明为 synthetic; 不存在于源代码,由编译器生成 |
ACC_ENUM | 0x4000 | 声明为enum |
Java语法中,接口中的字段默认包含ACC_PUBLIC, ACC_STATIC, ACC_FINAL标识。ACC_FINAL,ACC_VOLATILE不能同时选择等规则。
紧跟其后的name_index和descriptor_index是对常量池的引用,分别代表着字段的简单名和方法的描述符。
3.8 方法表
方法表用于描述类或接口中声明的方法,格式如下:
method_info {
u2 access_flags; //访问标识
u2 name_index; //名称索引
u2 descriptor_index; //描述符索引
u2 attributes_count; //属性个数
attribute_info attributes[attributes_count]; //属性表的具体内容
}
方法访问标识如下:(表中加粗项是方法独有的)
标识名 | 标识值 | 解释 |
---|---|---|
ACC_PUBLIC | 0x0001 | 声明为 public; 可以从包外部访问 |
ACC_PRIVATE | 0x0002 | 声明为 private; 只有定义的类可以访问 |
ACC_PROTECTED | 0x0004 | 声明为 protected;只有子类和相同package的类可访问 |
ACC_STATIC | 0x0008 | 声明为 static;属于类变量 |
ACC_FINAL | 0x0010 | 声明为 final; 不能被覆写 |
ACC_SYNCHRONIZED | 0x0020 | 声明为 synchronized; 同步锁包裹 |
ACC_BRIDGE | 0x0040 | 桥接方法, 由编译器生成 |
ACC_VARARGS | 0x0080 | 声明为 接收不定长参数 |
ACC_NATIVE | 0x0100 | 声明为 native; 由非Java语言来实现 |
ACC_ABSTRACT | 0x0400 | 声明为 abstract; 没有提供实现 |
ACC_STRICT | 0x0800 | 声明为 strictfp; 浮点模式是FP-strict |
ACC_SYNTHETIC | 0x1000 | 声明为 synthetic; 不存在于源代码,由编译器生成 |
- 对于方法里的Java代码,进过编译器编译成字节码指令后,存放在方法属性表集合中“code”的属性内。
- 当子类没有覆写父类方法,则方法集合中不会出现父类的方法信息。
- Java语言中重载方法,必须与原方法同名,且特征签名不同。特征签名是指方法中各个参数在常量池的字段符号引用的集合,不包括返回值。当时Class文件格式中,特征签名范围更广,允许方法名和特征签名都相同,但返回值不同的方法,合法地共存子啊同一个Class文件中。
3.9 属性表
属性表格式:
attribute_info {
u2 attribute_name_index; //属性名索引
u4 attribute_length; //属性长度
u1 info[attribute_length]; //属性的具体内容
}
属性表的限制相对宽松,不需要各个属性表有严格的顺序,只有不与已有的属性名重复,任何自定义的编译器都可以向属性表中写入自定义的属性信息,Java虚拟机运行时会忽略掉无法识别的属性。 关于虚拟机规范中预定义的属性,这里不展开讲了,列举几个常用的。
属性名 | 使用位置 | 解释 |
---|---|---|
Code | 方法表 | 方法体的内容 |
ConstantValue | 字段表 | final关键字定义的常量值 |
Deprecated | 类、方法表、字段表 | 声明为deprecated |
InnerClasses | 类文件 | 内部类的列表 |
LineNumberTable | Code属性 | Java源码的行号与字节码指令的对应关系 |
LocalVariableTable | Code属性 | 方法的局部变量描述 |
Signature | 类、方法表、字段表 | 用于支持泛型的方法签名,由于Java的泛型采用擦除法,避免类型信息被擦除后导致签名混乱,Signature记录相关信息 |
Code属性 java程序方法体中的代码,经编译后得到的字节码指令存储在Code属性内,Code属性位于方法表的属性集合中。但与native或者abstract的方法则不会存在Code属性中。
Code属性的格式如下:
Code_attribute {
u2 attribute_name_index; //常量池中的uft8类型的索引,值固定为”Code“
u4 attribute_length; //属性值长度,为整个属性表长度-6
u2 max_stack; //操作数栈的最大深度值,jvm运行时根据该值佩服栈帧
u2 max_locals; //局部变量表最大存储空间,单位是slot
u4 code_length; // 字节码指令的个数
u1 code[code_length]; // 具体的字节码指令
u2 exception_table_length; //异常的个数
{ u2 start_pc;
u2 end_pc;
u2 handler_pc; //当字节码在[start_pc, end_pc)区间出现catch_type或子类,则转到handler_pc行继续处理。
u2 catch_type; //当catch_type=0,则任意异常都需转到handler_pc处理
} exception_table[exception_table_length]; //具体的异常内容
u2 attributes_count; //属性的个数
attribute_info attributes[attributes_count]; //具体的属性内容
}
- slot是虚拟机未局部变量分配内存使用的最小单位。对于byte/char/float/int/short/boolean/returnAddress等长度不超过32位的局部变量,每个占用1个Slot;对于long和double这两种64位的数据类型则需要2个Slot来存放。
- 实例方法中有隐藏参数this, 显式异常处理器的参数,方法体定义的局部变量都使用局部变量表来存放。
- max_locals,不是所有局部变量所占Slot之和,因为Slot可以重用,javac编译器会根据变量的作用域来分配Slot给各个变量使用,从而计算出max_locals大小。
- 虚拟机规范限制严格方法不允许超过65535个字节码,否则拒绝编译。
Code属性是Class文件中最重要的属性,Java程序的幸福课分为代码(方法体中的Java代码)和元数据(包含类、接口、字段、方法定义以及其他信息)两部分。
ConstantValue属性 ConstantValue属性是指被static关键字修饰的变量(也称为类变量)。
- 类变量: 在类构造器方法或者使用ConstantValue属性来赋值
- 实例变量:在实例构造器方法进行赋值
Java虚拟机采用基于栈的架构,其指令由操作码和操作数组成。
- 操作码:一个字节长度(0~255),意味着指令集的操作码个数不能操作256条。
- 操作数:一条指令可以有零或者多个操作数,且操作数可以是1个或者多个字节。编译后的代码没有采用操作数长度对齐方式,比如16位无符号整数需使用两个字节储存(假设为byte1和byte2),那么真实值是
(byte1 << 8) | byte2
。
放弃操作数对齐操作数对齐方案:
- 优势:可以省略很多填充和间隔符号,从而减少数据量,具有更高的传输效率;Java起初就是为了面向网络、智能家具而设计的,故更加注重传输效率。
- 劣势:运行时从字节码里构建出具体数据结构,需要花费部分CPU时间,从而导致解释执行字节码会损失部分性能。
二、指令
大多数指令包含了其操作所对应的数据类型信息,比如iload,表示从局部变量表中加载int型的数据到操作数栈;而fload表示加载float型数据到操作数栈。由于操作码长度只有1Byte,因此Java虚拟机的指令集对于特定操作只提供有限的类型相关指令,并非为每一种数据类型都有相应的操作指令。必要时,有些指令可用于将不支持的类型转换为可被支持的类型。
对于byte,short,char,boolean类型,往往没有单独的操作码,通过编译器在编译期或者运行期将其扩展。对于byte,short采用带符号扩展,chart,boolean采用零位扩展。相应的数组也是采用类似的扩展方式转换为int类型的字节码来处理。 下面分门别类来介绍Java虚拟机指令,都以int类型的数据操作为例。
栈是指操作数栈
2.1 栈操作相关
load和store
- load 命令:用于将局部变量表的指定位置的相应类型变量加载到栈顶;
- store命令:用于将栈顶的相应类型数据保入局部变量表的指定位置;
变量进栈 | 含义 | 变量保存 | 含义 |
---|---|---|---|
iload | 第1个int型变量进栈 | istore | 栈顶nt数值存入第1局部变量 |
iload_0 | 第1个int型变量进栈 | istore_0 | 栈顶int数值存入第1局部变量 |
iload_1 | 第2个int型变量进栈 | istore_1 | 栈顶int数值存入第2局部变量 |
iload_2 | 第3个int型变量进栈 | istore_2 | 栈顶int数值存入第3局部变量 |
iload_3 | 第4个int型变量进栈 | istore_3 | 栈顶int数值存入第4局部变量 |
lload | 第1个long型变量进栈 | lstore | 栈顶long数值存入第1局部变量 |
fload | 第1个float型变量进栈 | fstore | 栈顶float数值存入第1局部变量 |
dload | 第1个double型变量进栈 | dstore | 栈顶double数值存入第1局部变量 |
aload | 第1个ref型变量进栈 | astore | 栈顶ref对象存入第1局部变量 |
const、push和ldc
- const、push:将相应类型的常量放入栈顶
- ldc:则是从常量池中将常量
常量进栈 | 含义 |
---|---|
aconst_null | null进栈 |
iconst_m1 | int型常量-1进栈 |
iconst_0 | int型常量0进栈 |
iconst_1 | int型常量1进栈 |
iconst_2 | int型常量2进栈 |
iconst_3 | int型常量3进栈 |
iconst_4 | int型常量4进栈 |
iconst_5 | int型常量5进栈 |
lconst_0 | long型常量0进栈 |
fconst_0 | float型常量0进栈 |
dconst_0 | double型常量0进栈 |
bipush | byte型常量进栈 |
sipush | short型常量进栈 |
常量池操作 | 含义 |
---|---|
ldc | int、float或String型常量从常量池推送至栈顶 |
ldc_w | int、float或String型常量从常量池推送至栈顶(宽索引) |
ldc2_w | long或double型常量从常量池推送至栈顶(宽索引) |
pop和dup
- pop用于栈顶数值出栈操作;
- dup用于赋值栈顶的指定个数的数值,并将其压入栈顶指定次数;
栈顶操作 | 含义 |
---|---|
pop | 栈顶数值出栈(不能是long/double) |
pop2 | 栈顶数值出栈(long/double型1个,其他2个) |
dup | 复制栈顶数值,并压入栈顶 |
dup_x1 | 复制栈顶数值,并压入栈顶2次 |
dup_x2 | 复制栈顶数值,并压入栈顶3次 |
dup2 | 复制栈顶2个数值,并压入栈顶 |
dup2_x1 | 复制栈顶2个数值,并压入栈顶2次 |
dup2_x2 | 复制栈顶2个数值,并压入栈顶3次 |
swap | 栈顶的两个数值互换,且不能是long/double |
注意:dup2对于long、double类型的数据就是一个,对于其他类型的数据,才是真正的两个,这个的2代表的是2个slot的数据。
2.2 对象相关
字段调用
字段调用 | 含义 |
---|---|
getstatic | 获取类的静态字段,将其值压入栈顶 |
putstatic | 给类的静态字段赋值 |
getfield | 获取对象的字段,将其值压入栈顶 |
putfield | 给对象的字段赋值 |
方法调用
方法调用 | 作用 | 解释 |
---|---|---|
invokevirtual | 调用实例方法 | 虚方法分派 |
invokestatic | 调用类方法 | static方法 |
invokeinterface | 调用接口方法 | 运行时搜索合适方法调用 |
invokespecial | 调用特殊实例方法 | 包括实例初始化方法、父类方法 |
invokedynamic | 由用户引导方法决定 | 运行时动态解析出调用点限定符所引用方法 |
方法返回
方法返回 | 含义 |
---|---|
ireturn | 当前方法返回int |
lreturn | 当前方法返回long |
freturn | 当前方法返回float |
dreturn | 当前方法返回double |
areturn | 当前方法返回ref |
对象和数组
- 创建类实例: new
- 创建数组:newarray、anewarray、multianewarray
- 数组元素 加载到 操作数栈:xaload (x可为b,c,s,i,l,f,d,a)
- 操作数栈的值 存储到数组元素: xastore (x可为b,c,s,i,l,f,d,a)
- 数组长度:arraylength
- 类实例类型:instanceof、checkcast
2.3 运算指令
运算指令是用于对操作数栈上的两个数值进行某种运算,并把结果重新存入到操作栈顶。Java虚拟机只支持整型和浮点型两类数据的运算指令,所有指令如下:
运算 | int | long | float | double |
---|---|---|---|---|
加法 | iadd | ladd | fadd | dadd |
减法 | isub | lsub | fsub | dsub |
乘法 | imul | lmul | fmul | dmul |
除法 | idiv | ldiv | fdiv | ddiv |
求余 | irem | lrem | frem | drem |
取反 | ineg | lneg | fneg | dneg |
其他运算:
- 位移:ishl,ishr,iushr,lshl,lshr,lushr
- 按位或: ior,lor
- 按位与: iand, land
- 按位异或: ixor, lxor
- 自增:iin
- 比较:dcmpg,dcmpl,fcmpg,fcmpl,lcmp
2.4 类型转换
类型转换用于将两种不同类型的数值进行转换。
(1) 对于宽化类型转换(小范围向大范围转换),无需显式的转换指令,并且是安全的操作。各种范围从小到大依次排序: int, long, float, double。
(2)对于窄化类型转换,必须显式地调用类型转换指令,并且该过程很可能导致精度丢失。转换规则中需要特别注意的是当浮点值为NaN, 则转换结果为int或long的0。虽然窄化运算可能会发生上/下限溢出和精度丢失等情况,但虚拟机规范明确规定窄化转换U不可能导致虚拟机抛出异常。
类型转换指令:i2b, i2c,f2i
等等。
2.5 流程控制
控制指令是指有条件或无条件地修改PC寄存器的值,从而达到控制流程的目标
- 条件分支:ifeq、iflt、ifnull、ifnonnull等
- 复合分支:tableswitch、lookupswitch
- 无条件分支:goto、goto_w、jsr、jsr_w、ret
2.6 同步与异常
异常:
Java程序显式抛出异常: athrow指令。在Java虚拟机中,处理异常(catch语句)不是由字节码指令来实现,而是采用异常表来完成。
同步:
方法级的同步和方法内部分代码的同步,都是依靠管程(Monitor)来实现的。
Java语言使用synchronized语句块,那么Java虚拟机的指令集中通过monitorenter和monitorexit两条指令来完成synchronized的功能。为了保证monitorenter和monitorexit指令一定能成对的调用(不管方法正常结束还是异常结束),编译器会自动生成一个异常处理器,该异常处理器的主要目的是用于执行monitorexit指令。
2.7 小结
在基于堆栈的的虚拟机中,指令的主战场便是操作数栈,除了load是从局部变量表加载数据到操作数栈以及store储存数据到局部变量表,其余指令基本都是用于操作数栈的。
一、概述
本文主要讲述虚拟机如何把 Class文件加载到内存的过程。校验、转换解析和初始化,最终形成可被虚拟机使用的Java类型,这就是虚拟机的类加载机制。类型的加载、连接和初始化都是在程序运行期间完成,这样做的优劣势,如下:
- 优势:提高Java程序的灵活性,Java动态扩展的语言特性就是依赖运行期动态加载和动态连接。当面向接口的应用程序,可以等到运行时指定实现类;可以通过类加载器,让程序运行时加载一个二进制流作为程序一部分。
- 劣势:增加类加载的性能开销。
二、 类加载的生命周期
类的生命周期是指把Class字节码从文件中加载到内存,直到卸载内存整个过程,分为7个步骤。
图中用红色圈起来的3个过程分别为验证、准备、解析,它们合称为链接(Linking)过程。另外图中紫色的5项是严格按照执行。而蓝色的解析阶段不一定要在初始化之前, 也可以在初始化之后再解析,这种情况称为动态绑定或晚期绑定。
1. 加载
虚拟机在加载阶段,主要工作如下:
- 通过
类的全限定名
获取该类的二进制字节流; - 将字节流所代表的静态存储结构 转化为 方法区的运行时数据结构;
- 生成代表该类的Class对象并存放方法区,作为方法区该类的各种数据的访问入口。
对于上述字节流,可能来源:
- 压缩包,例如jar/war等格式;
- 网络,典型场景applet;
- 运行时计算生成,例如动态代理技术,在java.lang.reflect.Proxy中,利用ProxyGenerator.generateProxyClass来为特定接口生成形如“*$Proxy”的代理类的二进制字节流;
- 数据库,例如中间件服务器(SAP Netweaver)。
注:对于数组类,不通过类加载器创建,而是由虚拟机直接创建的。另外加载阶段尚未完成,连接阶段可能已经开始。
2. 验证
验证是连接阶段(Linking)的第一步,目的是为了确保Class文件的字节流符合虚拟机规范,不会危害虚拟机自身安全。比如:访问数组越界问题,将对象转型为未实现的类型,跳转到不存在的代码区等情绪编译器都会拒绝编译,也就是无法生成Class文件,既然如此,为什么还要验证呢?原因是Class文件不一定都是由java源码编译而成,可以是任何途径,所以验证还是很有必要的,尽可能保证系统能承受住恶意代码攻击。
验证主要工作分4阶段:
- 文件格式验证:验证是否符合Class文件格式规范;
- 元数据验证:验证是否符合Java语言规范;
- 字节码验证:验证数据流和控制流分析;
- 符号引用验证:验证符号引用转化为直接引用。
2.1 文件格式验证
验证点有比如是否魔数0xCAFEBABE开头;主、次版本号是否范围之内;常量池中常量tag标示是否正确等等,只有通过全部的验证,才能把字节流存储到内存的方法区。
2.2 元数据验证
经过文件格式验证,字节流已加载到方法区,这个阶段工作是对方法区的字节码进行语义分析,保证符合Java语言规范。 验证点比如:
- 该类是否有父类(除Object之外,所有类都应该有父类)
- 该类是否继承不允许继承的类(final类)
- 非抽象类,是否都实现其父类的抽象方法或接口中的方法
- 类的字段、方法是否与父类矛盾(例如覆盖父类的final字段,或重载不符合规则)
- … 除上面列举外,还有很多。经过元数据验证,能确保元数据都是符合规范。
2.3 字节码验证
比如操作数栈的数据类型和指令代码序列配合,跳转指令不会跳到方法体之外等。HotSpot虚拟机提供 -XX:-UseSplitVerifier选项来关闭这项优化。
2.4 符号引用验证
校验点:
- 符号引用中通过字符串描述的全限定名是否能找到对应的类;
- 在指定类是否存在符合方法的字段描述符以及简单名称所描述的方法和字段;
- 符号引用中的类、字段、方法的访问权限检查。
- …等等
对于虚拟机的类加载机制来说,验证阶段非常重要的,但不是一定必要的。如果所运行的全部代码(包含自己编写以及第三方包的代码)都已经被反复使用和验证过,那么可以考虑使用 -Xverify:none参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。
3. 准备
主要工作:static变量
分配内存,并设置类变量的初始值的阶段。
(1). 类变量:赋予零值
数据类型的零值表,如下:
类型 | int | long | float | double | short | byte | char | boolean | reference |
零值 | 0 | 0L | 0.0f | 0.0d | (short)0 | (byte)0 | ‘\u0000’ | false | null |
例如:
public static int value = 10;
在准备阶段,会为变量value
在方法区分配内存并初始化零值,即value=0
,而非10。 因为对于value的赋值10,是由putstatic
指令完成。该指令是在java程序被编译后,存放在类构造器<clinit>
方法之中。所以 value
=10的操作是在类初始化的时候才发生,故类变量在准备期value=0
。
(2). 常量:赋予真实值
例如:
public static final int value = 10;
对于常量,准备阶段会把类字段的字段属性表中的ConstantValue属性所指定的值(此处是10),赋给常量(value
),故常量在准备期间value =10
;
(3). 实例变量:不赋任何值
该阶段仅对类变量进行内存分配,而对于实例变量(或者称呼为成员变量)并不会分配内存,也就更不用提赋值的事。实例变量的初始化,是随着对象实例化时在Java堆上分配内存而进行的。
4. 解析
主要工作:虚拟机将常量池内的符号引用替换为直接引用的阶段。
先解析下符号引用和直接引用的概念
- 符号引用(Symbolic Reference):以一组符号来描述引用目标,符号可以是任意形式的字面量。只能要准确定位到目标即可。符号引用与虚拟机的内存布局无关,引用的目标也不一定存在内存。这样兼容性强,各种虚拟机只需要能接受符号引用即可。
- 直接引用(Direct Reference):直接引用就是指向目标的指针、相对偏移量或者能简介定位到目标的句柄。直接引用和虚拟机内存布局息息相关。直接引用的目标必然存在与内存中。
同一个符号引用 在不同的虚拟机中解析出来的直接引用地址一般都是不相同的;同一个符号引用,在同一个虚拟机下,多次解析时,会对第一次解析结果进行缓存(常量池记录直接引用,并标记已解析状态),从而避免多次解析。
特殊情形,对于invokedynamic指令,不会进行缓存过程,每次使用前都会进行解析。
5. 初始化
主工作:主要是执行类构造器方法clinit。(class init的简称)
类初始化阶段是类加载的最后一个阶段。在初始化之前的过程中,用户可控的地方只有通过自定义类加载器参与,其余都是虚拟机主导和控制。
到了初始化,才开始真正的执行类中定义的Java程序代码。
(1). 类的构造方法
类构造方法是由编译器自动收集源文件中的类变量赋值操作
和静态语句块
合并而成的。收集顺序是由语句在源文件的顺序所决定。故静态语句块只能访问定义之前的静态变量;对于定义之后的变量可以赋值,但不能访问。
- clinit方法与类的实例构造方法init不同,
clinit方法不需要显式调用父类构造器
,虚拟机会保证子类的clinit方法执行之前,父类的clinit方法已经执行完毕。故第一个被执行的clinit方法的类肯定是java.lang.Object; clinit方法不是必需的
,对于没有静态块和类变量赋值操作,编译器不会生成clinit方法。父类静态语句和静态变量赋值优先于子类
.interface中不能有静态语句块
,但仍可以有变量初始化的赋值操作,也可以生成clinit方法。但接口和类的不同是,执行接口的clinit方法不需要先执行父接口的clinit方法。只有当父接口中定义的变量使用时,父接口才会初始化。- 虚拟机保证类构造方法可以多线程正确执行,会加锁、同步的操作。 一个线程执行类构造方法,其他线程
阻塞等待
,当类构造方法有耗时操作,会造成多进程的阻塞,往往比较隐蔽。
(2). 类初始化时机
虚拟机规范中严格规定有且只有5种情况下,当类没有初始化时必须立即对类进行初始化:
- 遇到
new
、getstatic
、putstatic
或invokeStatic
这4条字节码指令时。常见场景:- 使用new关键字实例化对象时,触发
new
; - 读取类变量时,触发
getstatic
;(final常量除外) - 设置类变量时,触发
putstatic
; - 调用类的静态方法时,触发
invokeStatic
;
- 使用new关键字实例化对象时,触发
- 虚拟机启动时,需指定一个要执行的主类(含有
main()
的类),虚拟机会先初始化该类; - 初始化一个类时,当其父类没有初始化,则需要先触发其父类的初始化;
- 使用
java.lang.reflect
包中的方法对类进行反射调用时; - 当
java.lang.invoke.MethodHandle
实例最后的解析结果为REF_getStatic
、REF_putStatic
、REF_invokeStatic
的方法句柄,并且该句柄所对应的类没有进行过初始化;
上面讲到final常量不能触发类初始化,是由于在编译时已把数据放入常量池的静态字段,当读取类的static final
字段时,并不需要初始化类,而是从常量池中去获取相应的数据。
上述的5种场景的行为都是对类的一个主动引用过程。除此之外,还有被动引用并不会除非类的初始化过程。 另外
一、概述
执行引擎是Java虚拟机非常最核心的部分,对于物理即的执行引擎是直接建立在处理器、硬件、指令集合操作系统层面,而虚拟机执行引擎则是由自行定制的指令集与执行引擎的结构体系。执行引擎在执行Java会有解释执行(通过解释器)和编译执行(通过JIT生成的本地代码)两种选择,对于Android ART又多了一种提前编译器(AOT)。
接下来,主要讲解虚拟机的方法执行过程,对于Java虚拟机的解释器的执行模型(不考虑异常处理):
do {
atomically calculate pc and fetch opcode at pc;
if (operands) fetch operands;
execute the action for the opcode;
} while (there is more to do);
对象创建
对象创建,不包括数组和Class对象,例如 Person person = new Person()
,
当虚拟机遇到new指令时:
- 在常量池中查找“Person”,看能否定位到Person类的符号引用;如果能,则继续执行。
- 再检查Person类是否已经加载、解析和初始化;如果没有初始化,则先执行类加载过程。
- 类加载后,虚拟机为新生成的person对象在堆上分配相应大小的内存。(对象大小在类加载后确定)
- 内存分配后,虚拟机将分配的内存空间都初始化为零值(不包括对象头)。实例变量不赋初值也能使用对应的零值。
- 设置对象头信息,比如对象的哈希值,gc分代年龄等。
从虚拟机角度,到此一个新的对象已经创建完成。但从Java视角,对象才刚刚开始,init构造方法还没有执行,所有字段还是零。执行完init方法,按java程序的构造方法进行初始化后,对象便是彻底创建完成。