从学习Java语言的第一天起,我们就被告知与其他语言相比,Java的一大特点在于它的平台无关性,即Write Once, Run Everywhere. 而构成平台无关性的基石就在于所有JVM都采用了字节码作为它们的程序存储格式,因此今天主要就分析一下class文件的结构。
知识准备
在详细分析class文件结构之前,我们需要了解一些基本概念:
- class文件以8字节为基本单位来进行存储,中间没有任何分隔符;
- 当数据项需要占用的空间大于8字节时,会按照高位在前的方式来进行分割;
- class文件只有两种数据类型:无符号数、表;
- 无符号数属于基本数据类型,以u1、u2、u4、u8分别代表1个字节、2个字节、4个字节和8个字节的无符号数;
- 表是由多个无符号数或者其它表作为数据项构成的符合数据类型,表名习惯性都以 _info 结尾。
因此本质上整个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 | attributes_count | 1 | 属性个数 |
attribute_info | attributes | attributes_count | 具体属性 |
可以看到,这16种数据项大致可以分为3类:
- 3个描述文件属性的数据项:魔数和主次版本号
- 11个描述类属性的数据项:类、字段、方法等信息
- 2个描述代码属性的数据xiang:
接下来我们就逐一来看看这些数据项的含义。整个分析过程我们将以下面这段代码对应的class文件为基础:
public class JavaTest {
private static String name = "JVM";
public static void main(String[] args) {
System.out.println("Hello " + name);
}
}
1.魔数与版本
每个class文件的头4个字节称为魔数,用于确定这个文件是否能被虚拟机所接受。class文件的魔数值为CAFEBABE。
第5、6字节为次版本号,7、8字节为主版本号。Java的主版本号从45开始,JDK1.1之后每个大版本发布,主版本号加1。高版本的jdk能前向兼容之前版本的class文件,但不能运行以后版本的class文件。
从图1可以看到,次版本号为0000,主版本号为0031,这说明该class文件可以被1.5及以后版本的jdk运行。
2.常量池
紧接着主版本号之后的是常量池入口,由于常量池中常量数量不固定,因此入口使用第一个u2类型的数据代表常量池计数值,该计数器从1开始。图1中常量池计数值为0034,代表常量池中一共有51个常量。
常量池中每一个常量都是一个表,jdk1.7之后一共有14种类型的常量,他们对应着14个不同结构的表,但这14个表都有一个共同特点:那就是表开始的第一位是一个u1类型的标志位,代表当前常量属于哪种常量类型。其取值和含义如下表所示:
类型 | 标志 | 描述 |
---|---|---|
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_InterfaceMehtodref_info | 11 | 接口方法的符号引用 |
CONSTANT_NameAndType_info | 12 | 字段或方法的部分符号引用 |
CONSTANT_MethodHandle_info | 15 | 方法句柄 |
CONSTANT_MethodType_info | 16 | 方法类型 |
CONSTANT_InvokeDynamic_info | 18 | 动态方法调用点 |
这14种常量的结构如下表所示:
有了这些基础,我们继续分析前面提到的class文件:
第一个u1类型的变量代表常量类型为0A,对应的表为CONSTANT_Methodref_info,表示方法引用,紧接着一个u2类型的变量000C,它表示声明该方法的类描述符为常量池汇中的第12个常量,第二个u2类型的变量001D表示指向该方法名称及类型的描述符为常量池中的第29个常量。
按照同样的方式,下图给出了前面14个常量的字节码,其中前面12个都是指向了常量池中的其它常量,第13、14个常量是两个类型为1(即UTF-8编码的常量),对应的英文字符分别为name、Ljava/lang/String.
剩下其他常量的划分方式是类似的,事实上,jdk已经为我们提供了专门用于分析class文件的工具javap,利用javap -v JavaTest.class得到常量池中的52个常量如下,可以看到,前面14个常量的划分与我们之前分析的完全一致。
bogon:Downloads shiyangsheng$ javap -v JavaTest.class
Classfile /Users/shiyangsheng/Downloads/JavaTest.class
Last modified 2018-3-17; size 842 bytes
MD5 checksum fbb2370c6b7413a0636806a0e492224a
Compiled from "JavaTest.java"
public class com.youzan.shys.advice.JavaTest
minor version: 0
major version: 49
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #12.#29 // java/lang/Object."<init>":()V
#2 = Fieldref #30.#31 // java/lang/System.out:Ljava/io/PrintStream;
#3 = Class #32 // java/lang/StringBuilder
#4 = Methodref #3.#29 // java/lang/StringBuilder."<init>":()V
#5 = String #33 // Hello
#6 = Methodref #3.#34 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#7 = Fieldref #11.#35 // com/youzan/shys/advice/JavaTest.name:Ljava/lang/String;
#8 = Methodref #3.#36 // java/lang/StringBuilder.toString:()Ljava/lang/String;
#9 = Methodref #37.#38 // java/io/PrintStream.println:(Ljava/lang/String;)V
#10 = String #39 // JVM
#11 = Class #40 // com/youzan/shys/advice/JavaTest
#12 = Class #41 // java/lang/Object
#13 = Utf8 name
#14 = Utf8 Ljava/lang/String;
#15 = Utf8 <init>
#16 = Utf8 ()V
#17 = Utf8 Code
#18 = Utf8 LineNumberTable
#19 = Utf8 LocalVariableTable
#20 = Utf8 this
#21 = Utf8 Lcom/youzan/shys/advice/JavaTest;
#22 = Utf8 main
#23 = Utf8 ([Ljava/lang/String;)V
#24 = Utf8 args
#25 = Utf8 [Ljava/lang/String;
#26 = Utf8 <clinit>
#27 = Utf8 SourceFile
#28 = Utf8 JavaTest.java
#29 = NameAndType #15:#16 // "<init>":()V
#30 = Class #42 // java/lang/System
#31 = NameAndType #43:#44 // out:Ljava/io/PrintStream;
#32 = Utf8 java/lang/StringBuilder
#33 = Utf8 Hello
#34 = NameAndType #45:#46 // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#35 = NameAndType #13:#14 // name:Ljava/lang/String;
#36 = NameAndType #47:#48 // toString:()Ljava/lang/String;
#37 = Class #49 // java/io/PrintStream
#38 = NameAndType #50:#51 // println:(Ljava/lang/String;)V
#39 = Utf8 JVM
#40 = Utf8 com/youzan/shys/advice/JavaTest
#41 = Utf8 java/lang/Object
#42 = Utf8 java/lang/System
#43 = Utf8 out
#44 = Utf8 Ljava/io/PrintStream;
#45 = Utf8 append
#46 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuilder;
#47 = Utf8 toString
#48 = Utf8 ()Ljava/lang/String;
#49 = Utf8 java/io/PrintStream
#50 = Utf8 println
#51 = Utf8 (Ljava/lang/String;)V
由此可见,常量池在class文件中占据了绝大部分内容(中间用红框框出来的就是常量池内容):
3.访问标志
紧接着常量池之后的两个字节表示访问标志,主要是用来标记类或者接口层次的一些属性。目标之定义了16个标志位中的8位,没有使用到的一律为0。 具体标志位如下表:
标志名称 | 标志值 | 描述 |
---|---|---|
ACC_PUBLIC | 0x0001 | 是否为public类型 |
ACC_FINAL | 0x0010 | 是否为final类型 |
ACC_SUPER | 0x0020 | 是否允许使用invokespcial字节码指令的新语义,jdk1.0.2之后编译出来的类,此标志都为真 |
ACC_INTERFACE | 0x0200 | 是否为接口 |
ACC_ABSTRACT | 0x0400 | 是否为abstract类型(对接口和抽象类来说,此标志都为真) |
ACC_SYNTHETIC | 0x1000 | 标识这个类并非由用户代码产生 |
ACC_ANNOTATION | 0x2000 | 是否是注解 |
ACC_ENUM | 0x4000 | 是否是枚举 |
显然,对JavaTest类而言,只有ACC_PUBLIC、ACC_SUPER两个标志应该为真,因此access_flags=0x0021.
4.类索引、父类索引和接口索引集合
在访问标志之后,有3个用来确定一个类的继承关系的数据,按先后顺序分别是:
- 类索引:用于确定类的全限定名
- 父类索引:用于确定父类的全限定名
- 接口索引:用于描述类实现了哪些接口
它们在class文件中的位置如下:
可见,类索引为11,父类索引为12,接口索引集合大小为0,根据前面得到的常量池,可以知道第11、12个常量为:
...
#11 = Class #40 // com/youzan/shys/advice/JavaTest
#12 = Class #41 // java/lang/Object
...
#40 = Utf8 com/youzan/shys/advice/JavaTest
#41 = Utf8 java/lang/Object
...
5.字段表集合
在接口索引之后是字段表集合,字段表用来描述接口或者类中声明的变量。它包括类级变量和实例级变量,但是不包括局部变量以及从父类和接口中继承而来的字段。字段表的格式如下:
类型 | 名称 | 数量 | 含义 |
---|---|---|---|
u2 | access_flags | 1 | 字段修饰符 |
u2 | name_index | 1 | 字段和方法简单名称在常量池中的引用 |
u2 | descriptor_index | 1 | 字段和方法描述符在常量池中的引用 |
u2 | attributes_count | 1 | 描述字段额外信息属性的个数 |
attribute_info | attributes | attributes_count | 具体描述字段的额外信息属性 |
5.1字段修饰符
字段修饰符与类中的访问标志很类似,用来描述字段的一些属性:
标志名称 | 标志值 | 描述 |
---|---|---|
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 | 是否enum类型 |
5.2全限定名
把类全路径中的.替换为/,同时在最后加入一个;即可。
5.3简单名称
简单名称指的是没有类型和修饰符的字段或者方法名称。
5.4描述符
描述符用来描述字段的数据类型、方法的参数列表和返回值。其中基本类型字段的描述符用一个大写字母来表示,而对象类型则用字符L加上对象类型的全限定名来表示。具体如下表:
描述符 | 含义 |
---|---|
B | 基本类型byte |
C | 基本类型char |
D | 基本类型double |
F | 基本类型float |
I | 基本类型int |
J | 基本类型long |
S | 基本类型short |
Z | 基本类型boolean |
V | 基本类型void |
L | 对象类型,如Ljava/lang/Object |
对于数组类型,每一个维度都是用一个前置的“[”来描述,如java.lang.String[][]类型的二位数组将被记录为[[java/lang/String;
描述方法时,将按照先参数列表、后返回值的顺序来描述。其中参数列表严格按照参数的顺序放在一组小括号()之内。例如方法java.lang.String.toString()的描述符为()Ljava/lang/String;
了解了这几个概念之后,我们回到JavaTest的class文件:
- fields_count=0x0001表明这个类只有一个字段表数据;
- access_flags=0x000A表明ACC_PRIVATE与ACC_STATIC标志位为1真,其它标志位为0;
- name_index=0x000D表明字段简单名称为常量池中的第13个常量,也就是name;
- descriptor=0x000E表明字段描述符为常量池中的第14个常量,也就是Ljava/lang/String;
- attributes_count=0x0000表明字段额外属性个数为0;
由此可以反过来得到该类的一个属性为 private static String name;
6.方法表集合
对方法描述的方式与对字段描述的方式基本一致,方法表的结构也与字段表的结构完全一致,不同之处在于方法的访问标志与字段的访问标志有所区别。例如volatile与transient不能修饰方法,但是方法却有synchronized、native、strictfp和abstract等属性。其具体访问标志如下:
标志名称 | 标志值 | 描述 |
---|---|---|
ACC_PUBLIC | 0x0001 | 是否为public类型 |
ACC_PRIVATE | 0x0002 | 是否为final类型 |
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 | 是否abstract |
ACC_STRICTFP | 0x0800 | 是否strictfp |
ACC_SYNTHETIC | 0x1000 | 是否由编译器自动产生 |
让我们继续回到class文件:
6.1 第一个方法
- methods_count=0x0003表明该类有3个方法;
- 第一个方法的access_flags=0x0001表明只有ACC_PUBLIC标志位为真;
- name_index=0x000F表明方法简单名称为常量池中的第15个常量,也就是<init>;
- descriptor=0x0010表明方法修饰符为常量池中的第16个常量,也就是()V;
- attributes_count=0x0001表明第一个方法有一个额外属性,且索引值就是其后的0x0011,也就是常量池中的第17个常量Code。
- Code属性是该方法的具体字节码描述。
由此得到第一个方法为public void init(),这个方法是编译器自动添加的实例构造器方法。
Code属性也是class文件中最重要的一个属性,如果把一个Java程序中的信息分为代码(方法体里的代码)和元数据(描述类、字段、方法的其他信息)两部分,那么Code属性描述的就是代码的信息,其它所有数据都用于描述元数据。由于Code数据极其重要也相对复杂,我将在另外一篇文章中单独介绍,这里直接给出init()方法的Code属性在class文件中的表示(简单来说,前面4个字节0000 002F表示属性值的长度,也就是47个字节,也就是说后续47个字节都是Code属性的内容):
6.2 第二个方法
第一个方法的Code属性后面紧跟着的是第二个方法的描述,同样的分析方法,不难得出第二个方法为public static void main(String[]);其Code属性值的长度为0000 004A,也就是74个字节。
6.3 第三个方法
同样,很容易得到第三个方法为static clinit void();这个方法是编译器自动添加的类构造器方法,其Code属性值的长度为0000 001E,也就是30个字节。
实际上,这个分析与javap得到的结果也是一致的。
{
public com.youzan.shys.advice.JavaTest();
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 9: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/youzan/shys/advice/JavaTest;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: new #3 // class java/lang/StringBuilder
6: dup
7: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V
10: ldc #5 // String Hello
12: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
15: getstatic #7 // Field name:Ljava/lang/String;
18: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
21: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
24: invokevirtual #9 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
27: return
LineNumberTable:
line 14: 0
line 15: 27
LocalVariableTable:
Start Length Slot Name Signature
0 28 0 args [Ljava/lang/String;
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: ldc #10 // String JVM
2: putstatic #7 // Field name:Ljava/lang/String;
5: return
LineNumberTable:
line 11: 0
}
7.属性表集合
属性表集合用于描述某些场景的专有信息,它一共有21个属性,属性表结构如下:
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | info | attribute_length |
由于涉及属性太多,这里也不再展开,只是简单说明下之类的属性代表什么意思。从class文件可以看出,该类的属性表集合只有一个元素,001B表示常量池中的第27个常量,也就是SourceFile,001C表示常量池中的第28个常量,也就是JavaTest.java,也就是说,SourceFile属性记录了生成这个class文件的源码文件的名称。