在上一篇文章中我们一起来看了一下 Java 虚拟机的类加载过程,包括虚拟机加载、验证、准备、解析和初始化 5 个大步骤,同时我们还讨论了 Java 虚拟机加载类时采用的双亲委派模型思想。在这篇文章中我们来一起看一下 class 文件的结构,来进一步加深我们对虚拟机的类加载机制和类机制的理解。本文参考了 《深入理解 Java 虚拟机》一书。
我们都知道一个 Java 类(.java
)文件在被 Java 编译器(javac
) 编译过后,如果语法没有错误,则会生成一个对应的 .class
文件,这个 .class
文件是一个二进制文件,用一定的格式保存了我们书写的类的所有信息。不像 xml
和 json
这些带有标志的语言一样,.class
文件是一个纯二进制文件,其中的数据是紧凑并且没有任何分隔符的,因此 .class
文件中的数据的顺序和含义是具有非常严格的规定的,.class
文件中的数据格式可以用以下表格描述:
类型 | 名称 | 数量 | 含义 |
---|---|---|---|
u4 | magic | 1 | 文件魔数 |
u2 | minor_version | 1 | 次版本号 |
u2 | major_version | 1 | 主版本号 |
u2 | constant_pool_count | 1 | 常量池中的常量数 + 1 |
cp_info | constant_pool | constant_pool_count - 1 | 常量池信息 |
u2 | access_flag | 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 | attributes_count | 1 | 类的其他属性信息数量 |
attribute_info | attributes | attributes_count | 类的其他属性信息 |
其中,u
代表无符号整数类型,u1
则代表 1 个字节的无符号整数类型,u2
代表 2 个字节的无符号整数类型,u4
代表 4 个字节的无符号整数类型,而其他的类型(cp_info
, field_info
, method_info
, attribute_info
)等则是 .class
文件自定义的数据结构(表)。有了这个基础之后,我们来通过例子来理解上面表格的数据含义,新建一个 ClassContent
类:
public class ClassContent {
public static final String STR = "s";
public static final int A = 1;
public int getInt() {
int x = 1;
return x;
}
public static void main(String[] args) {
System.out.println("hello");
}
}
非常简单的一段代码,没有什么特殊含义,我们编译这个类,可以得到对应的类文件(ClassContent.class
),
用16进制编辑器打开对应的类文件(ClassContent.class
),笔者这边使用的是 010 Editor:
前 4 个字节为 .class
文件的魔数,用于做一个最简单的文件类型校验。比如你将一个 .jpg
类型的图片文件后缀名改成 .class
时,这个 .class
文件是无效的,因为第一步的魔数就不一样。这个是一个固定的数据,对于其他的文件类型可能也存在,只不过值不一样而已。在 .class
文件中值为 CAFEBABY
(咖啡宝贝?),这个值非常有意思,因为其意义正好对应 Java 的图标:
来杯 82 年的 Java 压压惊?
按字节顺序继续往下看,接下来的 2 个字节代表该 .class
文件要求装载它的虚拟机的最低次版本号,这里为 0,证明只要虚拟机的主版本号不小于当前类文件的主版本号就可以加载这个类。
接下来两个字节代表该 .class
文件要求装载它的虚拟机的最低主版本号,这里为 0x0034
,即为 10 进制的 52,JDK 的主版本号是从 45 开始的,接下去的每一个大版本号都为之前的大版本号 + 1,高版本号的虚拟机可以向下兼容加载低版本号的类文件,但无法加载版本号比它更高的类文件。这个其实很好理解,因为 Java 每一次升级都会带来一些语法变化和一些新特性(Java 8 带来了新的 API,lambda 表达式等),对应的会带来支持这些新特性的虚拟机,而老版本的虚拟机并不支持这些新特性,所以为了安全,虚拟机不允许加载版本号比自己的版本号高的类文件。
比如 JDK 1 能加载版本号为 45.0 ~ 45.65535 的 .class
文件,但是无法加载版本号为 46.0 ~ 46.65536 的 .class
文件。在这个例子中 .class
文件主版本号为 52,次版本号为 0,总版本号为 52.0,证明其只能被 JDK 1.8 以上版本提供的虚拟机加载。
接下来的多个字节代表当前类的常量池信息,我们先看紧接着前面的两个字节:0x002D
,这个值为 10 进制的 45,意味着该类中的常量数为 44,为什么是 44 呢?因为 .class
中的常量的下标从 1 开始,那么下标 0 代表的是什么呢?代表的是某项数据不引用常量中的任何常量项目,这句话怎么理解呢?我们可以从对象的值上理解,比如我们有一个 Person
类,现在我们定义了一个 Person
类的引用:Person person;
,我们暂时没有 Person
类型的对象去给这个引用赋值,那么我们会把它赋值为 null:person = null;
。这就相当于不引用任何Personal类型的对象项目,那么类文件中的常量池的下标 0 也是一样的道理:如果类文件中某个项目引用到的常量的下标为 0,证明这个项目不需要使用常量池中任何一个项目的值。
另外,常量池中的项目类型是多样的,因为常量本身就是多类型的(字符串常量、整形常量、浮点型常量、复杂对象类型常量…)下表列举了所有常量池中的项目可能出现的类型:
类型 | 标志 | 含义 |
---|---|---|
CONSTANT_Utf8_info | 1 | UTF-8 编码的字符串 |
CONSTANT_Integer_info | 3 | 整形字面量 |
CONSTANT_Float_info | 4 | 浮点型字面量 |
CONSTANT_Long_info | 5 | 长整型字面量 |
CONSTANT_Double_info | 6 | 双精度浮点型字面量 |
CONSTANT_Class_info | 7 | 类或者接口的符号引用 |
CONSTANT_String_info | 8 | 字符串类型字面量 |
CONSTANT_Fieldref_info | 9 | 字段的符号引用 |
CONSTANT_Methodref_info | 10 | 类中方法的符号引用 |
CONSTANT_InterfaceMethodref_info | 11 | 接口中方法的符号引用 |
CONSTANT_NameAndType_info | 12 | 字段或者方法的部分符号引用 |
CONSTANT_MethodHandle_info | 15 | 表示方法句柄 |
CONSTANT_MethodType_info | 16 | 标识方法类型 |
CONSTANT_InvokeDynamic_info | 18 | 表示一个动态方法调用点 |
非常繁琐的是上面的每一种常量类型都有一个自定义的数据结构(表)类型,这些结构我们在碰到时再进行列举。这些表结构有一个共性:表的第一个字节数据名为 tag,代表的是当前常量的类型,(即为上面常量类型表中的标志)。我们来看上面 .class
文件的第一个常量类型(紧接着上面的常量数量字节后面的 1 个字节数据):0x0A
,即为 10 进制的 10,对应的常量类型为 CONSTANT_Methodref_info
,即为一个描述方法的常量类型,那么我们来看一下这种类型的常量的表结构:
类型 | 名称 | 含义 |
---|---|---|
u1 | tag | 常量类型,值为 10 |
u2 | class_index | 指向声明这个方法的类描述符 CONSTANT_Class_info 的索引项 |
u2 | method_index | 指向描述该方法信息的描述符 CONSTANT_NameAnd_Type_info 的索引项 |
这个表的属性也很好理解,既然这个表示用来描述一个类的方法的信息,那么我们肯定需要知道定义这个方法的类的信息和这个方法本身的信息(方法名、参数个数和类型、返回值等),我们继续看紧跟着 tag 的后两个字节:0x0006
,即为 10 进制的 6,也就是说这个方法所属的类的信息储存在常量池中第 6 个常量中,我们继续看后面两个字节:0x001E
,即为 10 进制的 30,也就是说这个方法本身的信息在储存在常量池中的第 30 个常量中。它们的具体值我们待会可以通过工具生成,这里需要知道这个分析过程即可。我们到这里已经分析完了常量池的第一个常量字段,下面我们来简单看一下第二个常量,来巩固一下这个分析过程。
紧接着第一个常量信息字节结束,第二个常量的 tag 值为 ox09
,即为 10 进制的 9,对应的常量类型为 CONSTANT_Fieldref_info
类型,即为一个描述字段信息的常量类型,我们来看一下这种类型的常量的表结构:
类型 | 名称 | 含义 |
---|---|---|
u1 | tag | 常量类型,值为 9 |
u2 | class_index | 指向声明这个字段的类或接口的描述符 CONSTANT_Class_info 的索引项 |
u2 | field_index | 指向描述该字段信息的描述符 CONSTANT_NameAnd_Type_info 的索引项 |
和方法信息描述表类似,字段也有所属的类和字段本身的信息(修饰符、类型、名称)。我们来看接下来的 4 个字节,分析出这个字段的所属类信息和其本身的信息,这两个值分别为 0x001F
和 0x0020
,对应 10 进制分别为 31 和 32。也就是说这两个信息储存在常量池中第 31 个和第 32 个常量中。这两个常量中我们都碰到了 CONSTANT_Class_info
和 CONSTANT_NameAnd_Type_info
这两种常量类型,那么我们来看一下他们的表结构,首先是 CONSTANT_Class_info
`:
类型 | 名称 | 含义 |
---|---|---|
u1 | tag | 常量类型,值为 7 |
u2 | index | 指向这个类的全限定名的常量项索引 |
接下来是 CONSTANT_NameAnd_Type_info
:
类型 | 名称 | 含义 |
---|---|---|
u1 | tag | 常量类型,值为 12 |
u2 | name_index | 指向该字段或者方法名称的常量项的索引 |
u2 | descibe_index | 指向该字段或者方法描述符的常量项的索引 |
好了,有了上面的基础之后,我们来借助工具来看一下常量中的所有常量信息,我们在 ClassContent.class
所在的目录下执行 javap -v ClassContent.class
命令行来获取该类文件的详细信息:
由于版面原因,这里并没有截取所有的字节码,这里我们关注常量池中的信息即可,我们可以看到刚刚我们分析的前两个常量分别为 CONSTANT_Methodref_info
和 CONSTANT_Fieldref_info
类型,和这里生成的常量类型匹配,同时第一个方法描述常量对应的类信息为第 6 个常量,第 6 个常量为 CONSTANT_CLass_info
类型,其类的全限定名用一个 CONSTANT_Utf8_info
类型的常量来描述,这个常量类型的表结构如下:
类型 | 名称 | 含义 |
---|---|---|
u1 | tag | 常量类型,值为 1 |
u2 | length | UTF-8 编码的字符串所占用的字节数 |
u1 | bytes | 长度为 length 并使用 UTF-8 编码后的字符串数据,总体占用 length 个字节 |
这里的类的全限定名为 java/lang/Object
,也就是这个方法是在 java.lang.Object
中定义的,我们再看方法的详细信息,在第 30 个常量中,这个常量类型为 CONSTANT_NameAnd_Type_info
类型,对应的方法名和描述分别为 <init>
和 ()V
,还原之后为 void <init>()
,这个方法本身在 Object
类中并没有定义,这是因为类在编译时编译器为这个类自动生成的一个方法,类中的一些非静态代码块和非静态变量赋值操作都会移至该方法中执行。
接下来的是描述字段类型的常量,所属的类信息在第 31 个常量中储存,字段本身信息在第 32 个常量中储存,继续 “递归” 查找,发现这个字段所属的类为 java.lang.System
,字段名为 out
,类型为 java.io.PrintStream
。其实这个就是因为我们代码中调用了 System.out.println
方法导致的,在进行代码编译时,编译器会将某个类中的常量值储存在该常量调用类的常量池中。
总体看常量池中的常量信息,我们会发现能直接储存“字面量”值的只有基本类型对应的常量数据类型(CONSTANT_Utf8_info
, CONSTANT_Integer_info
, CONSTANT_Float_info
, CONSTANT_Double_info
, CONSTANT_Long_info
)。那么如果我们定义了一个 boolean
类型的常量会怎么样呢?它会被当作 int
类型的常量处理(byte
,char
,short
类型的常量也是如此 )。所有的复杂常量类型中的属性真实值最终都是通过这几个基本表中的值来储存。最后给出所有常量类型的数据表结构(来自《深入理解 Java 虚拟机》):
在常量池部分结束了之后,紧接着的两个字节代表的是访问标识,访问标识即为该类定义时的访问权限,比如该类是否为抽象类,是否为接口,是否为 final 类,是否为枚举等等。具体的标志位和含义如下表:
标志名称 | 标志值 | 含义 |
---|---|---|
ACC_PUBLIC | 0x0001 | 该类是否为 public 类型 |
ACC_FINAL | 0x0010 | 该类是否为 final 类 |
ACC_SUPER | 0x0020 | 是否允许使用 invokespecial 字节码指令的新语义,invokespecial指令的语义在 JDK1.0.2 发生改变过,因此在 JDK 1.0.2 之后编译出来的类中这个标志一定为真 |
ACC_INTERFACE | 0x0200 | 该类是否为接口类型 |
ACC_ABSTRACT | 0x0400 | 是否为 abstract 类型,对于接口和抽象类来说,这个标志为真,其他为假 |
ACC_SYNTHETIC | 0x1000 | 标志这个类并非由用户代码产生的(动态代理) |
ACC_ANNOTATION | 0x2000 | 标识这是一个注解 |
ACC_ENUM | 0x4000 | 标识这是一个枚举 |
对于我们上面的 ClassContent
类来说,其是 public
修饰的,同时是使用 JDK 1.8 语法编译的,因此它的 ACC_PUBLIC
和 ACC_SUPER
标志为真,即其 access_flag
的值应该为 0x0021
,我们来看一下:
图中红色线条标注部分即为 access_flag
标志开始部分,我们可以看到这个结果和我们上面计算的相符。
紧接着 access_flag
后面的两个字节为 this_class
数据,表示的是当前类信息,其值作为下标指向常量池的一个 CONSTANT_Class_info
类型的常量,这里的值为 0x0005
,即为常量池中的第 5 个常量,我们回过头去看看,第 5 个常量确实为 CONSTANT_Class_info
类型,类的全限定名具体值为第 36 个常量的值,即为 ClassContent
。
接下来的两个字节数据为该类的父类的信息,值为 0x0006
,同样的我们去找一下常量池中第 6 个常量的信息,对应的类的全限定名储存在第 37 个常量中,即为 java/lang/Object
。也就是说 ClassContent
类的父类为 Object
类,这也是毋庸置疑的。
接下来的两个字节数据为该类实现的接口数量,如果该类本身是一个接口,则为该接口继承的接口数量。因为 ClassContent
并没有实现任何接口,因此这两个字节的数据为 0x0000
,即为 0。
接下来会有 interfaces_count
个 u2 类型的数据,指向的是描述该类实现 / 继承的接口的信息常量在常量池中的下标(均为 CONSTANT_Class_info
类型)。因为在这里 ClassContent
并没有实现任何接口,因此这里没有任何数据。
和 interfaces_count
属性类似,接下来的两个字节数据代表的是该类定义的字段数,在这里为 0x0002
,即为两个字段。
紧接着是 fileds_count
个 fields_info
表结构的数据,描述的是当前类定义的字段的信息,fields_info
表结构如下:
类型 | 名称 | 数量 | 含义 |
---|---|---|---|
u2 | access_flag | 1 | 字段的访问标识 |
u2 | name_index | 1 | 字段名常量在常量池中的下标 |
u2 | descriptor_index | 1 | 字段类型在常量池中的下标 |
u2 | attributes_count | 1 | 额外属性信息数量 |
attribute_info | attributes | attributes_count | 额外属性信息 |
我们来继续看字段信息数据,紧接着的两个字节为 access_flag
,这里值为 0x0019
,我们来看一下在字段中可能出现的访问标志:
标志名称 | 标志值 | 含义 |
---|---|---|
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 | 0x1000 | 标志这个字段由编译器自动生成的 |
ACC_ENUM | 0x4000 | 标识字段是否为一个枚举 |
根据这个表,我们可以还原出第一个字段的修饰符:public static final
(10 + 8 + 1)。我们接着看下两个字节(name_index)的数据:0x0007
,我们去找下标为 7 的常量值:STR
,也就是说这个字段名为 STR
,继续看下两个字节(descriptor_index)的数据:0x0008
。找到常量池中下标为 8 的常量值:Ljava/lang/String;
意味这这个字段是 String
类型的。我们继续看下两个字节(attributes_count)的数据:0x0001
,意味着这个字段有 1 个额外属性信息。那么接下来的一部分字节数据就是用来描述这个额外的属性表的信息,我们来看看 attribute_info
表的结构:
类型 | 名称 | 数量 | 含义 |
---|---|---|---|
u2 | attribute_name_index | 1 | 属性名在常量池中的常量下标 |
u4 | attributes_length | 1 | 属性数据的长度 |
u1 | info | attributes_length | 属性数据 |
我们接着看下两个字节(attribute_name_index)的数据:0x0009
,在常量池中第 9 项常量的数据为 ConstantValue
,意味着这个属性表为 ConstantValue
表,继续看接下来四个字节(attributes_length)的数据:0x00000002
,意味着该属性值的数据量为 2 个字节,接下来两个字节即为该属性的数据:0x0A00
,即为 10 ,这个属性表的数据分布如下图:
那么这个 10 代表什么意义呢?我们先来看看 ConstantValue
表的结构:
类型 | 名称 | 数量 | 含义 |
---|---|---|---|
u2 | attribute_name_index | 1 | 属性名在常量池中的常量下标 |
u4 | attributes_length | 1 | 属性数据的长度 |
u1 | constantValue_index | attributes_length | 指向常量池中下标为 constantValue_index 的常量 |
好了,我们知道了,上面的那个 10 代表的是常量池中的一个常量的下标,那么我们来看看常量池中第 10 个常量是什么:经过查找,第 10 个常量的字面量为 s
,这不就是我们在代码中定义的常量值吗。至此,我们可以还原出这个字段的完整定义:public static final String STR = "s";
。我们已经解析了第一个常量,第二个常量也是同样的道理,我们提取出描述第二个常量的数据:00 19 00 0B 00 0C 00 01 00 09 00 00 00 02 00 0D
。access_flag
值为 0x0019
,字段名常量所在常量池下标为 0x000B
即为 11,字段类型常量所在常量池下标为 00 0C
,得到的值为 I
(即为 int),常量表中常量值所在常量池的下表为 0x000D
,得到的值为1
。所以这个常量还原出来的代码定义为 public static final int A = 1;
。此外将常量值储存在字段的额外信息中,待虚拟机加载该类时在准备阶段时就可以将最终值赋值给该常量。而一般的静态字段在准备阶段时会被赋 0 值,在初始化阶段中执行 <cinit>
方法时才会赋最终值。
在上面属性表中,属性名储存在常量池中,而属性表本身通过 attribute_name_index 数据指向属性表名字常量在常量池中的下标来记录属性表名称。除了 ConstantValue
以外,Java 虚拟机还有很多其他类型的属性表,最常见的便是 Code
表,它的结构我们在分析类文件中的方法属性时会介绍,如果虚拟机在进行属性表解析时发现属性表名不是其可以识别(内置)的属性表名时,则会略过这一属性表数据的所有数据,继续向后解析。
字段信息解析完成之后,接下来的两个字节的数据代表当前类的方法数,值为 0x0003
:
也就是说这个类一共有三个方法,但是我们明明在代码中只定义了 2 个方法啊,第三个方法怎么来的?别忘了之前提到的 <init>
方法,编译器在编译每一个类时都会为这个类提供一个 <init>
方法。
紧接着便是方法的具体信息了,这里先来看一下 method_info
表的格式:
类型 | 名称 | 数量 | 含义 |
---|---|---|---|
u2 | access_flag | 1 | 方法的访问标识 |
u2 | name_index | 1 | 方法名常量在常量池中的下标 |
u2 | descriptor_index | 1 | 方法描述常量在常量池中的下标 |
u2 | attributes_count | 1 | 额外属性信息数量 |
attribute_info | attributes | attributes_count | 额外属性信息 |
方法可能存在的访问标识如下:
标志名称 | 标志值 | 含义 |
---|---|---|
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 | 方法是否为抽象方法 |
ACC_STRICTFP | 0x0800 | 方法是否为 strictfp |
ACC_SYNTHETIC | 0x1000 | 方法是否为编译器自动生成的 |
如果你理解了上一节字段相关的内容,那么这两个表格对你来说一点难度都没有。我们继续来看方法的内容,紧接着 methods_count
的两个字节为第一个方法的 access_flag
属性值,为 0x0001
,即为 public
修饰的方法,接下来两个字节为 name_index
属性值,为 0x000E
,对应常量池中第 14 个常量,值为 <init>
,即该方法的方法名为 <init>
,接下来两个字节为 descriptor_index
的属性值,为 0x000F
,对应常量池中的第 15 个常量,即该方法的方法描述为 ()V
,也就是说该方法没有参数和返回值,那么我们现在可以复原出这个方法了:public void <init>();
。我们接着来看 attributes_count
属性的值,这里为 0x0001
,即该方法有一个额外的属性表,属性的表名为 attribute_name_index
的值指向常量池对应下标的常量值,即为下标为 0x0010
的常量值,对应的常量池中的常量值为 Code
。我们应该可以猜到这个 Code
属性表就是储存该方法代码的字节码的表结构了,我们来看看 Code
表的结构:
类型 | 名称 | 数量 | 含义 |
---|---|---|---|
u2 | attribute_name_index | 1 | 属性名在常量池中的常量下标 |
u4 | attributes_length | 1 | 属性数据的长度 |
u2 | max_stack | 1 | 方法执行时操作数栈深度的最大值 |
u2 | max_locals | 1 | 方法执行时局部变量表所需要的存储空间,这里的单位 Slot,一个 Slot 可以储存长度不超过32位的局部变量值 |
u4 | code_length | 1 | 代码编译成字节码之后的代码长度 |
u1 | code | code_length | 代码内容,每一个字节数据对应一个字节码 |
u2 | exception_table_length | 1 | 方法的异常表长度 |
exception_info | exception_table | exception_table_length | 方法抛出的异常信息 |
u2 | attribute_count | 1 | 额外属性表数目 |
attribute_info | attributes | attribute_count | 额外属性表信息 |
<init>
方法的 Code
属性完整数据如下(底色为深蓝色的数据):
这个方法存在 5 个字节的字节码数据,字节码之所以被称为字节码,就是因为每一个字节码都有一个字节的数据对应,通过一个字节的数据可以确定一个字节码,Java 虚拟机已经定义了 200 多条字节码指令(一个字节的数据范围为 0~255)。我们通过某个字节的数据就可以得到其所代表的字节码。在这里 <init>
方法中的 5 条字节码指令分别为:
aload_0: 将第一个引用类型本地变量推至操作数栈顶
invokespecial: 调用父类构造方法,实例初始化方法,似有方法(这里为调用父类构造方法)
nop: 什么都不做
aconst_null: 将 null 推至操作数栈顶
return: 从当前方法返回 void
接下来的是方法的异常信息和额外信息,这部分篇幅过大了,这里不细讲了。小伙伴们可以参考 《深入理解 Java 虚拟机》原书。剩下的两个方法小伙伴们可以自行分析。
紧接着方法字节数据结束后的两个字节为 attributes_count
数据,代表的是当前类的额外属性表数量。在这个类文件中存在一个额外属性表,这部分数据在类文件数据末尾:
紧接着上面的 attributes_count
数据后的数据为当前类文件的唯一一个属性表数据:
根据属性表的一般结构,我们可以得到该属性表的表名属性(attribute_name_index)为 0x001C
对应常量池中的常量为 SourceFile
,也就是说这是一个 SourceFile
表,我们来看看 SourceFile
表的结构:
类型 | 名称 | 数量 | 含义 |
---|---|---|---|
u2 | attribute_name_index | 1 | 属性名在常量池中的常量下标 |
u4 | attributes_length | 1 | 属性表中数据的长度 |
u2 | sourcefile_index | 1 | 指向常量池中下标为 sourcefile_index 的常量 |
这里我们可以得到该表中 sourcefile_index
的值为 0x001D
,对应常量池中下标为 29 的常量,即为 ClassContent.java
,这也就是编译出这个 .class
文件的 Java 文件的文件名。
与字段和方法中的属性表相同,这里的属性表可以有多种类型,Java 虚拟机内置的属性表的表名如下:
我们在上面已经讲过了 Code
、ConstantValue
和 SourceFile
属性表的结构,关于其他属性表的结构就需要小伙伴们自己去参阅相关书籍和资料了。
回想一下我们在上篇文章中讨论的 Java 类加载机制,需要经过五大步骤:加载、验证、准备、解析、初始化。其实 验证过程是最复杂的,因为这个过程需要扫描整个在加载过程中得到得到的 .class
文件格式的二进制数据,也就是相当于将我们在上面模拟的解析 .class
文件的过程,并且判断相关的数据是否合法,比如文件的魔数是否为 CAFEBABY
,类的主次版本号是否不高于当前虚拟机的版本号,同时需要检测字节码的调用是否合法,比如一个局部变量是不是在未被赋初始值就被使用了,是否使用了不当的跳转指令等等。总言之这部分工作需要确保该类在加载进入虚拟机之后在被使用的过程中不会出现致命性的问题以威胁虚拟机的正常运行状态。
而在解析这一步中虚拟机需要将类中出现的符号引用替换为直接引用,这个过程可能又会触发其他类的加载,比如有两个类 A 和类 B ,类 B 中有一个 A 类的引用,那么在加载类 B 的时,在解析过程中发现引用了 类 A 的变量,那么这时候可能会触发虚拟机对类 A 的加载。
好了,在这篇文章中我们通过一个例子来看了一下类文件格式,相信你对 Java 类机制有了一个更深的理解。如果博客中有什么不正确的地方,还请多多指点。如果觉得这篇文章对您有帮助,请不要吝啬您的赞。
谢谢观看。。。