深入理解Java虚拟机
类文件结构
虚拟机
虚拟机作为一个通用的、与系统无关的执行平台,不与包括Java在内的任何一门语言绑定,只与.class文件关联,即无论任何语言都可以表示为虚拟机所理解的.class文件。
.class
- 任何一个.class文件都对应着一个唯一的类或接口的定义信息,但反过来并非如此,某些类或接口可以通过类加载器直接生成。
- class文件是一种以8字节为基础单位的二进制流,数据按照严格顺序紧凑排列,没有间隙符,若数据超过8字节,则分成多个8字节单元存储,且高位在前。
- class文件采用类似C结构体的方式储存数据,数据类型只有无符号数和表两种。
- 无符号数 u1,u2,u3,u4代表1,2,4,8个字节的无符号数,可以用来表示数字、索引引用、数量值、UTF-8编码的字符串值;
- 表是一种复合数据,由多个无符号数或其他表构成,描述有层次结构的复合数据结构数据。
.class文件结构
魔数
.class文件的前四个字符为魔数,其功能用来认证,即用来确认该文件是否是一个能够被虚拟机接受的Class文件,Class文件的魔数为0xCAFEBABE(咖啡宝贝)。
版本号
魔数后四个字符为版本号,其中前两个为次版本号,后两个为主版本号。
常量池
资源仓库,数量不定。入口放置一项u2类型数据,用来容量计数(从1开始,若为5则有4项常量)。0索引代表不引用任何一个常量池项目。
常量池中有两大类常量:字面量和符号引用。
常量池中每个常量都是一个表,表开始的第一位是一个u1类型标志位,代表这个常量属于哪种常量类型,截止到JDK1.7一共有14种项目类型。
javap
JDK的bin目录下的专门用于分析Class文件字节码的工具javap。使用示范javap -verbose TestClass
访问标志
常量池结束后紧跟的两个字节代表访问标志,用于识别一些类或者接口层次的访问信息(是Class?Interface?public?abstract?final等)
索引
类索引(u2)用于确定类的全限定名
父类索引(u2)用于确定父类的全限定名
接口索引(u2集合)用于确定实现的接口,第一个u2为计数器
三个索引按照顺序排列在访问标志之后
字段表
用于描述接口或类中声明的变量,包括类级和实例级,不包含方法中定义的局部变量。
结构中按照上下左右顺序为:
- 访问控制
- 索引(简单名称)
- 索引(字段和方法的描述符,type、参数列表、返回值。一个维度数组前面加个[表示
int[]表示为[I
,方法按顺序加在前面的()里int test(char[] c,int a)表示为([CI)I
) - 属性表 (额外信息,非固定)
入口地址第一个数据(u2)为容量
方法表集合
结构与字段表很多相同,部分变化如下:
- 访问标志中移除volatile和transient,增加synchronized、abstract、native、strictfp
- 属性表集合可选项有部分变化,增加了Code字段,里面放了方法代码
入口地址第一个数据(u2)为容量
属性表集合
在Java7的虚拟机规范中,预定义的属性已有21项。
【Code】
方法体中的代码经过编译后变为字节码储存在该属性中,非必须。
max_locals
代表局部变量最大存储空间,单位Slot,Slot为虚拟机为局部变量分配内存的最小单位,基本类型大多只占一个Slot,long和double占两个。Slot可以复用
code_length
代表字节码长度,使用一个u4存储,但虚拟机明确规定一个方法长度超过65535将解决编译,因此实际上只占了一个u2。用u4的原因是某些Jsp编译器会将Jsp内容和页面信息等内容归并与同一个方法里导致超长字节码。
this
注意!对于实例方法(非static),即使没有任何参数,在字节码信息中可以看到其参数是1,这是由于this的存在,虚拟机将this当成一个普通的入参参数处理。
异常表
字节码之后是方法的显示异常处理表,非必须。
如果字节码在start-pc行到end_pc行之间出现了catch_type及其子类的异常,则转到handler_pc行进行处理。异常表属于Java代码一部分,编译器使用异常表而非简单的跳转执行try-catch-finally。
【Exception】
与上面的异常表不同,该属性是throw后列举的可能出现的异常
【LineNumberTable】
描述Java源代码行号和字节码行号之间的对应关系,默认,但非必须,如果参数配置取消,造成抛出异常信息时不会显示行号。
【LocalVariableTable】
描述帧栈中局部变量表中的变量与Java源代码中定义的变量之间的关系,默认但非必须,取消后将丢失参数名称,显示为arg1、arg2……
【SourceFile】
记录生成这个class文件的源文件名称,非必须。大多数类名和文件名相同,但有例外(如内部类),取消该配置抛出异常时不会显示出代码所属文件名
【ConstantValue】
通知虚拟机自动为静态变量赋值。若基本类型或String类型被修饰为static final
则生成ConstantValue进行初始化,否则用类构造器初始化方法初始化。
【InnerClasses】
记录内部类与宿主类之间的关系。
【Deprecated及Synthetic】
均为标志类布尔类型。
Deprecated表示某个类、字段、方法被作者不推荐使用
Synthetic代表此字段或方法不是有Java源码产生的,是由编译器自行添加的
【StackMapTable】
复杂变长属性,位于【Code】的属性表中,被虚拟机加载的字节码验证阶段被新类型检查验证器使用,代替以前耗费性能的基于数据流分析的类型推导验证器。不再在运行期间分析数据流,改为在编译期间将验证信息直接记录到class文件中,检查验证类型代替推导过程,提升效率。
【Signature】
类、接口、初始化方法或成员的泛型签名如果包含了类型变量或参数化类型,Signature属性记录泛型签名信息。使用该属性是因为Java是伪泛型,编译后泛型信息擦除掉,实现简单省空间,但也有坏处,如运行时无法通过反射获得泛型信息。该属性解决这个问题。
【BootStrapMethods】
复杂变长,只能出现一次,位于类文件属性表。保存invokedynamic指令引用的引导方法限定符。
字节码指令
指令由操作码(一个字节)+操作数(即参数)组成
iload
指令从局部变量表中加载int类型数据到操作数栈中
fload
指令从局部变量表中加载float类型数据到操作数栈中
这种指令中包含数据类型信息,但操作码只有一个字节,最多只能有256个指令,无法对所有操作都加入数据类型信息,因此只提供了有限的类型操作。在对一些不支持的类型的指令操作时可以转换为支持类型的指令,如大部分指令都没有支持byte、char、short、boolean等类型,因此编译器会在编译期间或者运行期间将byte、char、short、boolean转换为int型去操作。
加载和存储指令
- 将一个局部变量加载到操作栈:iload、iload_<n>、lload、lload_<n>、fload、fload_<n>、dload、dload_<n>、aload、aload_<n>。
- 将一个数值从操作数栈存储到局部变量表:istore、istore_<n>、lstore、lstore_<n>、fstore、fstore_<n>、dstore、dstore_<n>、astore、astore_<n>。
- 将一个常量加载到操作数栈:bipush、sipush、ldc、ldc_w、ldc2_w、aconst_null、iconst_m1、iconst_<i>、lconst_<l>、fconst_<f>、dconst_<d>。
- 扩充局部变量表的访问索引的指令:wide。
<n>
代表一组指令
运算指令
对两个操作数栈上的值进行某种运算,并将结果重新存入到操作栈顶
类型转换指令
小范围到大范围的安全转换,虚拟机直接支持
大范围到小范围的窄化转换,需要显式使用指令操作,窄化操作为:
对象创建和访问指令
操作数栈管理指令
用于直接对操作数栈进行管理
控制转移指令
方法调用和返回指令
- invokevirtual指令用于调用对象的实例方法,根据对象的实际类型进行分派(虚方法分派),这也是Java语言中最常见的方法分派方式。
- invokeinterface指令用于调用接口方法,他会在运行时搜索一个实现了这个接口方法的对象,找出适合的方法进行调用。
- invokespecial指令用于调用一些需要特殊处理的实例方法,包括实例初始化方法、私有方法和父类方法。
- invokestatic指令用于调用类方法(static方法)。
- invokedynamic指令用于运算时动态解析出调用点限定符所引用的方法,并执行该方法,前面4条调用指令的分派逻辑都固化在Java虚拟机内部,而invokedynamic指令的分派逻辑是由用户所设定的引导方法决定的。
方法调用指令与数据类型无关,而方法返回指令是根据返回值的类型区分的,包括ireturn(当返回值是boolean、byte、char、short和int类型时使用)、lreturn、freturn、dreturn和areturn,另外还有一条return指令供声明为void的方法、实例初始化方法以及类和接口的类初始化方法使用。
异常处理指令
显式抛出通过athrow
抛出,运行时自动抛出不使用指令使用异常表
同步指令
Java虚拟机可以支持方法级的同步和方法内部一段指令序列的同步,这两种同步结构是使用管程(Monitor)来支持的。
方法级的同步是隐式的,即无需通过字节码指令来控制,他实现在方法调用和返回操作之中。虚拟机可以从方法常量池的方法表结构中的ACC_SYNCHRONIZED访问标志得知一个方法是否声明为同步方法。当方法调用时,调用指令将会检查方法的ACC_SYNCHRONIZED访问标志是否被设置,如果设置了,执行线程就要求先成功持有管程,然后才能执行方法,最后当方法完成(无论是正常完成还是非正常完成)时释放管程。在方法执行期间,执行线程持有了管程,其他任何线程都无法再获取到同一个管程。如果一个同步方法执行期间抛出了异常,并且在方法内部无法处理此异常,那么这个同步方法所持有的管程将在异常抛到同步方法之外时自动释放。
同步一段指令集序列通常是由Java语言中的synchronized语句块来表示的,Java虚拟机的指令集中有monitorenter和monitorexit两条指令来支持synchronized关键字的语义,正确实现synchronized关键字需要Javac编译器与Java虚拟机两者共同协作支持