前言
要想深入的了解jvm,了解java编译后的类文件结构和字节码是很有必要的。虽然这部分内容(主要是class文件的数据结构)比较枯燥,但是这也是最基础的内容,是我们深入理解jvm的内存、类的加载等内容的基石。
Class类文件结构
class文件是一组以8位字节为基础的二进制流,各个数据项目按照顺序排列在Class文件中,中间没有任何分隔符。因此整个class文件中存储的内容几乎全是程序运行时的必要数据。当遇到需要占用8位以上字节空间的数据项时,会按照高位在前的方式分割成若干个8位字节存储。
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
class文件只有两种伪数据结构:无符号数和表。可以看到每个表的前面都会有一XX count的,这是一个前置容量计数器,用来记录对应类型的数量。
数据项
class文件的数据项很多,这里不展开一个个地讲,主要介绍一些关键的。
常量池
字段表
方法表
属性表
常量池
常量池相信很多人都听过,这里的常量池指的是class文件中的常量池。
常量池中主要存储两类类型:字面量和符号引用。
字面量:字面量指的是java语言层的常亮,如String s="123",那么这个"123"就是常量。对于基本类型的封装类型,范围在-127-128之间的,也是常量。当然了,声明为final的值,在整个程序运行过程中不可变,自然也是常量了。
符号引用:java中的符号引用主要包括以下3类常量:
类和接口的全限定名
字段的名称和描述符
方法的名称和描述符
java是在虚拟机加载class文件的时候进行动态连接的,class文件中不会保存各方法字段的最终内存布局信息,因此这些字段、方法的符号引用不经过虚拟机运行时转换的话,无法得到真正的内存地址,也就无法被jvm使用。当虚拟机运行时,需要从常量池获得对应的符号引用,再在类创建或运行时解析,翻译到具体的内存地址中。
常量池中的每一项常量都是一个表,这些表有一个共同的特点,每个表的第一位都是一个u1类型的标志位,取值如下:
常量池中数据项类型
类型标志
类型描述
CONSTANT_Utf8
1
UTF-8编码的Unicode字符串
CONSTANT_Integer
3
int类型字面值
CONSTANT_Float
4
float类型字面值
CONSTANT_Long
5
long类型字面值
CONSTANT_Double
6
double类型字面值
CONSTANT_Class
7
对一个类或接口的符号引用
CONSTANT_String
8
String类型字面值
CONSTANT_Fieldref
9
对一个字段的符号引用
CONSTANT_Methodref
10
对一个类中声明的方法的符号引用
CONSTANT_InterfaceMethodref
11
对一个接口中声明的方法的符号引用
CONSTANT_NameAndType
12
对一个字段或方法的部分符号引用
访问标志
访问标志用来识别类或接口的访问信息,比如这个Class是类还是接口,是public还是private,是否被声明为final等。具体标志位及含义如下:
标志名称
标志值
含义
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
标志这是一个枚举
类索引、父类索引与接口索引集合
我们都知道,java中是单继承多实现的,除了Object类之外每个类都有父类,因此它们是唯一的,而一个类可以实现多个接口,因此接口是不唯一的,用集合表示。类索引和父类索引都是用一个u2类型数据来表示的,而接口索引集合则是一组u2类型的数据表示。
类索引、父类索引和接口索引集合都按顺序排在访问标志的后面。类索引和父类索引u2类型的索引各指向一个类型为CONSTANT_Class_info的类描述符常量,用来描述具体的类。对于接口索引第一项u2则为接口索引计数器,用来记录实现了多少个接口,如果为0则后面不再占用任何字节。
字段表集合
字段表用于声明类或接口中声明的变量。字段包括类变量和实例变量。在java中一个字段是如何描述的,举个的例子
public static final String num="13234";
可以看出来,首先是访问范围,是公有的还是私有的,或者受保护的,这个信息决定了字段是否堆特定范围的类可见。
其次是一些关键字修饰的描述信息,是实例变量还是类变量,是否可变,并发可见性,是否可被序列化等,这些关键字包括static、final 、volatile、transient等。
在后面便是字段的数据类型(基本数据类型、数组、对象)和名称。
上述的这些修饰符都是用布尔值来描述的,而数据类型和名称都是不确定的,通常引用常量池的常量来描述。
字段表结构如下:
类型
名称
数量
u2
access_flags
1
u2
name_index
1
u2
descriptor_index
1
u2
attributes_count
1
attribute_info
attributes
attributes_count
字段修饰符在access_flag下,access_flag的内容如下:
标志名称
标志值
含义
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_TRANSTENT
0x0080
字段是否为transient
ACC_SYNCHETIC
0x1000
字段是否为由编译器自动产生
ACC_ENUM
0x4000
字段是否为enum
name_index表示的是字段的简单名称,像上面的简单名称就是“num”,descriptor_index 表示字段和方法的描述符。
描述符号的作用是用来描述字段的数据类型、方法的参数列表(数量、类型及顺序)和返回值。根据描述符的规则:基本数据类型以及代表无返回值的void类型都用一个大写的字符来表示,而对象类型则用字符L加对象的全限定名来描述,详情如下:
标志符
含义
B
基本数据类型byte
C
基本数据类型char
D
基本数据类型double
F
基本数据类型float
I
基本数据类型int
J
基本数据类型long
S
基本数据类型short
Z
基本数据类型boolean
V
基本数据类型void
L
对象类型
对于数组类型,每一个维度用一个前置的“[”字符来描述,如定义个int[][]类型的二维数组,记录为:"[[I"。
用描述符来描述方法时,按照先参数列表后返回值的顺序描述。参数裂变按照参数顺序放在“()”内,如方法void login()描述符为“()V”,方法java.lang.String toString()的描述符为“()Ljava.lang.String”。
方法表集合
Class文件存储格式中对方法和字段的描述完全一致,方法表的字段结构和字段表一样,包括访问标志、名称索引、描述符索引、属性表集合几项。这些数据的含义非常类似,在访问标志和属性表集合有所区别。
类型
名称
数量
u2
access_flags
1
u2
name_index
1
u2
descriptor_index
1
u2
attributes_count
1
attribute_info
attributes
attributes_count
voliate和transient不能修饰方法,所以访问标志没有了ACC_VOLATILE标志和ACC_TRANSIENT标志。同样,一些方法的关键字如:synchronized、native、strictfp、abstract等可以修饰方法,其标志位取值如下:
标志名称
标志值
含义
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_SYHCHRONRIZED
0x00 20
方法是否为synchronized
ACC_BRIDGE
0x00 40
方法是否是有编译器产生的方法
ACC_VARARGS
0x00 80
方法是否接受参数
ACC_NATIVE
0x01 00
方法是否为native
ACC_ABSTRACT
0x04 00
方法是否为abstract
ACC_STRICTFP
0x08 00
方法是否为strictfp
ACC_SYNTHETIC
0x10 00
方法是否是有编译器自动产生的
方法里的代码
方法的定义可以通过访问标志、名称和描述符索引来表述清除,那么方法中的代码又在哪里呢?我们之前提到了属性表集合,方法里的java代码经过编译器编译成字节码指令后,存放在方法的属性表集合里一个名为“Code”的属性里。
属性表集合
我们从上面不止一次的看到了属性表集合,不管是Class文件、字段表还是方法表中,都有属性表集合(attribute_info)这一项,用于描述某些特定场景的专有信息。
与class文件其它数据项目严格要求顺序长度不同,属性表集合限制相对比较宽松,不要求各个属性表具有严格顺序,只要不与已有属性名重复,任何人实现的编译器均可向属性表写入自己的属性,jvm运行时会自动忽略掉不认识的属性。
java7中定义的属性如下表:
属性名称
使用位置
含义
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指令引用的引导方式限定符