前言: Java语言的口号是“一次编写,到处运行”, 实现平台的无关性。 sun公司以及其他的虚拟机公司发布了许多可以运行在各种不同平台上的虚拟机,这些虚拟机都可以载入和执行一种与平台无关的字节码,从而实现程序的“一次编写,到处运行”。字节码存储格式实现了这种平台无关性。 Java虚拟机不和包含java语言在内的任何语言绑定, 其只和“Class文件”特定的字节码文件相关联。基于安全考虑,Java虚拟机规范要求在Class文件中使用许多强制性的语法和结构化约束,任何语言使用特定编译器编译成被虚拟机接收的Class文件都可以运行,即: 虚拟机并不关心Class文件的来源是何种语言。
Class类文件结构概览:
Class文件是一组以8位bit为基础单位的二进制流,各个数据项目严格按照顺序紧凑排列在Class文件之中,中间没有添加任何分隔符。 任何一个Class文件都对应着唯一一个类或接口的定义信息。Class文件格式采用一种类似于C语言结构体的伪结构来存储数据,这种伪结构只有两种数据类型: 无符号数和表。
其中, 无符号数属于基本数据类型,以u1、u2、u4、u8来分别代表1、2、4、8个字节的无符号数, 其可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值。 表是由多个无符号数或者其他表作为数据项构成的复合数据结构,所有表习惯性以“_info”结尾。表用于描述有层次关系的复合结构的数据。整个Class文件本质上就是一张表。
Class文件格式
类型 | 名称 | 数量 |
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_count | 1 |
u2 | interfaces | interfaces_count |
u2 | fields_count | 1 |
field_info | fields | fields_count |
u2 | methods_count | 1 |
method_info | methods | methods_count |
u2 | attribute_count | 1 |
attribute_info | attributes | attributes_count |
无论是无符号数还是表,当需要描述同一类型但数量不定的多个数据时,经常会使用一个前置的容量计数器加若干个连续的数据项的形式,这一系列连续的数据为某一类型的集合。 如: constant_pool_count 和 constant_pool
Class文件的结构是被严格规定的,其中的每个字节的顺序代表的含义都是不能改变的。
Class文件结构解析:
1. 魔数与Class文件的版本
Class文件的头四个字节为魔数(Magic Number),用来确定此文件是否为一个能被虚拟机接收的Class文件。紧接着魔数的四个字节存储的是Class文件的版本号: 第5,6个字节是次版本号(Minor Version), 7,8个字节是主版本号(Major Version)。 Java版本号从45开始,JDK1.1之后的每个JDK大版本发布主版本上加一。高版本的JDK能向下兼容以前版本的Class文件,但不能运行以后版本的Class文件,虚拟机会拒绝执行。
package org.fenixsoft.clazz;
public class TestClass{
private int m;
public int inc(){
return m + 1;
}
}
可以看到, 头四个字节cafe babe 是魔数。 0000 为次版本号, 0034为主版本号, ox0034是十进制的52即jdk1.8
2. 常量池
紧接着主版本号后面的是常量池入口, 常量池可以理解为Class文件中资源仓库,它是Class文件结构中与其他项目管理最多的数据类型,也是占Class文件空间最大的数据项目之一,同时也是Class文件中第一个出现的表类型数据项目。
常量池中常量的数量不固定,常量池入口是一个u2类型的数据表示计数器。 这个计数器从1开始。如上图所示, 计数器为ox0016,即十进制的22,代表常量次中有21项常量。索引范围1-21。第0项常量可以表示不引用任何一个常量池项目。Class文件结构中只有常量池的容量计数从1开始,其他的都是从0开始。
常量池主要存放字面量和符号引用。 符号引用包含: 类和接口的权限定名称、字段的名称和描述符、方法的名称和描述符。
常量池总每一项常量都是表,jdk1.7之前共有11种结构各不相同的表结构,在jdk1.7中为了更好支持动态语言调用,额外增加了三种。 这14种表中的第一位是一个u1类型的标志位,代表当前这个常量属于哪种常量类型。
14种常量池项目类型
常量池中数据项类型 | 类型标志 | 类型描述 |
CONSTANT_Utf8_info | 1 | UTF-8编码的Unicode字符串 |
CONSTANT_Integer_info | 3 | int类型字面值 |
CONSTANT_Float_info | 4 | float类型字面值 |
CONSTANT_Long_info | 5 | long类型字面值 |
CONSTANT_Double_info | 6 | double类型字面值 |
CONSTANT_Class_info | 7 | 对一个类或接口的符号引用 |
CONSTANT_String_info | 8 | String类型字面值 |
CONSTANT_Fieldref_info | 9 | 对一个字段的符号引用 |
CONSTANT_Methodref_info | 10 | 对一个类中声明的方法的符号引用 |
CONSTANT_InterfaceMethodref_info | 11 | 对一个接口中声明的方法的符号引用 |
CONSTANT_NameAndType_info | 12 | 对一个字段或方法的部分符号引用 |
来看第一项常量:
其tag为u1类型为ox07, 查上表可以发现是CONSTANT_Class_info类型, 其类型只有一个u1的tag和u2的name_index, name_index是一个索引值(ox0002),其指向常量池中的CONSTANT_Ut8_info常量类型,代表了这个类的权限定名。常量池中第二项0x01,查上表可知是一个CONSTANT_Utf8_info类型。
类型 | 名称 | 数量 |
u1 | tag | 1 |
u2 | length | 1 |
u1 | bytes | length |
Class文件中的方法、字段等都需要引用CONSTANT_Utf8_info型常量来描述名称,所以CONSTANT_Utf8_info的最大长度就是Java方法中方法、字段名称的最大长度,这里u2类型最大值为65535. 所以如果Java中定义了超过64kb的英文字符的变量或者方法名称,将无法编译。
接着看Class文件, 在tag后面是长度0x001d,也就是十进制的29,即从此往后的29个字节为相关的常量名称。后面的常量也可以照此过程进行分析。 可以使用Java内置工具javap来帮助计算:
javap -verbose TestClass:
从上图可以看出,我们计算的第一个常量#1 = Class 指向了第二个常量 常量名称为 org/fenixsoft/clazz/TestClass , 与上面分析过程对应。
3. 访问标志
常量池结束后紧接着的两个字节表示访问标志(access_flags), 这组标志用于识别一些类或者接口层次的访问信息。
访问标志表
标志名称 | 标志值 | 含义 |
ACC_PUBLIC | 0x00 01 | 是否为Public类型 |
ACC_FINAL | 0x00 10 | 是否被声明为final,只有类可以设置 |
ACC_SUPER | 0x00 20 | 是否允许使用invokespecial字节码指令的新语义. |
ACC_INTERFACE | 0x02 00 | 标志这是一个接口 |
ACC_ABSTRACT | 0x04 00 | 是否为abstract类型,对于接口或者抽象类来说,次标志值为真,其他类型为假 |
ACC_SYNTHETIC | 0x10 00 | 标志这个类并非由用户代码产生 |
ACC_ANNOTATION | 0x20 00 | 标志这是一个注解 |
ACC_ENUM | 0x40 00 | 标志这是一个枚举 |
TestClass类是被public修饰的类,使用了jdk1.2以后的编译器进行了编译,因此其ACC_PUBLIIC和ACC_SUPER标志被设置为true, 标志为: 0x0001 | 0x0020=0x0021
4. 类索引、父类索引和几口索引集合
类索引和父类索引都是一个u2类型的数据,接口索引是一组u2类型的数据的集合。 Class文件中使用这三项来确定这个类的继承关系。 其中各个索引用来指向全限定名。 类和父类索引指向CONSTANT_Class_info类型,其中的name_index又可以找到CANSTANT_Utf8_info类型常量从而确定类的全限定名称。 对于接口索引集合,入口第一项是u2类型的接口计数器,如果没有接口则计数器为0,后面的接口索引表不再占用任何字节。
其中类#1指向CONSTANT_Utf8_info代表的#2, 父类Object#3 指向COSTANT_Utf8_info代表的#4。 没有接口。
5. 字段表集合
字段表用于描述接口或者类中生命的变量、字段,包括类级和实例变量。不包含方法内部的局部变量。 字段信息: 作用域、修饰符、实例还是类变量、可变性(final)、并发可见性(volatile)、可否被序列化(transient)、基本类型、名称。上述的字段信息处理名称和数据类型外无法固定,只能引用常量池中的常量描述,其它信息很适合使用标志位来表示。
字段表结构
类型 | 名称 | 数量 |
u2 | access_flags | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
字段访问标志
标志名称 | 标志值 | 含义 |
ACC_PUBLIC | 0x00 01 | 字段是否为public |
ACC_PRIVATE | 0x00 02 | 字段是否为private |
ACC_PROTECTED | 0x00 04 | 字段是否为protected |
ACC_STATIC | 0x00 08 | 字段是否为static |
ACC_FINAL | 0x00 10 | 字段是否为final |
ACC_VOLATILE | 0x00 40 | 字段是否为volatile |
ACC_TRANSTENT | 0x00 80 | 字段是否为transient |
ACC_SYNCHETIC | 0x10 00 | 字段是否为由编译器自动产生 |
ACC_ENUM | 0x40 00 | 字段是否为enum |
由字段表的信息可以看出,第一项是访问标志,访问标志和java语言中标志的一致。紧接着是name_index和descriptor_index.他们都是对常量池的引用。分别代表者字段的简单名称以及字段和方法的描述符。
全限定名就是将类的全名称中的"."换成"/"。 简单名称就是没有类型或者参数修饰的方法或者字段名称。 inc()方法和m字段的简单名称是inc和m。
方法和字段的描述符: 描述符用来描述字段的数据类型、方法的参数列表和返回值。 根据描述符的规则,基本数据类型以及void用一个大写字符表示,对象类型则用字符L加对象的全限定名来表示。
描述符标识字段含义
数据类型 | 描述符 |
---|---|
byte | B |
char | C |
double | D |
float | F |
int | I |
long | J |
short | S |
boolean | Z |
特殊类型void | V |
对象类型 | L + 对象全限定名。如 Ljava/lang/String 表示 String 类型 |
数组类型 | 每增加一个维度则在对应的字段描述符前增加一个 [ ,如一维数组 int[] 的描述符为 [I ,二维数组 String[][] 的描述符为 [[Ljava/lang/String |
当描述符用来描述方法时,按照先参数列表后返回值的顺序描述,参数列表按照参数的严格顺序放在一组小括号“()”内。 如方法void inc()的描述符为: "()V",方法java.lang.String toString()的描述符为: “()Ljava/lang/String;” 方法int indexof(char[] cs, int s ) 的描述符: “([CI)I”
继续看Class文件, 第一个为字段容量的计数器0x0001, 说明只有一个字段。接下来是访问标志0x0002,代表private标识符,字段名称0x0005查看上面javap的常量池可以发现是m, 描述符是0x0006指向常量池的#6 是I, 表示int类型。 因此可以确定源代码中的字段是private int m;。 在描述符后面还有属性表集合用于存储一些额外信息。这里可以看出属性表计数器为0即没有额外信息。
字段表集合中不会列出超类或者父接口中继承而来的字段,但有可能列出Java代码中不存在的字段。如内部类为了保持对外部类的访问性,会自动添加指向外部类实例的字段。
6. 方法表集合
Class文件存储格式中对方法的描述与对字段的描述几乎采用了完全一致的方式,方法表的结构和字段表一样。 方法表和字段表仅在访问标志和属性表集合的可选项中有所区别。
volatile关键字和transient关键字不能修饰方法,synchronized、native、strictfp、abstract关键字可以修饰方法。
方法表结构
类型 | 名称 | 数量 |
u2 | access_flags | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
方法访问标志
标志名称 | 标志值 | 含义 |
ACC_PUBLIC | 0x00 01 | 方法是否为public |
ACC_PRIVATE | 0x00 02 | 方法是否为private |
ACC_PROTECTED | 0x00 04 | 方法是否为protected |
ACC_STATIC | 0x00 08 | 方法是否为static |
ACC_FINAL | 0x00 10 | 方法是否为final |
ACC_SYNCHRONIZED | 0x00 20 | 方法是否为synchronized |
ACC_BRIDGE | 0x00 40 | 方法是否是由编译器产生的桥接方法 |
ACC_VARARGS | 0x0080 | 方法是否接受不定参数 |
ACC_NATIVE | 0x01 00 | 方法是否为native |
ACC_ABSTRACT | 0x0400 | 方法是否为abstract |
ACC_STRICTFP | 0x0800 | 方法是否为strictfp |
ACC_SYNTHETIC | 0x1000 | 方法是否是由编译器自动产生的 |
方法中的代码存储在Code属性中,下面会继续介绍属性相关信息。
接着看class文件:
可以看出 method_count为0x0002, 即有两个方法。一个是由编译器添加的<init>,另一个是源码中的inc()方法。接着0x0001是方法的访问标志为ACC_PUBLIC,0x0007是方法的名称索引,查看上面的常量池中可以看出#7出的名称为<init>。接着是描述符索引,属性数量和属性名称索引。
与字段表相对应,如果父类方法没有在子类进行重写,方法表集合中不会出现来自父类的方法信息。方法表中会出现由编译器产生的方法如“<init>”。Class文件中的特征签名比源码中的签名范围要广一些,包括了返回值。
7. 属性表集合
在class文件、字段表、方法表都可以携带自己的属性表集合,用于描述某些场景专有的信息。
属性表集合的限制相对于Class文件中的其它数据项目相对宽松。只要属性名唯一即可。Java虚拟机忽略他人写入的不认识的属性。
虚拟机规范预定义的属性
属性名称 | 使用位置 | 含义 |
Code | 方法表 | Java代码编译成的字节码指令 |
ConstantValue | 字段表 | final关键字定义的常量池 |
Deprecated | 类,方法,字段表 | 被声明为deprecated的方法和字段 |
Exceptions | 方法表 | 方法抛出的异常 |
EnclosingMethod | 类文件 | 仅当一个类为局部类或者匿名类是才能拥有这个属性,这个属性用于标识这个类所在的外围方法 |
InnerClass | 类文件 | 内部类列表 |
LineNumberTable | Code属性 | Java源码的行号与字节码指令的对应关系 |
LocalVariableTable | Code属性 | 方法的局部便狼描述 |
StackMapTable | Code属性 | JDK1.6中新增的属性,供新的类型检查检验器检查和处理目标方法的局部变量和操作数有所需要的类是否匹配 |
Signature | 类,方法表,字段表 | 用于支持泛型情况下的方法签名 |
SourceFile | 类文件 | 记录源文件名称 |
SourceDebugExtension | 类文件 | 用于存储额外的调试信息 |
Synthetic | 类,方法表,字段表 | 标志方法或字段为编译器自动生成的 |
LocalVariableTypeTable | 类 | 使用特征签名代替描述符,是为了引入泛型语法之后能描述泛型参数化类型而添加 |
RuntimeVisibleAnnotations | 类,方法表,字段表 | 为动态注解提供支持 |
RuntimeInvisibleAnnotations | 表,方法表,字段表 | 用于指明哪些注解是运行时不可见的 |
RuntimeVisibleParameterAnnotation | 方法表 | 作用与RuntimeVisibleAnnotations属性类似,只不过作用对象为方法 |
RuntimeInvisibleParameterAnnotation | 方法表 | 作用与RuntimeInvisibleAnnotations属性类似,作用对象哪个为方法参数 |
AnnotationDefault | 方法表 | 用于记录注解类元素的默认值 |
BootstrapMethods | 类文件 | 用于保存invokeddynamic指令引用的引导方式限定符 |
对于每个属性,他的名称需要从常量池中引用一个CONSTANT_Utf8_info类型的常量来表示,属性值的结构完全自定义,只需要一个u4属性去说明属性值所占用的位数即可。
属性表结构
类型 | 名称 | 数量 |
u2 | attribute_name_index | 1 |
u2 | 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_length |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
attribute_name_index是一项指向CONSTANT_Utf8_info型常量的索引。常量值固定“Code”,代表的是此属性的名称。
max_stack: 代表了操作数栈的最大深度。
max_locals代表了局部变量表所需的存储空间,单位为slot,slot是虚拟机为局部变量表分配内存所使用的最小单位,小于32为的类型占一个slot。 方法参数(包括this), 显示异常处理参数、方法体中的局部变量都需要使用局部变量表来存放,slot是可以重用的。
code_length和code用来存储Java源程序编译后生成的字节码指令。当虚拟机读到code中的一个字节码时,就可以对应找出这个字节码代表的是什么指令。u1类型表示一个指令,一共可以表达256条指令。目前大概有200条对应。 code_length使用u4类型,但虚拟机限制了只能使用u2长度。Code属性用于描述代码,其它数项目用于描述元数据。
字节码指令之后是异常表集合,其对于code属性不是必须的。
-- Exceptions属性
这里的Exceptions不是上述的异常表,Exceptions属性的作用是列举出方法中可能抛出的受查异常,即方法后的throws关键字后面列举的异常。
类型 | 名称 | 数量 |
u2 | attribute_name_index | 1 |
u2 | attribute_lrngth | 1 |
u2 | attribute_of_exception | 1 |
u2 | exception_index_table | number_of_exceptions |
Exception属性中的number_of_exceptions项表示方法可能抛出的number_of_exceptions种受查异常,每一种受查异常使用一个exception_index_table项表示,exception_index_tsble是一个指向常量池中CONSTANT_Class_info型常量的索引,代表了该受查异常的类型.
-- LineNumberTable属性
LineNumberTable属性用于描述Java源码行号与字节码行号之间的对应关系。不是运行时必须的属性,但默认会生成到Class文件之中。在javac时,可以使用-g:none或者-g:lines来选择生成或者不生成。 如果不生成当出现异常时不会显示错误的行号,调试时也无法安装源码行来设置断点。
-- LocalVariableTable属性
用于描述栈帧中局部变量表中的变量与Java源码中定义的变量之间的关系。也不是必须,但默认会生成到Class文件中。 使用 -g:none或者-g:vars选项来控制。 不使用的话,当引用方法时,所有的参数名称都将消失。IDE会使用arg0,arg1等占位符代替原有的参数名。
-- sourcefile属性
用于记录生成这个class文件的源码文件名称,使用-g:source来生成,不生成时,当抛出异常,堆栈中将不会显示出错代码所属的文件名。
-- ConstantValue属性
用来通知虚拟机自动为静态变量赋值。只有被static关键字修饰的变量才可以使用这项属性。对于类变量,如果同时使用final和static来修饰一个变量,并且这个变量时基本类型或String类型的话,就生成CanstantValue属性进行初始化。没有final修饰的话,在<cinit>中进行初始化。
-- InnerClasses属性
用于记录内部类与宿主类之间的关联。
-- Deprecated、Synthetic属性
Deprecated属性用来标识类、方法、字段被标记为过时
Synthetic属性代表字段或者方法不是有Java源码直接产生的,而是由编译器自行添加的。处理<init>和<cinit>方法外,所有非用户代码产生的类、方法、字段都应该至少设置Synthetic属性和ACC_SYNTHETIC标志中的一项。
-- StackMapTable属性
此属性在JDK1.6发布后添加到Class文件规范中。位于Code属性的属性表中。目的在于替代以前比较消耗性能的基于数据流分析的类型推导验证器。
-- Signature属性
此属性在JDK1.5发布后增加到Class文件规范中。可以在类、字段表和方法表结构的属性表中。jdk1.5添加了泛型机制,signature属性会记录类、接口、方法等泛型签名信息。由于泛型的擦除机制,在运行期间没有泛型相关信息,Signature属性弥补了这个缺陷。
-- BootstrapMethods属性
此属性在JDK1.7发布后增加到Class文件规范中。位于类文件的属性表中。用于保存invokedynamic指令引用的引导方法限定符。
--------------------------------------------------
深入理解java虚拟机JVM高级特性与最佳实现 读书笔记