JVM不和Java在内任何语言绑定,只与class文件,这种特定的二进制文件格式关联。Class字节码文件中包含虚拟机指令集和符号表,以及若干其它辅助信息
无关性的基石
虚拟机发展到JDK 1.7~1.8的时候,JVM设计者通过JSR-292实现了其他语言运行与JVM上
class文件是一组以8位字节为基础单位的二进制流,紧凑,中间无分隔符。
当遇到需要占用8位字节以上空间的数据项,按Big-Endian方式分割成若干个8位字节
Class文件格式,是一种类似于C语言结构体的伪结构来存储数据。这种伪结构只有两种数据类型
整个Class文件,本质上就是一张表
结构总览
每个字节码文件的头4个字节,一定是0xCAFEBABE,这个数值叫做Magic Number
u4这一无符号数(是一种数据类型,看上文)可以表示magic
紧接着4个字节的魔数之后,第5和第6个字节是次版本号(Minor Version)
第7和第8个字节是Class文件的主版本号(Major Version)
接着第8个字节的,是常量迟Constant Pool的入口
由于常量池数量不固定,所以在入口,需要一项u2类型的计数器,constant_pool_count,这是第9个字节
这个技术器跟习惯的不一样,从1开始算,不是0,0带表”不引用任何一个常量池项目”
存放数据类型:
Java代码进行javac编译时,不像C/C++那样,有”连接”的步骤。
而是在虚拟机加载class文件的时候,进行动态连接。即,Class文件不会保存各个方法、字段的最终内存布局信息。因此,这些字段、方法的引用,不经过运行期转换的话,无法得到真正内存的入口地址,也就无法直接被虚拟机使用
常量池中的每一个常量,都是表
JDK 1.7 为更好支持 动态语言调用,新增CONSTANT_MethodHandle_info,CONSTANT_MethodType_info,CONSTANT_InvokeDynamic_info
表的结构组成
常量池的表除了tag,还有自己不同的结构
例如:
CONSTANT_MethodHandle_info
- tag(u1类型)
- name_index(u2类型) 索引值,指向CONSTANT_Utf8_info
,代表类或接口的全限定名
class文件中,方法和字段,都需要引用CONSTANT_Utf8_info
型常量来描述名称,所以utf8_info常量的最大长度,就是Java中方法、字段名的最大长度,u2类型,2个字节,最大值为65535。所以,当定义了超过64K英文字符的变量或方法名,JVM无法编译
JDK的bin目录下有个javap工具,是Oracle为我们准备的查看class文件的工具,运行例子如下图
常量池结束后,接着,是2个字节的访问标志access_flags
access_flags一共有16个标志位,当前只定义了8个
这个标志用于识别访问权限、访问层次的信息,例如:
访问标志之后,是类索引(this_class)、父类索引(super_class)、接口索引集合(interfaces)。前面两个是u2类型数据,接口则是u2类型数据的集合
class文件由这3项数据来确定这个类的继承关系
除了java.lang.Object外,所有类的父类索引都不为0
类索引/父类索引–>持有2个u2类型–>指向CONSTANT_Class_info的类描述符常量–>找到定义在Utf_info中的全限定名
接口索引集合,入口的第一项,是u2类型的数据,接口计数器(interfaces_count),表示索引的容量,如果类没有实现接口,那么计数器的值为0
字段表(field_info)用于描述接口,或者类中声明的变量
字段表结构
field包括类变量和实例变量,但不包括方法内部声明的局部变量
Java中描述一个字段(field)的标签:
名称、数据类型适合引用常量池的常量来描述,而其他剩下的,适合用布尔值描述
字段表包含的固定数据项目,到descriptor_index就结束了
下图
理解了上面的字段表的内容,接下来,方法表可以如法炮制。
Java中描述一个方法的标签:
方法表结构
方法没有volatile和transient关键字,所以access_flag就没有这两个标志了,跟方法对应的,增加6个标志
标志名称 | 标志值 | 含义 |
---|---|---|
ACC_SYNCHRONIZED | 0x0020 | 是否为synchronized |
ACC_BRIDGE | 0x0040 | 是否是由编译器产生的桥接方法 |
ACC_VARARGS | 0x0080 | 是否接受不定参数 |
ACC_NATIVE | 0x0100 | 是否为native |
ACC_ABSTRACT | 0x0400 | 是否为abstract |
ACC_STRICPFP | 0x0800 | 是否为strictfp |
方法里的代码,经过编译器编译成字节码指令,存放在方法属性表集合的”code”属性中
重载overload一个方法,除了要有与原方法相同的简单名称之外,还必须有一个与原方法不同的特征签名,特征签名,是一个方法中,各个参数在常量池中的字段符号的引用的集合,也就是说,方法参数列表要不一样
属性表attribute_info,在class文件、字段表、方法表内,都可以存在,描述某些场景专有的信息
属性表的数据项目比较多,这里列出一些重要的
名称 | 使用位置 | 含义 |
---|---|---|
Code | 方法表 | Java代码编译成的字节码指令 |
ConstantValue | 字段表 | final定义的常量值 |
Exceptions | 方法表 | 方法抛出的异常 |
EnclosingMethod | 类文件 | 仅当一个类为内部类,或匿名类时才有的属性,用于标识包着这个类的外围方法 |
InnerClass | 类文件 | 内部类列表 |
LineNumberTable | Code属性 | Java源码与字节码指令的对应关系 |
LocalVariableTable | Code属性 | 方法的局部变量描述 |
BootStrapMethods | 类文件 | JDK 1.7新增,保存invokedynamic指令引用的引导方法限定符 |
属性表结构
Code属性
Java方法体中的代码,经过编译器处理后,最终变为字节码指令,存储在Code属性中
构成
ConstantValue属性
只有类变量/静态变量才有的属性。虚拟机对类变量和实例变量的赋值方式以及时机有所不同
实例变量的赋值
类变量的赋值
Java虚拟机的指令构成:
採用面向操作数栈,而不是寄存器的架构
Java虚拟机的解释器,基本执行模型
do {
自动计算PC寄存器的值,+1;
根据PC寄存器的指示位置,从字节码流中取出操作码;
if (字节码存在操作数)
从字节码流中取出操作数;
执行操作码定义的操作
} while(字节码流长度 > 0)
用于 栈帧中的局部变量表和操作数栈 之间的来回传输
将1个局部变量加载到操作栈
iload, iload_,lload,lload_,fload,fload_,dload,dload_,aload,aload_
将1个数值从操作数栈存储到局部变量表
istore,istore_,lstore,lstore_,fstore,ftore_,dstore,dstore_,astore,astore_
将1个常量加载到操作数栈
bipush,sipush,ldc,ldc_w,ldc2_w,aconst_null,iconst_ml,iconst_,lconst_,fconst_,dconst_
扩充局部变量表的访问索引的指令
wide
运算或算术指令,是对两个操作数栈上的值,做某种特定运算,并将运算结果重新存入操作数栈的栈顶
没有直接byte,char,short,boolean类型的算术指令,用操作int类型的指令代替
整型与浮点型在溢出和被0整除时,有不同行为表现
虽然类实例和数组都是对象,但虚拟机对这两者的创建与操作,使用了不同的字节码指令
new
newarray
,anewarray
,multianewarray
getfield
,putfield
,getstatic
,putstatic
baload
,caload
,saload
,iaload
,laload
,faload
,daload
,aaload
bastore
,castore
,sastore
,iastore
,fastore
,dastore
,aastore
arraylength
instanceof
,checkcast
操作数栈的操作,无非是压栈、弹栈
pop
,pop2
dup
,dup2
,dup_x1
,dup2_x1
,dup_x2
,dup2_x2
swap
使得Java虚拟机有条件,或无条件,从指定的位置指令,继续执行程序,而不是控制转移指令的下一条指令。
概念模型上,可以理解为,在有条件/无条件地修改PC寄存器的值
条件分支:ifeq
,iflt
,ifle
,ifne
,ifgt
,ifge
,ifnull
,ifnonnull
,if_icmpeq
,if_icmpne
,if_icmplt
,if_icmpgt
,if_icmple
,if_icmpge
,if_acmpeq
,if_acmpne
复合条件分支:tableswitch
,lookupswitch
goto
,goto_w
,jsr
,jsr_w
,ret
invokevirtual
: 调用对象实例方法invokeinterface
:调用接口方法invokespecial
:调用需要特殊处理的实例方法,如实例初始化方法、私有方法、父类方法invokestatic
:调用类方法invokedynamic
:运行时,动态解析出调用点限定符所引用的方法,并执行该方法;分派逻辑由用户设定的引导方法决定使用异常表
Java虚拟机支持方法级别的同步,以及某段代码(指令序列)的同步,通过管程(Monitor)来支持,叫监视器不挺好的嘛。
方法级别的同步是隐式的,无须通过字节码指令来控制,方法的常量池的方法表结构中的访问标志ACC_SYNCHRONIZED,可以得知,方法是否为同步方法
同步代码块,需要monitorenter
,monitorexit
2条关键指令
虚拟机的实现方式,主要有