本文所有内容的JDK版本为 OpenJDK 11
JDK11 Class File官方说明。
Java解析字节码文件源码参考,以下为部分字节码解析源码展示。
java
public ClassFile(DataInputStream in) {
try {
//magic: 0xCAFEBABE
this.magic = ClassReader.readInt(in);
System.out.println("magic: >>> " + this.magic);
this.minorVersion = ClassReader.readUnsignedShort(in);
System.out.println("minorVersion: >>> " + this.minorVersion);
this.majorVersion = ClassReader.readUnsignedShort(in);
System.out.println("majorVersion: >>> " + this.majorVersion);
this.constantPoolCount = ClassReader.readUnsignedShort(in);
System.out.println("constantPoolCount: >>> " + this.constantPoolCount);
this.constantPool = new ConstantPoolInfo[this.constantPoolCount];
for (int i = 1; i < this.constantPoolCount; i++) {
ConstantPoolInfo cp = ConstantPoolFactory.getCp(in);
this.constantPool[i] = cp;
System.out.println(
"constantPool[" + i + "]: " + cp.getClass().getSimpleName() + " >>> " + JSONObject.toJSONString(cp));
}
this.accessFlags = ClassReader.readUnsignedShort(in);
System.out.println("\naccessFlags: >>> " + this.accessFlags);
//this class name index in constant pool
this.thisClass = ClassReader.readUnsignedShort(in);
System.out.println("thisClass: >>> " + this.thisClass);
//super class name index in constant pool
this.superClass = ClassReader.readUnsignedShort(in);
System.out.println("superClass: >>> " + this.superClass);
//interface count
this.interfaceCount = ClassReader.readUnsignedShort(in);
System.out.println("interfaceCount: >>> " + this.interfaceCount);
this.interfaces = new int[this.interfaceCount];
for (int i = 0; i < this.interfaceCount; i++) {
int f = ClassReader.readUnsignedShort(in);
this.interfaces[i] = f;
System.out.println("interfaces[" + i + "] >>> " + f);
}
//field count
this.fieldCount = ClassReader.readUnsignedShort(in);
System.out.println("fieldCount: >>> " + this.interfaceCount);
this.fields = new Field[this.fieldCount];
for (int i = 0; i < this.fieldCount; i++) {
Field field = new Field(in);
this.fields[i] = field;
System.out.println("fields[" + i + "] >>> " + JSONObject.toJSONString(field));
}
} catch (IOException e) {
e.printStackTrace();
}
}
Java 虚拟机不与包括 Java 语言在内的任何程序语言绑定,它只与" Class 文件"这种特定的二进制文件格式所关联, Class 文件中包含了 Java 虚拟机指令集
、符号表
以及若干其他辅助信息
。基于安全方面的考虑,《 Java 虚拟机规范》中要求在 Class 文件必须应用许多强制性的语法和结构化约束。作为一个通用的 、与机器无关的 执行平台,任何其他语言的实现者都可以将 Java 虚拟机作为他们语言的运行基础,以 Class 文件作为他们产品的交付媒介。
根据《Java虚拟机规范》的规定,Class文件格式采用一种类似于C语言结构体的伪结构来存储数据,这种伪结构只有两种数据类型:**无符号数**
和**表**
。
- 无符号数:以u1、u2、u4、u8来分别代表一个、两个、四个、八个字节的无符号数。无符号数可以用来描述数字、索引引用、数量值或者按照UTF8编码构成的字符串值。
- 表:由多个或者其他表作为数据项构成的复合数据类型。有点类似于Java中所说的对象。
java
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];
}
Test.java
是我们一个测试文件,生成的字节码文件见紧邻的代码块内容。
java
public class Test {
private Integer variable;
public static void main(String[] args) throws Exception {
System.out.println("Hello World.");
}
}
bash
ca fe ba be 00 00 00 37 00 27 0a 00 06 00 18 09 |.......7.'......|
00 19 00 1a 08 00 1b 0a 00 1c 00 1d 07 00 1e 07 |................|
00 1f 01 00 08 76 61 72 69 61 62 6c 65 01 00 13 |.....variable...|
4c 6a 61 76 61 2f 6c 61 6e 67 2f 49 6e 74 65 67 |Ljava/lang/Integ|
65 72 3b 01 00 06 3c 69 6e 69 74 3e 01 00 03 28 |er;...<init>...(|
29 56 01 00 04 43 6f 64 65 01 00 0f 4c 69 6e 65 |)V...Code...Line|
4e 75 6d 62 65 72 54 61 62 6c 65 01 00 12 4c 6f |NumberTable...Lo|
63 61 6c 56 61 72 69 61 62 6c 65 54 61 62 6c 65 |calVariableTable|
01 00 04 74 68 69 73 01 00 0e 4c 74 6f 64 6f 50 |...this...LtodoP|
6b 67 2f 54 65 73 74 3b 01 00 04 6d 61 69 6e 01 |kg/Test;...main.|
00 16 28 5b 4c 6a 61 76 61 2f 6c 61 6e 67 2f 53 |..([Ljava/lang/S|
74 72 69 6e 67 3b 29 56 01 00 04 61 72 67 73 01 |tring;)V...args.|
00 13 5b 4c 6a 61 76 61 2f 6c 61 6e 67 2f 53 74 |..[Ljava/lang/St|
72 69 6e 67 3b 01 00 0a 45 78 63 65 70 74 69 6f |ring;...Exceptio|
6e 73 07 00 20 01 00 0a 53 6f 75 72 63 65 46 69 |ns.. ...SourceFi|
6c 65 01 00 09 54 65 73 74 2e 6a 61 76 61 0c 00 |le...Test.java..|
09 00 0a 07 00 21 0c 00 22 00 23 01 00 0c 48 65 |.....!..".#...He|
6c 6c 6f 20 57 6f 72 6c 64 2e 07 00 24 0c 00 25 |llo World...$..%|
00 26 01 00 0c 74 6f 64 6f 50 6b 67 2f 54 65 73 |.&...todoPkg/Tes|
74 01 00 10 6a 61 76 61 2f 6c 61 6e 67 2f 4f 62 |t...java/lang/Ob|
6a 65 63 74 01 00 13 6a 61 76 61 2f 6c 61 6e 67 |ject...java/lang|
2f 45 78 63 65 70 74 69 6f 6e 01 00 10 6a 61 76 |/Exception...jav|
61 2f 6c 61 6e 67 2f 53 79 73 74 65 6d 01 00 03 |a/lang/System...|
6f 75 74 01 00 15 4c 6a 61 76 61 2f 69 6f 2f 50 |out...Ljava/io/P|
72 69 6e 74 53 74 72 65 61 6d 3b 01 00 13 6a 61 |rintStream;...ja|
76 61 2f 69 6f 2f 50 72 69 6e 74 53 74 72 65 61 |va/io/PrintStrea|
6d 01 00 07 70 72 69 6e 74 6c 6e 01 00 15 28 4c |m...println...(L|
6a 61 76 61 2f 6c 61 6e 67 2f 53 74 72 69 6e 67 |java/lang/String|
3b 29 56 00 21 00 05 00 06 00 00 00 01 00 02 00 |;)V.!...........|
07 00 08 00 00 00 02 00 01 00 09 00 0a 00 01 00 |................|
0b 00 00 00 2f 00 01 00 01 00 00 00 05 2a b7 00 |..../........*..|
01 b1 00 00 00 02 00 0c 00 00 00 06 00 01 00 00 |................|
00 08 00 0d 00 00 00 0c 00 01 00 00 00 05 00 0e |................|
00 0f 00 00 00 09 00 10 00 11 00 02 00 0b 00 00 |................|
00 37 00 02 00 01 00 00 00 09 b2 00 02 12 03 b6 |.7..............|
00 04 b1 00 00 00 02 00 0c 00 00 00 0a 00 02 00 |................|
00 00 0c 00 08 00 0d 00 0d 00 00 00 0c 00 01 00 |................|
00 00 09 00 12 00 13 00 00 00 14 00 00 00 04 00 |................|
01 00 15 00 01 00 16 00 00 00 02 00 17 |.............|
magic
每个Class文件的头4个字节被称为魔数,即0xCAFEBABE,唯一作用就是确定这个文件是否为一个能被虚拟机接收的Class文件。类加载过程中的验证
阶段就会包含此部分的验证。由于大小端差异,可能展示略有不同。
minor_version
魔数后面两个字节0000
是次版本号。
major_version
次版本号后面两个字节0x0037
是主版本号,十进制为55,即Java11对应的版本号。版本号的意义是:高版本的JDK能向下兼容以前版本的Class文件,但不能运行以后版本的Class文件 。因为《Java虚拟机规范》在Class文件校验部分明确要求了即使文件格式并未发生任何变化,虚拟机也必须拒绝执行超过其版本号的Class文件。
constant_pool_count
接下来的两个字节代表常量池数量0x0027
,十进制为39。与我们的习惯不同,计数是从1开始的而不是0,所以代表我们这个Class文件中有38个常量值,索引范围是1~38。之所以不从0开始,是因为0有特殊考虑,即不引用任何常量池时。
constant_pool
常量池。主要包含两大类常量:符号引用和字面量(又称直接引用)。
- 符号引用:存放常量池索引
- 字面量:常量。
常量池第一个字节为标志位
,0a
即十进制10
,查表速得为CONSTANT_Methodref
。
CONSTANT_Methodref
的结构为:
java
CONSTANT_Methodref_info {
u1 tag;
u2 class_index;
u2 name_and_type_index;
}
往后读两个字节00 06
即为class_index,该值为常量池的一个索引,表示指向常量池中#6元素。通过jclasslib工具可以知道这是一个CONSTANT_Class
类型的常量池索引,最终指向的class name是一个java/lang/Object
的字符串。
再往后读两个字节00 18
即为name_and_type_index,十进制24,同样是常量池中的一个CONSTANT_NameAndType
类型的索引,指向常量池中#24元素。通过jclasslib工具快速查看一下表示一个名为返回值为void的方法,即Java.lang.Object的初始化方法。
至此第一个常量池变量就解析完了,后面37个依次类推可依次解析完成。
access_flags
在常量池结束之后,紧接着的2个字节代表访问标志(access_flags),这个标志用于识别一些类或者接口层次的访问信息,包括:这个Class是类还是接口;是否定义为public类型;是否定义为abstract 类型;如果是类的话,是否被声明为final等等。
0x0021
即为access_flag,即ACC_PUBLIC
和 ACC_SUPER
this_class
this_class用于确定这个类的全限定名。0x0005
表示指向常量池#5号索引。
super_class
super_class用于确定这个类的父类的全限定名,由于Java语言不允许多 重继承,所以父类索引只有一个,除了java.lang.Object之外,所有的Java类都有父类,因此除了 java.lang.Object外,所有Java类的父类索引都不为0。
0x0006
表示指向常量池#6号索引。
interfaces_count
接口计数器(int erfaces _count ),表示索引表 的容量。0x0000
表示指向接口数量为0。如果该类没有实现任何接口,则该计数器值为0,后面接口的索引表不再占用任何字节。
interfaces
接口索引表,每一个item指向常量池索引。由于此测试类没有接口,故而字节码文件缺失该部分内容。
fields_count
fields
methods_count
methods
attributes_count
attributes
字节码文件解析过程对后续**类加载**
以及**字节码增强技术**
都有很大裨益。