正文
我们知道Java最有名的宣传口号就是:“一次编写,到处运行(Write Once,Run Anywhere)”,而其平台无关性则是依赖于JVM, 所有的java文件都被编译成字节码(class)文件,而虚拟机只需要认识字节码文件就可以了。想要弄懂虚拟机以及类加载机制,这部分内容是不可不知的。
Class文件是一组以8字节为基础单位的二进制流,所有数据无间隔的排列在Class文件之中,多字节数据以大端(big-endian order)的方式存储。Class文件以一种接近于C中结构体的伪代码形式存储数据结构,并且只包含无符号数和表两种数据结构:
- 无符号数:u1、u2、u4、u8分别表1、2、4、8字节的无符号数
- 表:由多个无符号数或者其他表组成的复合数据类型, Class文件本身也是一张表。
Class表结构:
ClassFile { u4 magic; u2 minor_version; u2 major_version; u2 constant_pool_count; cp_info constant_pool[constant_pool_count-1]; u2 access_flags; u2 this_class; u2 super_class; u2 interfaces_count; u2 interfaces[interfaces_count]; u2 fields_count; field_info fields[fields_count]; u2 methods_count; method_info methods[methods_count]; u2 attributes_count; attribute_info attributes[attributes_count]; }
参照上面的数据结构,Class文件由10个部分组成:
1 . 魔数
2 . Class文件主次版本号
3 . 常量池
4 . 访问标记
5 . 当前类名
6 . 父类名
7 . 继承的接口名
8 . 包含的所有字段的数量+字段
9 . 包含的所有方法的数量+方法
10 . 包含的所有属性的数量+属性下面我们依次对每个部分进行分析:
1. 魔数
魔数(Magic number)用来确定文件类型,这里就是检测文件是否是能够被虚拟机接受的Class文件。很多文件都使用魔数来确定文件类型,而不是扩展名(因为扩展名可以任意修改)。可以参看我的深入理解程序构造(一)。Class文件的魔数是“0xcafebabe”,咖啡宝贝?Java本身也是一种爪哇咖啡,真是挺有缘的。
这里我也写个小的测试程序,来看看它的二进制码流:
package com.shuqing28; public class TestClass { private int m; public int inc() { return m+1; } }
我们使用javac编译成.class文件,Windows下可以使用WinHex打开,Linux下则可以使用hexdump打开二进制,命令如下:
1$ hexdump -C TestClass.class 200000000 ca fe ba be 00 00 00 34 00 16 0a 00 04 00 12 09 |.......4........| 300000010 00 03 00 13 07 00 14 07 00 15 01 00 01 6d 01 00 |.............m..| 400000020 01 49 01 00 06 3c 69 6e 69 74 3e 01 00 03 28 29 |.I...<init>...()| 500000030 56 01 00 04 43 6f 64 65 01 00 0f 4c 69 6e 65 4e |V...Code...LineN| 600000040 75 6d 62 65 72 54 61 62 6c 65 01 00 12 4c 6f 63 |umberTable...Loc| 700000050 61 6c 56 61 72 69 61 62 6c 65 54 61 62 6c 65 01 |alVariableTable.| 800000060 00 04 74 68 69 73 01 00 19 4c 63 6f 6d 2f 73 68 |..this...Lcom/sh| 900000070 75 71 69 6e 67 32 38 2f 54 65 73 74 43 6c 61 73 |uqing28/TestClas| 1000000080 73 3b 01 00 03 69 6e 63 01 00 03 28 29 49 01 00 |s;...inc...()I..| 1100000090 0a 53 6f 75 72 63 65 46 69 6c 65 01 00 0e 54 65 |.SourceFile...Te| 12000000a0 73 74 43 6c 61 73 73 2e 6a 61 76 61 0c 00 07 00 |stClass.java....| 13000000b0 08 0c 00 05 00 06 01 00 17 63 6f 6d 2f 73 68 75 |.........com/shu| 14000000c0 71 69 6e 67 32 38 2f 54 65 73 74 43 6c 61 73 73 |qing28/TestClass| 15000000d0 01 00 10 6a 61 76 61 2f 6c 61 6e 67 2f 4f 62 6a |...java/lang/Obj| 16000000e0 65 63 74 00 21 00 03 00 04 00 00 00 01 00 02 00 |ect.!...........| 17000000f0 05 00 06 00 00 00 02 00 01 00 07 00 08 00 01 00 |................| 1800000100 09 00 00 00 2f 00 01 00 01 00 00 00 05 2a b7 00 |..../........*..| 1900000110 01 b1 00 00 00 02 00 0a 00 00 00 06 00 01 00 00 |................| 2000000120 00 03 00 0b 00 00 00 0c 00 01 00 00 00 05 00 0c |................| 2100000130 00 0d 00 00 00 01 00 0e 00 0f 00 01 00 09 00 00 |................| 2200000140 00 31 00 02 00 01 00 00 00 07 2a b4 00 02 04 60 |.1........*....`| 2300000150 ac 00 00 00 02 00 0a 00 00 00 06 00 01 00 00 00 |................| 2400000160 06 00 0b 00 00 00 0c 00 01 00 00 00 07 00 0c 00 |................| 2500000170 0d 00 00 00 01 00 10 00 00 00 02 00 11 |.............| 260000017d
看第一行的前4个字节的十六进制就是0xcafebabe
,所以文件类型确实为.class文件。
2. 版本号
第5和第6字节是次版本号(Minor Version),第7和第8字节是主版本号(Major Version)。这里看出我们的主版本号是0x0034
,也就是52,下面是JDK与其对应的版本号关系:
JDK 1.8 = 52JDK 1.7 = 51JDK 1.6 =50JDK 1.5 = 49JDK 1.4 = 48JDK 1.3 = 47JDK 1.2 = 46JDK 1.1 = 45
可以看出我使用的是Java8编译的代码。
3. 常量池
我们继续看二进制文件的第一行:
00000000 ca fe ba be 00 00 00 34 00 16 0a 00 04 00 12 09 |.......4........|
在主版本号0x0034
后的是0x0016
,这个值表示常量池的容量。常量池可以理解为Class文件的资源仓库,常量池中包含的数据结构是这样的:
cp_info { u1 tag; u1 info[]; }
常量池中的每个项目都包含一个tag开头的cp_info对象,代表着常量类型,info则根据不同的类型各有各的结构。目前一共有14种常量类型:Constant TypeValue
CONSTANT_Class | 7 |
CONSTANT_Fieldref | 9 |
CONSTANT_Methodref | 10 |
CONSTANT_InterfaceMethodref | 11 |
CONSTANT_String | 8 |
CONSTANT_Integer | 3 |
CONSTANT_Float | 4 |
CONSTANT_Long | 5 |
CONSTANT_Double | 6 |
CONSTANT_NameAndType | 12 |
CONSTANT_Utf8 | 1 |
CONSTANT_MethodHandle | 15 |
CONSTANT_MethodType | 16 |
CONSTANT_InvokeDynamic | 18 |
上面的0x0016
翻译成十进制是22,那么常量池中有21个常量,因为常量池中索引是从1开始计数的,所以常量索引范围是1~21。
00000000 ca fe ba be 00 00 00 34 00 16 0a 00 04 00 12 09 |.......4........|
接下看常量池的第一个常量, tag是0x0a
, 查上面的常量表就是CONSTANT_Methodref,表示接下来定义的是一个方法,知道类型后,我们可以查一下CONSTANT_Methodref的结构,这里可以参考Oracle的官方文档The class File Format,
CONSTANT_Methodref_info { u1 tag; u2 class_index; u2 name_and_type_index; }
由于.class文件是无间隔的二进制文件,所以接着读:
- tag:
0x0a
,上面已经说了指代CONSTANT_Methodref常量 - class_index:指向常量池中CONSTANT_Class_info类型的常量,代表上面方法的名称
- name_and_type_index : 指向常量池中CONSTANT_NameAndType_info常量,是对方法的描述
因为class_index占两个字节,所以紧接着读到了0x0004
,也就是4,指向常量池中的第4个常量,name_and_type_index是0x0012
,指向第18个常量。后面会分析到第4和第18个常量。继续往下读,到第一行的最末了,是个0x09,指示的是CONSTANT_Fieldref,表示接下来是对一个域的定义, 查官方文档,格式为:
CONSTANT_Fieldref_info { u1 tag; u2 class_index; u2 name_and_type_index; }
结构和CONSTANT_Methodref_info一样,这时候读到了第二行:
00000010 00 03 00 13 07 00 14 07 00 15 01 00 01 6d 01 00 |.............m..|
class_index为0x0003
,指向第3个常量,name_and_type_index为0x0013
指向第13个常量。这时候继续往后读,终于读到第3个常量了。此时tag是0x07
,查表可得为CONSTANT_Class类型,此类型的常量代表一个类或者接口的符号引用,CONSTANT_Class的结构:
CONSTANT_Class_info { u1 tag; u2 name_index; }
tag是7, name_index是0x0014
,十进制就是20,指向第20个常量,这样我们已经读了很多个字节了。但是这样解析下去很累,还好java自带的javap工具可以帮我们分析出字节码的内容。
执行下面语句:
javap -verbose TestClass.class
我们可以得到:
Last modified Nov 14, 2017; size 381 bytes MD5 checksum 102d643185c4823ef103931ff3e34462 Compiled from "TestClass.java" public class com.shuqing28.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 // com/shuqing28/TestClass.m:I #3 = Class #20 // com/shuqing28/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 Lcom/shuqing28/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 com/shuqing28/TestClass #21 = Utf8 java/lang/Object { public com.shuqing28.TestClass(); descriptor: ()V flags: ACC_PUBLIC ...//省略
这里我们可以看到Constant pool字段,后面依次列出了21个常量,可以看出第一个是Methodref型的常量,class_index指向第4个常量,第4个常量呢是CONSTANT_Class类型,name_index又指向第20个常量,可知是一个CONSTANT_Utf8类型的常量,前面没说到CONSTANT_Utf8,下面是它的结构:
CONSTANT_Utf8_info { u1 tag; u2 length; u1 bytes[length]; }
第一位tag为1,length指示字符数组的长度,bytes[length]是使用UTF-8缩略编码表示的字符串,这里解析出来是com/shuqing28/TestClass,即类的全限定名。继续回到第一个Methodref常量,它的name_and_type_index值是18, 继续找到第18个常量,是CONSTANT_NameAndType_info类型,代表的是一个方法的信息:
CONSTANT_NameAndType_info { u1 tag; u2 name_index; u2 descriptor_index; }
name_index指向了常量7, 即#7 = Utf8 <init>
, 是一个CONSTANT_Utf8_info类型,值为<init>,这个是方法的名称,descriptor_index指向了常量8,即#8 = Utf8 ()V
,是方法的描述,下文会说这个表达式是什么意思。
这样我们就可以一一把这21个常量分析清楚了。其实Class文件就是在一开始列出了一堆常量,后面的各种描述都是各种index,指向前面常量池中的各种常量,来描述整个类的定义。就像有一本字典,我们使用字典中的字来造我们的句子,只不过Class文件中造句是有严格格式规定的,下面的内容基本都按照固定格式,无间隔的描述一个类的内容。
4. 访问标志
常量池结束后,紧接着的两个字节代表访问标志(access_flags),这个标志用于识别一些类或者接口的访问信息,包括这个Class是类还是接口,是否是public的,是否是abstract,是否是final的。
访问标记含义如下表:标志名称标志值含义
ACC_PUBLIC | 0x0001 | Declared public; may be accessed from outside its package. |
ACC_FINAL | 0x0010 | Declared final; no subclasses allowed. |
ACC_SUPER | 0x0020 | Treat superclass methods specially when invoked by the invokespecial instruction. |
ACC_INTERFACE | 0x0200 | Is an interface, not a class. |
ACC_ABSTRACT | 0x0400 | Declared abstract; must not be instantiated. |
ACC_SYNTHETIC | 0x1000 | Declared synthetic; not present in the source code. |
ACC_ANNOTATION | 0x2000 | Declared as an annotation type. |
ACC_ENUM | 0x4000 | Declared as an enum type. |
access_flags中一共有16个标志位可用,当前只定义了8个,别的都为0,TestClass是public类型的,且使用JDK1.2以后的编译器进行编译的(使用JDK1.2以后的编译器编译,这个值都为真),别的标志都为假。所以access_flags的值应为:0x0001|0x0020 = 0x0021
。我们找到刚才常量池最后一行的地方:
000000e0 65 63 74 00 21 00 03 00 04 00 00 00 01 00 02 00 |ect.!...........|
65 63 74
分别对应ect,紧接着是0x0021
,与我们的分析结果一致。