Class类文件结构:
- 魔数
- 版本号
- 常量池
- 访问标志
- 类索引、父类索引和接口索引集合
- 字段表集合
- 方法表集合
- 属性表集合
Class类文件定义:
任何一个class文件都对应着惟一 一个类或接口的定义信息,反过来说,类和接口并不一定都定义在文件里。
class文件是一组以8位字节为基础单位的二进制流,各项数据项目严格按照顺序紧凑排列在class文件中。中间没有任何分隔符,这使得整个class文件中存储的内容几乎是程序运行的必要数据,没有间隙。
当遇到需要占用8位字节以上空间的数据项时,则会按照高位在前的方式分割成若干个8位字节进行存储。
class文件格式采用伪结构,该种结构中只有两种数据类型:无符号数和表。
无符号数:u1,u2,u4,u8
分别代表1个字节,2个字节,4个字节,8个字节的无符号数。无符号数用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值。
表:由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都习惯性以”_info”
结尾,整个class文件本质上就是一张表。
Class类文件实例:
我们下面用《深入理解Java虚拟机》的例子来了解一下
从java文件
到 class文件
以及 二进制流
之间的关系。
Java文件:
package org.fenixsoft.clazz;
public class TestClass {
private int m;
public int inc(){
return m+1;
}
}
Class文件的字节码:
public class org.fenixsoft.clazz.TestClass
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #4.#18 // java/lang/Object."<init>":()V
#2 = Fieldref #3.#19 // org/fenixsoft/clazz/TestClass.m:I
#3 = Class #20 // org/fenixsoft/clazz/TestClass
#4 = Class #21 // java/lang/Object
#5 = Utf8 m
#6 = Utf8 I
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 Lorg/fenixsoft/clazz/TestClass;
#14 = Utf8 inc
#15 = Utf8 ()I
#16 = Utf8 SourceFile
#17 = Utf8 TestClass.java
#18 = NameAndType #7:#8 // "<init>":()V
#19 = NameAndType #5:#6 // m:I
#20 = Utf8 org/fenixsoft/clazz/TestClass
#21 = Utf8 java/lang/Object
{
public org.fenixsoft.clazz.TestClass();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 19: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lorg/fenixsoft/clazz/TestClass;
public int inc();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: getfield #2 // Field m:I
4: iconst_1
5: iadd
6: ireturn
LineNumberTable:
line 23: 0
LocalVariableTable:
Start Length Slot Name Signature
0 7 0 this Lorg/fenixsoft/clazz/TestClass;
}
SourceFile: "TestClass.java"
Class文件的二进制流:
(1) 魔数
每个class文件的头四个字节
称为魔数,它的唯一作用是确定这个文件是否为一个被虚拟机接受的class文件。
(2) 版本号
紧接着魔数的4个字节存储的是class文件的版本号,第5、6是次版本号
,第7、8是主版本号
。笔者这里是50。版本号向下兼容,低版本的jdk是不接受高版本的class文件的。
十六进制34 (十进制 3*16+4 = 52)
(3) 常量池
紧接着主次版本号之后的是常量池入口,常量池可以理解为class文件之中的资源仓库,它是class文件结构中与其他项目关联最多的数据类型,也是占用Class文件空间最大的数据项目之一,同时还是Class文件中第一个出现的表类型数据项目。由于常量的数量是不固定的,所以常量池入口需要防止一个u2类型的数据,代表常量池容量的计数值。注意这个容量的技术是从1开始的。事例中0x 0016=22
,21个常量,索引值范围是1~21
。
设计者把索引为0空出来原因考虑到,满足后面某些指向常量池的索引值的数据在特定情况下需要表达”不引用任何一个常量池项目”
的含义,这种情况就可以把索引值置为0来表示。
常量池主要存放了两大类常量:
字面量:比较接近于java语言层面的常量概念,比如文本字符串、final的常量值。
符号引用:类和接口的全限定名、字段的名称和描述符、方法的名称和描述符
由于虚拟机加载Class文件的时候进行动态链接,也就是说,在Class文件中不会保存各个方法、字段的最终内存布局信息,因为这些字段、方法的符号引用不经过运行期转换的话无法得到真正的内存入口地址,也就无法直接被虚拟机使用。
常量池中的常量类型:
这些常量类型(表)都有一个共同的特点,就是表开始的第一位是一个u1类型的标志位,代表当前这个常量属于哪种常量类型。
常量池中的常量类型表结构:
查看常量池的第一个常量:
(上图中CONSTANT_NameAndType_info 改为 CONSTANT_NameAndType)
将二进制和字节码结合起来看:
下图红线区域为常量池区域:
(4) 访问标志
在常量池结束后,紧接着的两个字节代表访问标志,这个标志用于识别一些类或者接口层次的访问信息。
包括:还这个Class是类还是接口;是被定义为public类型;是否定义为abstract;如果是类的话,是否被声明为final等。具体标志位见下图:
TestClass是一个普通Java类,不是接口、枚举或者注解,被public关键字修饰但没有声明为final和abstract。
所以ACC_PUBLIC
和 ACC_SUPER
为真,其它的标识位都为假。两个为真的标志位的值相加为0x0021
。
(5) 类索引、父类索引和接口索引集合
类索引和父类索引都是一个u2类型的数据,而接口类型是一组u2类型的数据的集合,Class文件中由这三项数据来确定这个类的继承关系。类索引和父类索引以及接口索引都按照顺序排列在访问标识之后。
类索引用于确定这个类的全限定名称,父类索引用于确定这个类的父类的全限定名称。
由于java不允许多继承,所以父类索引就只有一个(除了Object类),接口索引集合就是用来描述这个类实现了哪些接口。对于接口索引,第一项是索引计数器,如果该类没有实现任何接口,则该项为0,后面的索引表不占用任何字节。
类索引和父类索引查找全限定名的过程:
(6) 字段表集合
字段表集合用于描述接口或者类中声明的变量。字段包括类级变量和实例变量(统称成员变量,即不包括局部变量)。
描述一个成员变量,可以包括以下信息:
字段的作用域(private、protected、public修饰符)、是否是静态变量(static)、可变性(final)、并发可见性(volatile修饰符)、是否可被序列化(transient修饰符)、字段数据类型(基本类型、对象、数组)、字段名称。这些属性要么有要么没有,所以很适合用标志位表示。
字段表结构:
access_flags
里存放字段修饰符。
name_index
里存放字段的简单名称。
descriptor_index
里存放字段的描述符。
简单名称:比如incr()方法 和m字段,其简单名称分别为incr 和 m
全限定名:以TestClass为例,其全限定名为org/fenixsoft/clazz/TestClass
描述符:描述字段的数据类型、方法的参数列表和返回值。
字段访问标志:
描述符标识字符含义:
对于数组类型,每一纬度将使用一个前置“[”
字符来描述,如一个定义为“java.lang.String[][]”
类型的二数组,将被记录为:“[[Ljava/lang/String;”
,一个整型数组“int[]”
将记录为“[I”
。
将二进制和字节码结合起来看:
注意:父类的字段信息不会在字段表集合里出现。
(6) 方法表集合
Class文件存储格式中对方法的描述与对字段的描述几乎采用了完全一致的方式。
方法表结构:
方法访问标志:
将二进制和字节码结合起来看:
注意:如果父类方法没有在子类中被重写,方法表集合中就不会出现来自父类的信息。
但同样的,有可能会出现由编译器自动添加的方法,最经典的便是类构造器和实例构造器。
在Java语言中,要重载一个方法,需要保证方法名相同,参数列表不同(即参数个数,顺序)。这里的参数列表就是特征签名。特征签名就是一个方法中各个参数在常量池中字段符号引用的集合,也就是因为返回值不包括在特征签名之中,因此在Java语言中,无法仅仅依靠返回值的不同来判断一个方法是否被重载。但是在Class文件格式中,特征签名范围更大一些,只要描述符不完全一致的两个方法可以共存
也就是说,方法名称相同,特征签名相同,但是返回值不同的两个方法,可以合法地共存于Class文件中。
(7) 属性表集合
在Class文件、字段表、方法表都可以携带自己的属性表集合,以用于描述某些场景专有的信息。
与Class文件中其他的数据项目要求严格的顺序、长度和内容不同,属性表集合的限制稍微宽松了一些,不再要求各个属性表具有严格顺序,并且只要不与现有的属性重复即可,任何人实现的编译器都可以向属性表中写入自己定义的属性信息,Java虚拟机运行时会忽略掉它不认识的属性。
下面列出一些属性中关键常用的部分:
每个属性的名称需要从CONSTANT_Utf8_info里获取,属性值的结构则完全自定义,只需要通过一个u4长度的属性去说明属性所占用的位数即可。一个符合规则的属性表应该满足:
Code属性:
Java程序方法体中的代码经过javac编译后,最终变成字节码指令存储在Code属性内。Code属性出现在方法表的属性集合中,但并非所有方法都具备这个属性,比如说接口或抽象类的无方法体的方法。
Code属性表的结构:
attribute_name_index
:属性名称索引项
attribute_length
:属性长度
max_stack
:操作数栈的最大深度
max_locals
:局部变量所需要的存储空间,这里单位是Slot
code_length
:字节码的长度
code
:字节码指令
在字节码指令之后是这个方法的异常处理表。
异常表对于Code属性来说并不是一定存在的,我们上面的TestClass就没有异常表生成。
异常表实际上是Java代码的一部分,编译器使用异常表而不是简单的跳转命令来实现Java异常即finally处理机制。
异常表结构:
如果当字节码在第start_pc
行到第end_pc
行之间(不含第end_pc
行)出现了类型为catch_type
或者其子类异常,则转到handler_pc
行继续处理。
Exceptions属性:
这里的Exceptions属性是在方法表中与Code属性平级的一项属性,读者不要和前面的异常表产生混淆。Exceptions属性的作用是列举出方法中可能抛出的受查异常,也就是方法描述时在throws关键字后面列举的异常。它的结构如下:
number_of_exceptions
:表示可能会抛出多少种受查异常
exception_index_table
:指向常量池中CONSTANT_Class_info
型常量的索引,代表了该受查异常的类型
LineNumberTable属性:
用于表示Java源行号和字节码行号的对应关系。它并不是运行时必须的属性,但是默认生成到Class文件中。
它的结构如下:
line_number_table_length
:line_number_info
的数量
line_number_info
:包括了start_pc
和line_number
两个u2类型的数据项
LocalVariableTable属性:
用于描述栈帧中局部变量表中的变量与Java源码中定义的变量之间的关系。它并不是运行时必须的属性,但是默认生成到Class文件中。
LocalVariableTable结构:
local_variable_table结构:
start_pc
和length
分别代表了这个局部变量的生命周期开始的字节码偏移量及其作用范围覆盖的长度,两者结合起来就是这个局部变量在字节码中的作用域范围。
name_index
和descriptor_index
都是指向常量池中CONSTANT_Utf8_info
型常量的索引,分别代表了这个局部变量的简单名称和描述符。
index
表示这个局部变量在栈帧局部变量表中的Slot
的位置。
SourceFile属性:
用于记录这个Class文件的源码文件名称。
还有一些主要的属性及其他属性,这里不再记录。