JVM不和Java在内任何语言绑定,只与class文件,这种特定的二进制文件格式关联。Class字节码文件中包含虚拟机指令集和符号表,以及若干其它辅助信息
无关性的基石
- 虚拟机
- 字节码存储格式
虚拟机发展到JDK 1.7~1.8的时候,JVM设计者通过JSR-292实现了其他语言运行与JVM上
Class类文件的结构
class文件是一组以8位字节为基础单位的二进制流,紧凑,中间无分隔符。
当遇到需要占用8位字节以上空间的数据项,按Big-Endian方式分割成若干个8位字节
Class文件格式,是一种类似于C语言结构体的伪结构来存储数据。这种伪结构只有两种数据类型
-
无符号数
- 可以描述 数字、索引引用、数量值、按照UTF-8编码构成字符串值 表
- 描述有层次关系的符合结构的数据。多个无符号数或者其他表作为数据项构成的复合数据类型,所有表,习惯性地以”_info”结尾
- 无符号数包括:
- u1–>1 byte(8 bits)
- u2–>2 byte2(16 bits)
- u4–>4 bytes(32 bits)
- u8–>8 bytes(64 bits)
- 表包括:
- 多个无符号数
- 其他表
整个Class文件,本质上就是一张表
结构总览
- Magic Number
- Version
- Constant Pool
- Access Flags
- This Class Name
- Super Class Name
- Interfaces
- Fields
- Methods
- Attributes
魔数(Magic Number)与Class文件的版本(Version)
每个字节码文件的头4个字节,一定是0xCAFEBABE,这个数值叫做Magic Number
u4这一无符号数(是一种数据类型,看上文)可以表示magic
-
作用
- 魔数的唯一作用,就是确定这个文件是否为一个能被虚拟机接收的字节码文件
紧接着4个字节的魔数之后,第5和第6个字节是次版本号(Minor Version)
第7和第8个字节是Class文件的主版本号(Major Version)
常量池(Constant Pool)
接着第8个字节的,是常量迟Constant Pool的入口
- 可以理解為Class文件中的资源仓库
- 与其他项目关联最多的数据类型
- 占用Class文件空间最大的数据类型
- Class文件中第一个出现的表类型数据项目
由于常量池数量不固定,所以在入口,需要一项u2类型的计数器,constant_pool_count,这是第9个字节
这个技术器跟习惯的不一样,从1开始算,不是0,0带表”不引用任何一个常量池项目”
存放数据类型:
- 字面量(literal).
- 文本字符串
- final修饰的常量
- 符号引用(Symbolic References),编译原理方面的概念
- 类和接口的全限定名(Fully Qualified Name)
- 字段的名称和描述符(Descriptor)
- 方法的名称和描述符
Java代码进行javac编译时,不像C/C++那样,有”连接”的步骤。
而是在虚拟机加载class文件的时候,进行动态连接。即,Class文件不会保存各个方法、字段的最终内存布局信息。因此,这些字段、方法的引用,不经过运行期转换的话,无法得到真正内存的入口地址,也就无法直接被虚拟机使用
常量池中的每一个常量,都是表
JDK 1.7 为更好支持 动态语言调用,新增CONSTANT_MethodHandle_info,CONSTANT_MethodType_info,CONSTANT_InvokeDynamic_info
表的结构组成
常量池的表除了tag,还有自己不同的结构
- u1类型的标志位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文件的工具,运行例子如下图
访问标志(Access Flags)
常量池结束后,接着,是2个字节的访问标志access_flags
access_flags一共有16个标志位,当前只定义了8个
这个标志用于识别访问权限、访问层次的信息,例如:
- 这个class是类还是接口
- 是否public类型
- 是否abstract
- 如果是类,是否声明为final
下图表
类索引、父类索引与接口索引集合(This Class,Super Class, Interfaces)
访问标志之后,是类索引(this_class)、父类索引(super_class)、接口索引集合(interfaces)。前面两个是u2类型数据,接口则是u2类型数据的集合
class文件由这3项数据来确定这个类的继承关系
除了java.lang.Object外,所有类的父类索引都不为0
类索引/父类索引–>持有2个u2类型–>指向CONSTANT_Class_info的类描述符常量–>找到定义在Utf_info中的全限定名
接口索引集合,入口的第一项,是u2类型的数据,接口计数器(interfaces_count),表示索引的容量,如果类没有实现接口,那么计数器的值为0
字段表集合(Fields)
字段表(field_info)用于描述接口,或者类中声明的变量
字段表结构
- access_flag(u2,分清上面的对类定义的access_flag)
- name_index(u2)
- descriptor_index(u2)
- attributes_count(u2)
- attributes(attribute_info)
field包括类变量和实例变量,但不包括方法内部声明的局部变量
Java中描述一个字段(field)的标签:
- 作用域(public,protect,default,private)
- 类变量还是实例变量(static)
- 可变性(final)
- 并发可见性(volatile,是否强制从主存读写)
- 是否参与序列化(transient)
- 类型(primitive,reference,array)
- 名称(name)
名称、数据类型适合引用常量池的常量来描述,而其他剩下的,适合用布尔值描述
字段表包含的固定数据项目,到descriptor_index就结束了
下图
方法表集合(Methods)
理解了上面的字段表的内容,接下来,方法表可以如法炮制。
Java中描述一个方法的标签:
- 作用域(public,protect,default,private)
- 类方法还是实例方法(static)
- 可变性(final)
- 并发同步性(synchronized)
- 是否严格精度(strictfp)
- 是否抽象(abstract)
- 是否native(native)
- 名称(name)
方法表结构
- access_flag(u2)
- name_index(u2)
- descriptor_index(u2)
- attributes_count(u2)
- attributes(attribute_info)
方法没有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一个方法,除了要有与原方法相同的简单名称之外,还必须有一个与原方法不同的特征签名,特征签名,是一个方法中,各个参数在常量池中的字段符号的引用的集合,也就是说,方法参数列表要不一样
属性表集合(Attributes)
属性表attribute_info,在class文件、字段表、方法表内,都可以存在,描述某些场景专有的信息
属性表的数据项目比较多,这里列出一些重要的
名称 | 使用位置 | 含义 |
---|---|---|
Code | 方法表 | Java代码编译成的字节码指令 |
ConstantValue | 字段表 | final定义的常量值 |
Exceptions | 方法表 | 方法抛出的异常 |
EnclosingMethod | 类文件 | 仅当一个类为内部类,或匿名类时才有的属性,用于标识包着这个类的外围方法 |
InnerClass | 类文件 | 内部类列表 |
LineNumberTable | Code属性 | Java源码与字节码指令的对应关系 |
LocalVariableTable | Code属性 | 方法的局部变量描述 |
BootStrapMethods | 类文件 | JDK 1.7新增,保存invokedynamic指令引用的引导方法限定符 |
属性表结构
- attribute_name_index(u2)
- attribute_length(u4)
- info(u1)
Code属性
Java方法体中的代码,经过编译器处理后,最终变为字节码指令,存储在Code属性中
构成
- attribute_name_index(u2)
- 指向CONTANT_Utf8_info类型常量索引,值为”Code”,
- attribute_length(u4)
- max_stack
- 代表操作数栈(Operand Stacks)深度的最大值
- 虚拟机根据这个值来分配栈帧中的操作栈
- max_locals
- 局部变量表所需的存储空间
- 存储基本单位是Slot(32位)
- 局部变量表中的Slot可重用
- 方法参数、方法体中定义的局部变量
- code_length
- code
- exception_table_length
- exception_table
- attributes_count
- attributes
ConstantValue属性
只有类变量/静态变量才有的属性。虚拟机对类变量和实例变量的赋值方式以及时机有所不同
实例变量的赋值
- 在实例构造器中方法中进行
类变量的赋值
- 可以在类构造器方法
- 使用ConstantValue属性
字节码指令
Java虚拟机的指令构成:
- 1个字节长度的操作码(Opcode)
- 0到多个操作数(Oprands,代表此操作所需参数)
採用面向操作数栈,而不是寄存器的架构
Java虚拟机的解释器,基本执行模型
do {
自动计算PC寄存器的值,+1;
根据PC寄存器的指示位置,从字节码流中取出操作码;
if (字节码存在操作数)
从字节码流中取出操作数;
执行操作码定义的操作
} while(字节码流长度 > 0)
加载和存储指令
用于 栈帧中的局部变量表和操作数栈 之间的来回传输
将1个局部变量加载到操作栈
iload, iload_<n>,lload,lload_<n>,fload,fload_<n>,dload,dload_<n>,aload,aload_<n>
将1个数值从操作数栈存储到局部变量表
istore,istore_<n>,lstore,lstore_<n>,fstore,ftore_<n>,dstore,dstore_<n>,astore,astore_<n>
将1个常量加载到操作数栈
bipush,sipush,ldc,ldc_w,ldc2_w,aconst_null,iconst_ml,iconst_<i>,lconst_<l>,fconst_<f>,dconst_<d>
扩充局部变量表的访问索引的指令
wide
运算指令
运算或算术指令,是对两个操作数栈上的值,做某种特定运算,并将运算结果重新存入操作数栈的栈顶
没有直接byte,char,short,boolean类型的算术指令,用操作int类型的指令代替
整型与浮点型在溢出和被0整除时,有不同行为表现
-
指令包括
- 加、减、乘、除、求余、取反、位移(i,l)、按位或、按位与、按位异或、局部变量自增(i)、比较(d,f,l)
对象创建与访问指令
虽然类实例和数组都是对象,但虚拟机对这两者的创建与操作,使用了不同的字节码指令
- 创建类实例的指令:
new
- 创建数组:
newarray
,anewarray
,multianewarray
- 访问类变量和实例变量:
getfield
,putfield
,getstatic
,putstatic
- 将1个数组元素加载到操作数栈:
baload
,caload
,saload
,iaload
,laload
,faload
,daload
,aaload
- 将1个操作数栈的值存储到数组元素中的指令:
bastore
,castore
,sastore
,iastore
,fastore
,dastore
,aastore
- 取得数组长度:
arraylength
- 检测类实例类型:
instanceof
,checkcast
操作数栈管理指令
操作数栈的操作,无非是压栈、弹栈
- 将操作数栈的栈顶1个或2个元素出栈:
pop
,pop2
- 复制栈顶1个或2个数值,并将复制值或2份复制值,重新压入栈顶:
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条关键指令
公有设计,私有实现
虚拟机的实现方式,主要有
- 将输入的Java虚拟机代码,在加载或执行时,翻译成另外一种虚拟机的指令集
- 将输入的Java虚拟机代码,在加载或执行时,翻译成宿主机CPU的本地指令集,即JIT代码生成技术