Class类文件的结构
任何一个Class文件都对应着唯一一个类或接口的定义信息,但反过来说,类或接口并不一定都得定义在文件里(譬如类或接口也可以通过类加载器直接生成)。
Class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在Class文件之中,中间没有添加任何分隔符,这使得整个Class文件中存储的内容几乎全部是程序运行的必要数据,没有空隙存在。当遇到需要占用8位字节以上空间的数据项时,则会按照高位在前[1]的方式分割成若干个8位字节进行存储。
根据Java虚拟机规范的规定,Class文件格式采用伪结构来存储数据,这种伪结构中只有两种数据类型:无符号数和表,后面的解析都要以这两种数据类型为基础。
无符号数属于基本的数据类型,以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值。
表是由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都习惯性地以“_info”结尾。表用于描述有层次关系的复合结构的数据,整个Class文件本质上就是一张表,它由表6-1所示的数据项构成。
无论是无符号数还是表,当需要描述同一类型但数量不定的多个数据时,经常会使用一个前置的容量计数器加若干个连续的数据项的形式,这时称这一系列连续的某一类型的数据为某一类型的集合。
魔数与Class文件的版本
每个Class文件的头4个字节称为魔数(Magic Number),它的唯一作用是确定这个文件是否为一个能被虚拟机接受的Class文件。很多文件存储标准中都使用魔数来进行身份识别,譬如图片格式,如gif或者jpeg等在文件头中都存有魔数。
紧接着魔数的4个字节存储的是Class文件的版本号:第5和第6个字节是次版本号(Minor Version),
第7和第8个字节是主版本号(Major Version)。Java的版本号是从45开始的,JDK 1.1之后的每个JDK大版本发布主版本号向上加1(JDK 1.0~1.1使用了45.0~45.3的版本号),高版本的JDK能向下兼容以前版本的Class文件,但不能运行以后版本的Class文件,即使文件格式并未发生任何变化,虚拟机也必须拒绝执行超过其版本号的Class文件。
package org.clazz;
public class TestClass{
private int m;
public int inc(){
return m+1;
}
}
开头4个字节的十六进制表示是0xCAFEBABE,
代表次版本号的第5个和第6个字节值为0x0000,
而主版本号的值为0x0033,也即是十进制的51,该版本号说明这个文件是可以被JDK 1.7或以上版本虚拟机执行的Class文件。
从JDK 1.1到JDK 1.7,主流JDK版本编译器输出的默认和可支持的Class文件版本号
这种顺序称为“Big-Endian”,具体是指最高位字节在地址最低位、最低位字节在地址最高位的顺序来存储数据,它是SPARC、PowerPC等处理器的默认多字节存储顺序,而x86等处理器则是使用了相反的“Little-Endian”顺序来存储数据。
常量池
紧接着主次版本号之后的是常量池入口,常量池可以理解为Class文件之中的资源仓库,它是Class文件结构中与其他项目关联最多的数据类型,也是占用Class文件空间最大的数据项目之一,同时它还是在Class文件中第一个出现的表类型数据项目。
由于常量池中常量的数量是不固定的,所以在常量池的入口需要放置一项u2类型的数据,代表常量池容量计数值(constant_pool_count),计数是从1开始的,如果十六进制数0x0013,即十进制的19,这就代表常量池中有19项常量,索引值范围为1~19。
在Class文件格式规范制定之时,设计者将第0项常量空出来,某些指向常量池的索引值的数据在特定情况下需要表达“不引用任何一个常量池项目”的含义,这种情况就可以把索引值置为0来表示。
Class文件结构中只有常量池的容量计数是从1开始,对于其他集合类型,包括接口索引集合、字段表集合、方法表集合等的容量计数都是从0开始的。
常量池中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References)。
字面量比较接近于Java语言层面的常量概念,如文本字符串、声明为final的常量值等。
而符号引用则属于编译原理方面的概念,包括了下面三类常量:
类和接口的全限定名(Fully Qualified Name)
字段的名称和描述符(Descriptor)
方法的名称和描述符
Java代码在进行Javac编译的时候,是在虚拟机加载Class文件的时候进行动态连接。
在Class文件中不会保存各个方法、字段的最终内存布局信息,因此这些字段、方法的符号引用不经过运行期转换的话无法得到真正的内存入口地址,也就无法直接被虚拟机使用。当虚拟机运行时,需要从常量池获得对应的符号引用,再在类创建时或运行时解析、翻译到具体的内存地址之中。
常量池中每一项常量都是一个表,在JDK 1.7之前共有11种结构各不相同的表结构数据,在JDK 1.7中为了更好地支持动态语言调用,又增加了3种(CONSTANT_MethodHandle_info、CONSTANT_MethodType_info和CONSTANT_InvokeDynamic_info)。
这14种表都有一个共同的特点,就是表开始的第一位是一个u1类型的标志位(tag),代表当前这个常量属于哪种常量类型。
之所以说常量池是最烦琐的数据,是因为这14种常量类型各自均有自己的结构。上图常量池的第一项常量,它的标志位是0x0A,即十进制的10,查表 这个常量属于CONSTANT_Methodref_info类型
在JDK的bin目录中,Oracle公司已经为我们准备好一个专门用于分析Class文件字节码的工具:javap
F:\>javap -v TestClass.class
Classfile /F:/TestClass.class
Last modified 2017-8-15; size 285 bytes
MD5 checksum cbd7dbd2cc55984adc03a6383debcf45
Compiled from "TestClass.java"
public class org.clazz.TestClass
SourceFile: "TestClass.java"
minor version: 0
major version: 51
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #4.#15 // java/lang/Object."<init>":()V
#2 = Fieldref #3.#16 // org/clazz/TestClass.m:I
#3 = Class #17 // org/clazz/TestClass
#4 = Class #18 // java/lang/Object
#5 = Utf8 m
#6 = Utf8 I
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 inc
#12 = Utf8 ()I
#13 = Utf8 SourceFile
#14 = Utf8 TestClass.java
#15 = NameAndType #7:#8 // "<init>":()V
#16 = NameAndType #5:#6 // m:I
#17 = Utf8 org/clazz/TestClass
#18 = Utf8 java/lang/Object
{
public org.clazz.TestClass();
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 2: 0
public int inc();
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 5: 0
}
访问标志
类索引、父类索引与接口索引集合
字段表集合
方法表集合
属性表集合
package org.clazz;
public class TestClass {
private int m;
public int inc() {
int x;
try {
x = 1;
return x;
} catch (Exception e) {
x = 2;
return x;
} finally {
x = 3;
}
}
}
F:\>javap -v TestClass.class
Classfile /F:/TestClass.class
Last modified 2017-8-16; size 433 bytes
MD5 checksum 0a7171d89c29166ea628992e27f96d84
Compiled from "TestClass.java"
public class org.clazz.TestClass
SourceFile: "TestClass.java"
minor version: 0
major version: 51
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #4.#18 // java/lang/Object."<init>":()V
#2 = Class #19 // java/lang/Exception
#3 = Class #20 // org/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 inc
#12 = Utf8 ()I
#13 = Utf8 StackMapTable
#14 = Class #19 // java/lang/Exception
#15 = Class #22 // java/lang/Throwable
#16 = Utf8 SourceFile
#17 = Utf8 TestClass.java
#18 = NameAndType #7:#8 // "<init>":()V
#19 = Utf8 java/lang/Exception
#20 = Utf8 org/clazz/TestClass
#21 = Utf8 java/lang/Object
#22 = Utf8 java/lang/Throwable
{
public org.clazz.TestClass();
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<i
":()V
4: return
LineNumberTable:
line 2: 0
public int inc();
flags: ACC_PUBLIC
Code:
stack=1, locals=5, args_size=1
0: iconst_1 //try块中的x=1
1: istore_1
2: iload_1 //保存x到returnValue中,此时x=1
3: istore_2
4: iconst_3 //finaly块中的x=3
5: istore_1
6: iload_2 //将returnValue中的值放到栈顶,准备给ireturn返回
7: ireturn
8: astore_2 //给catch中定义的Exception e赋值,存储在Slot 2中
9: iconst_2 //catch块中的x=2
10: istore_1
11: iload_1 //保存x到returnValue中,此时x=2
12: istore_3
13: iconst_3 //finaly块中的x=3
14: istore_1
15: iload_3 //将returnValue中的值放到栈顶,准备给ireturn返回
16: ireturn
17: astore 4 //如果出现了不属于java.lang.Exception及其子类的异常才会走到这里
19: iconst_3 //finaly块中的x=3
20: istore_1
21: aload 4 //将异常放置到栈顶,并抛出
23: athrow
Exception table:
from to target type
0 4 8 Class java/lang/Exception
0 4 17 any
8 13 17 any
17 19 17 any
LineNumberTable:
line 8: 0
line 9: 2
line 14: 4
line 10: 8
line 11: 9
line 12: 11
line 14: 13
StackMapTable: number_of_entries = 2
frame_type = 72 /* same_locals_1_stack_item */
stack = [ class java/lang/Exception ]
frame_type = 72 /* same_locals_1_stack_item */
stack = [ class java/lang/Throwable ]
}
如果没有出现异常,返回值是1;字节码中第0~4行所做的操作就是将整数1赋值给变量x,并且将此时x的值复制一份副本到最后一个本地变量表的Slot中(这个Slot里面的值在ireturn指令执行前将会被重新读到操作栈顶,作为方法返回值使用。returnValue)。