知识要点:
Java虚拟机简介
Class类文件结构
Class类文件结构详解
Java虚拟机简介
Java虚拟机(JVM)是运行Java程序的抽象计算机,它是一种计算机设备的规范,可以采用不同的方式进行实现。Java程序通过运行在JVM中从而实现跨平台特性。
Java虚拟机不和包括Java在内的任何语言绑定,它只和Class文件这种特定的二进制文件格式关联,Class文件中包含了虚拟机指令集和符号表以及若干其他辅助信息。基于安全方面的考虑,Java虚拟机规范要求在Class文件中使用许多强制性的语法和结构化约束,但任一门功能性语言都可以表示为一个能被Java虚拟机所接受的有效Class文件。
问题:是不是只有Java编译器才能完成Java到class字节码文件的编译过程?
虚拟机产品
- Sun HotSpot
- BEA JRocket
- IBM J9
- Microsoft JVM
- Google Android Dalvik
Class类文件结构
Class文件是一组以8位字节为基础单位的二进制流,各项数据项目严格按照顺序紧凑地排列在Class文件之中,中间没有添加任何分隔符,如果是超过8位字节以上空间的数据项,则会按照高位在前的方式(Big-Endian)分割成若干个8位字节进行存储。
魔数 | u4 | magic | 1 |
---|---|---|---|
次版本号 | u2 | minor_version | 1 |
主版本号 | u2 | major_version | 1 |
常量池计数器 | u2 | constant_pool_count | 1 |
常量池 | cp_info | constant_pool | constant_pool_count-1 |
访问标志 | u2 | access_flags | 1 |
类索引 | u2 | this_class | 1 |
父类索引 | u2 | super_class | 1 |
接口计数器 | u2 | interfaces_class | 1 |
接口索引集合 | u2 | interfaces | interfaces_count |
字段计数器 | u2 | fields_count | 1 |
字段表集合 | field_info | fields | fields_count |
方法计数器 | u2 | methods_count | 1 |
方法表集合 | method_info | methods | methods_count |
属性计数器 | u2 | attributes_count | 1 |
属性表集合 | attribute_info | attributes | attributes_count |
Class类文件组成
- Class文件中包含:
- Java虚拟机指令集
- 符号表
- 若干其他辅助信息
Class文件中只有两种数据类型 - 无符号数
- 表
无符号数
无符号数属于基本的数据类型,可用来描述数字,索引引用,数量值或者按照UTF-8编码构成的字符串值。无符号数据类型包含如下4种: - u1:1个字节
- u2:2个字节
- u4:4个字节
- u8:8个字节
表
表是由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都习惯性地以“_info”结尾。表用于描述由层次关系的复合结构的数据,整个Class文件本质上就是一张表。
Class文件结构详解
常量池
Java代码在编译后,Class文件并不会保存各个方法、字段的最终内存布局,因为在编译时,Java类并不知道引用类的实际内存地址,因此只能使用符号引用来代替,而这些字段、方法引用不经过运行期转行就无法得到真正的内存地址入口。
当虚拟机运行式,需要从常量池获得对应的符号引用,而后再类创建或运行时解析成具体的内存地址,比如:在类加载器加载Test类时,此时可以通过虚拟机获取Test类的实际内存地址,因此便可以将符号ai.yunxi.Test替换为该类的实际内存地址。
符号引用包含三类常量:
- 类和接口的全限定名, 如org.springframework.web.servlet.DispatherServlet
- 字段的名称和描述符
- 方法的名称和描述符
常量池结构
访问标志
紧接着常量池之后的两个字节代表访问标志(access_flags),用于识别一些类或者接口层次的访问信息,包括:这个Class是类还是接口、是否为public类型、是否为abstract类型、类是否声明为final等。标志位及其含义如下表:
ACC_FINAL | 0X0010 | 声明为final,只有类可以设置 |
---|---|---|
ACC_SUPER | 0X0020 | 使用invokespecial字节码指令的新语意,invokespecial指令的语意在JDK1.0.2发生过改变,为了区别这条指令使用哪种语意,JDK1.0.2之后编译出来的类都为真 |
ACC_INTERFACE | 0X0200 | 接口 |
ACC_ABSTRACT | 0X0400 | abstract类型,对于接口或者抽象类来说,此标志值为真,其他类为假 |
ACC_SYNTHETIC | 0X1000 | 这个类并非由用户代码产生 |
ACC_ANNOTATION | 0X2000 | 注解 |
ACC_ENUM | 0X4000 | 枚举 |
类父类与接口集合
访问标志之后顺序排列类索引(this)、父类索引(super)、接口索引集合(interfaces)。Class文件由这三项来确定这个类的集成关系。类索引和父类索引都是u2类型的数据。接口索引集合入口第一项是u2类型的接口计数器(interfaces_count)表示索引表的容量(即实现了几个接口)。如果该类没用实现任何接口,则计数器值为0,后面的接口索引表不再占用任何字节。
字段表集合
接口索引集合后边的是字段计数器:用于标识有多少个字段。字段计数器之后接着就是字段表集合。字段表(field_info)用于描述接口或者类中声明的变量。字段包括类级变量以及实例级变量。可以包括的信息有:
- 字段的作用域(public、private、protected修饰符)
- 实例变量还是类变量(static修饰符)
- 可变性(final)
- 并发可见性(volatile)
- 可否被序列化(transient)
- 字段数据类型(基本类型,对象,数组)
- 字段名称
字段表结构
u2 | access_flags | 1 |
---|---|---|
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | attribute_count | 1 |
attribute_info | attributes | attribute_count |
字段访问标志
ACC_PUBLIC | 0X0001 | 字段是否public |
---|---|---|
ACC_PRIVATE | 0X0002 | 字段是否private |
ACC_PROTECTED | 0X0004 | 字段是否protected |
ACC_STATIC | 0X0008 | 字段是否static |
ACC_FINAL | 0X0010 | 字段是否final |
ACC_VOLATILE | 0X0040 | 字段是否volatile |
ACC_TRANSIENT | 0X0080 | 字段是否transient |
ACC_SYNTHETIC | 0X0100 | 字段是否由编译器自动产生的 |
ACC_ENUM | 0X0400 | 字段是否enum |
全限定名、简单名称及描述符
- 全限定名
- 简单名称:没有类型和参数修饰的方法或者字段名称,如:inc()和int m简单名称就是:inc、m
- 描述符:用来描述字段的数据类型、方法的参数列表(数量、类型及顺序)和返回值
方法表集合
通过访问标志、名称索引、描述符索引可清楚的表达方法的定义,属性表是Class文件格式中最具扩展性的一种数据项目。
ACC_PUBLIC | 0X0001 | 方法是否public |
---|---|---|
ACC_PRIVATE | 0X0002 | 方法是否private |
ACC_PROTECTED | 0X0004 | 方法是否protected |
ACC_STATIC | 0X0008 | 方法是否static |
ACC_FINAL | 0X0010 | 方法是否final |
ACC_SYNCHRONIZED | 0X0020 | 方法是否synchronized |
ACC_BRIDGE | 0X0040 | 方法是否由编译器产生的桥接方法 |
ACC_VARARGS | 0X0080 | 方法是否接受不定参数 |
ACC_NATIVE | 0X0100 | 方法是否为native |
ACC_ABSTRACT | 0X0400 | 方法是否为abstract |
ACC_STRICTFP | 0X0800 | 方法是否为strictfp |
ACC_SYNTHETIC | 0X1000 | 防范是否由编译器自动产生 |
属性表
Class文件、字段表、方法表、属性表都可以携带自己的属性表集合,用于描述某些场景专有的信息。属性表集合的限制稍微宽松,不再要求各个属性表具有严格顺序,只要不与已有属性名重复,任何人实现的编译器都可以向属性表中写入自己定义的属性信息。
Code | 方法表 | Java代码编译成的字节码指令 |
---|---|---|
ConstantValue | 字段表 | final关键字定义的常量值 |
Deprecated | 类、方法表、字段表 | 被声明为deprecated的方法和字段 |
Exceptions | 方法表 | 方法抛出的异常 |
EnclosingMethod | 类文件 | 仅当一个类为局部类或者匿名类时才能拥有这个属性,这个属性用于标识这个类所在的外围方法 |
InnerClasses | 类文件 | 内部类列表 |
LineNumberTable | Code属性 | Java源码的行号与字节码指令的对应关系 |
LocalVariableTable | Code属性 | 方法的局部变量描述 |
StackMapTable | Code属性 | JDK1.6中新增的属性,供新的类型检查验证器(Type Checker)检查和处理目标方法的局部变量和操作数栈所需要的类型是否匹配 |
SourceFile | 类文件 | 记录源文件名称 |
Signature | 类、方法表、字段表 | JDK1.5中新增的属性,这个属性用于支持泛型情况下的方法签名,在Java语言中,任何类、接口、初始化方法或成员的泛型签名如果包含了类型变量(Type Variables)或参数化类型(Parameterized Types),则Signature属性会为它记录泛型签名信息。由于Java的泛型采用擦除法实现,在为了避免类型信息被擦除后导致签名混乱,需要这个属性记录泛型中的相关信息 |
SourceDebugExtension | 类文件 | JDK1.6中新增的属性,SourceDebugExtension属性用于存储额外的调试信息。譬如在进行JSP文件调试时,无法通过Java堆栈来定位JSP文件的行号,JSR-45规范为这些非Java语言编写,却需要编译成字节码并运行在Java虚拟机中的程序提供了一个进行调试的标准机制,使用SourceDebugExtension属性就可以用于存储这个标准所新加入的调试信息 |
Synthetic | 类、方法表、字段表 | 标识方法或字段为编译器自动生成的 |
LocalVariableTypeTable | 类 | JDK1.5中新增的属性,它使用特征签名代替描述符,是为了引入泛型语法之后能描述泛型参数化类型而添加 |
RuntimeVisibleAnnotations | 类、方法表、字段表 | JDK1.5新增的属性,为动态注解提供支持。RuntimeVisibleAnnotations属性用于注明哪些注解是运行时(实际上运行时就是进行反射调用)可见的 |
RuntimeInvisibleAnnotations | 类、方法表、字段表 | JDK1.5新增的属性,与RuntimeVisibleAnnotations属性作用刚好相反,用于指明哪些注解是运行时不可见的 |
RuntimeVisibleParameterAnnotations | 方法表 | JDK1.5新增的属性,作用与RuntimeVisibleAnnotations属性类似,只不过作用对象为方法参数 |
RuntimeInvisibleParameterAnnotations | 方法表 | JDK1.5新增的属性,作用与RuntimeInvisibleAnnotations属性类似,只不过作用对象为方法参数 |
AnnotationDefault | 方法表 | JDK1.5新增的属性,用于记录注解类元素的默认值 |
BootstrapMethods | 类文件 | JDK1.7中新增的属性,用于保存invokedynamic指令引用的引导方法限定符 |
属性表结构
u2 | attribute_name_index | 1 |
---|---|---|
u4 | attribute_length | 1 |
u1 | info | attribute_length |
Code属性表
Java程序方法体中的代码经过Javac编译处理后,最终变为字节码指令存储在Code属性中,Code属性出现在方法表的属性集合之中。但并非所有方法表都有Code属性,例如抽象类或接口。
u2 | attribute_name_index | 1 |
---|---|---|
u4 | attribute_length | 1 |
u2 | max_stack | 1 |
u2 | max_locals | 1 |
u4 | code_length | 1 |
u1 | code | code_length |
u2 | exception_table_length | 1 |
exception_info | exception_table | exception_table_length |
u2 | attribute_count | |
attribute_info | attributes | attribute_count |
attribute_name_index:指向CONSTANT_Utf8_info类型常量的值固定为“Code”
- attribute_length:属性值的总长度
- max_stack:操作数栈(Operand Stacks)深度的最大值
- max_locals:局部变量所需的存储空间(单位:Slot)
- code_length:和code是用来存储Java源程序编译后产生的字节码指令
异常属性表
start_pc | u2 | 1 |
---|---|---|
end_pc | u2 | 1 |
handle_pc | u2 | 1 |
catch_type | u2 | 1 |
Exceptions属性表
Exceptions属性是在方法表中与Code属性平级的一项属性。Exceptions属性的作用是列举出方法中可能抛出的受查异常(Checked Exceptions), 也就是方法描述时在throws关键字后面列举的异常
attribute_name_index | u2 | 1 |
---|---|---|
attribute_length | u4 | 1 |
number_of_exceptions | u2 | 1 |
exception_index_table | u2 | number_of_exceptions |
LineNumberTable属性表
LineNumberTable属性用于描述Java源码行号与字节码行号(字节码的偏移量)之间的对应关系。可以在编译的时候分别使用-g:none和-g:lines选项来取消或者要求生成这项信息。
attribute_name_index | u2 | 1 |
---|---|---|
attribute_length | u4 | 1 |
line_number_table_length | u2 | 1 |
line_number_table | line_number_info | line_number_table_length |
LocalVariableTable属性表
LocalVariableTable属性用于描述栈帧中局部变量表中的变量与Java源码中定义的变量之间的关系。
attribute_name_index | u2 | 1 |
---|---|---|
attribute_length | u4 | 1 |
local_varible_table_length | u2 | 1 |
local_variable_table | local_variable_info | local_varible_table_length |
variable_info
start_pc和length属性分别代表了这个局部变量的生命周期开始的字节码偏移量及其作用范围覆盖的长度,两者结合起来就是这个局部变量在字节码之中的作用域范围。name_index和descriptor_index都是指向常量池中CONSTANT_Utf8_info型常量的索引,分别代表了局部变量的名称及这个局部变量的描述符。index是这个局部变量在栈帧局部变量表中Slot的位置。当这个变量数据类型是64位类型时(double和long),它占用的Slot为index和index+1两个。
start_pc | u2 | 1 |
---|---|---|
length | u2 | 1 |
name_index | u2 | 1 |
descriptor_index | u2 | 1 |
index | u2 | 1 |
SourceFile
SourceFile属性用于记录生成这个Class文件的源码文件名称。sourcefile_index数据项是指向常量池中CONSTANT_Utf8_info型常量的索引,常量值是源码文件的文件名。
attribute_name_index | u2 | 1 |
---|---|---|
attribute_length | u4 | 1 |
sourcefile_index | u2 | 1 |
ConstantValue
ConstantValue属性的作用是通知虚拟机自动为静态变量赋值。只有被static关键字修饰的常量(类变量)才可以使用这项属性。目前Sun Javac编译器的选择是:如果同时使用final和static来修饰一个变量,并且这个变量的数据类型是基本类型或者java.lang.String的话,就生成ConstantValue属性来进行初始化,如果这个变量没有被final修饰,或者并非基本类型及字符串,则将会选择在<clinit>方法中进行初始化。
attribute_name_index | u2 | 1 |
---|---|---|
attribute_length | u4 | 1 |
constantvalue_index | u2 | 1 |
InnerClasses
InnerClasses属性用于记录内部类与宿主类之间的关联
attribute_name_index | u2 | 1 |
---|---|---|
attribute_length | u4 | 1 |
number_of_classes | u2 | 1 |
inner_classes | inner_classes_info | number_of_classes |
number_of_classes代表需要记录多少个内部类信息。
inner_classes_info
inner_class_info_index | u2 | 1 |
---|---|---|
outer_class_info_index | u2 | 1 |
inner_name_index | u2 | 1 |
inner_class_access_flags | u2 | 1 |
inner_class_info_index和outer_class_info_index都是指向常量池中CONSTANT_Class_info型常量的索引,分别代表了内部类和宿主类的符号引用。
- inner_name_index代表内部类的名称
- inner_class_access_flags是内部类的访问标志
Deprecated和Synthetic
Deprecated和Synthetic都属于标志类型的布尔属性,只存在有和没有的区别,没有属性值的概念。Deprecated代表已经不再推荐使用。Synthetic代表字段或者方法并不是有Java源码直接产生的,而是由编译器自行添加的。
attribute_name_index | u2 | 1 |
---|---|---|
attribute_length | u2 | 1 |
StackMapTable
StackMapTable属性在JDK1.6发布后增加到了Class文件规范中,它是一个复杂的变长属性,位于Code属性的属性表中。会在虚拟机类加载的字节码验证阶段被新类型检查验证器(Type Checker)使用,目的在于代替以前比较消耗性能的基于数据流分析的类型推导验证器。一个方法的Code属性最多只能有一个StackMapTable属性。
attribute_name_index | u2 | 1 |
---|---|---|
attribute_length | u4 | 1 |
number_of_entries | u2 | 1 |
stack_map_frame | stack_map_frame entries |
注:近期和大家分享有关jvm相关的技术点,欢迎大家讨论学习!