[JVM]理解Class文件(1):手动解析常量池
-
深入理解Java虚拟机
引言
class文件是Java虚拟机提供的语言无关性的基石,如果要深入地了解虚拟机,那么必须学习class文件是如何解析的。
Java虚拟机提供的语言无关性而常量池是class文件结构中第一个出现的表类型数据项目,也是Class文件中最烦琐的数据,理解了常量池的解析过程,Class文件其它字段的分析也就迎刃而解了。
Class文件结构
ClassFile结构
Class文件格式采用一种类似于C语言结构体的伪结构来存储数据,包含两种数据类型:
- 无符号数:类型有u1、u2、u4、u8分别代表1个字节、2个字节、4个字节、8个字节
- 表: 表是多个无符号数或者其他表组合而成的复合数据,如:cp_info constant_pool[constant_pool_count-1]就是一个表结构的数据。
每一个Class文件对应于一个如下所示的ClassFile结构体:
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];
}
示例java代码
将下面这段代码编译生成class,用作后续分析。
public class TestClass {
private static final String TEST = "test string";
}
示例class文件
将生成的class文件用UltraEdit打开,可以清楚地看到Java编译后生成的字节码,我们要解析的内容也就是这些字节码。
00000000h: CA FE BA BE 00 00 00 33 00 12 0A 00 03 00 0E 07 ;
00000010h: 00 0F 07 00 10 01 00 04 54 45 53 54 01 00 12 4C ;
00000020h: 6A 61 76 61 2F 6C 61 6E 67 2F 53 74 72 69 6E 67 ;
00000030h: 3B 01 00 0D 43 6F 6E 73 74 61 6E 74 56 61 6C 75 ;
00000040h: 65 08 00 11 01 00 06 3C 69 6E 69 74 3E 01 00 03 ;
00000050h: 28 29 56 01 00 04 43 6F 64 65 01 00 0F 4C 69 6E ;
00000060h: 65 4E 75 6D 62 65 72 54 61 62 6C 65 01 00 0A 53 ;
00000070h: 6F 75 72 63 65 46 69 6C 65 01 00 0E 54 65 73 74 ;
00000080h: 43 6C 61 73 73 2E 6A 61 76 61 0C 00 08 00 09 01 ;
00000090h: 00 09 54 65 73 74 43 6C 61 73 73 01 00 10 6A 61 ;
000000a0h: 76 61 2F 6C 61 6E 67 2F 4F 62 6A 65 63 74 01 00 ;
000000b0h: 0B 74 65 73 74 20 73 74 72 69 6E 67 00 21 00 02 ;
000000c0h: 00 03 00 00 00 01 00 1A 00 04 00 05 00 01 00 06 ;
000000d0h: 00 00 00 02 00 07 00 01 00 01 00 08 00 09 00 01 ;
000000e0h: 00 0A 00 00 00 1D 00 01 00 01 00 00 00 05 2A B7 ;
000000f0h: 00 01 B1 00 00 00 01 00 0B 00 00 00 06 00 01 00 ;
00000100h: 00 00 02 00 01 00 0C 00 00 00 02 00 0D ;
魔数和版本号
在解析常量池之前,先来看下Class文件中最开始的两个字段
-
魔数
魔数是用来标识这支文件是能被虚拟机所接受的Class文件。魔数值固定为0xCAFEBABE,不会改变。
由于文件扩展名是可以随意修改的,因此,很多文件都会使用魔数作为文件标识,如gif或者jpeg等在头文件中都存在魔数。
-
版本号
class文件版本由副版本号+主版本号组成(minor_version + major_version),通过版本号可以知道对应编译器的版本。下图列出了每个JDK版本对应的十六进制版本号
-
示例解析
头4个字节为魔数CA FE BA BE,紧接着为版本号00 00 00 33,对照上面的表格,可以知道编译器的版本为JDK 1.7.0
图1.魔数、版本号和常量池计数器
常量池
存放内容
常量池存放的内容如下:
-
字面量
- 文本字符串、声明为final的常量值
-
符号引用
- 类和接口的全限定名
- 字段的名称和描述符
- 方法的名称和描述符
常量池数据结构
要解析常量池的数据,先得看下它的数据结构,这些数据结构都是在 Java虚拟机规范(Java SE 7).pdf里面定义的。
-
ClassFile
在class文件中,常量池入口紧接在主版本后之后, constant_pool_count表示常量的个数,constant_pool是存放的是所有常量信息的表项,存放的个数为constant_pool_count - 1,constant_pool可以将理解为一个数组,其中的每一项代表一个常量。
由“图1.魔数、版本号和常量池计数器”所示constant_pool_count值为18,由于常量池容量计数是从1开始的,0用作表示“不引用任何对象”,因此,常量个数为17个(constant_pool_count - 1)
ClassFile {
... ...
u2 constant_pool_count;
cp_info constant_pool[constant_pool_count-1];
... ...
}
-
cp_info
要想知道constant_pool里面是怎样存放常量信息的,就需要先看cp_info这个结构体的内容
cp_info {
u1 tag;
u1 info[];
}
其中,tag表示常量的类型,info[]代表tag类型所属的表项。
tag值对应的类型如下表:
常量池tag以开头的class文件为例,紧接着常量池计数器后的"0A"是第一个常量的tag,"0A"对应十进制值为10,结合上表,得出第一个常量类型为CONSTANT_Methodref
-
CONSTANT_Methodref_info
CONSTANT_Methodref对应的数据结构为CONSTANT_Methodref_info,也就是说constant_pool[1]中的u1 info[]是CONSTANT_Methodref_info
-
tag表示当前数据类型CONSTANT_Methodref_info,说明这个常量是一个方法
-
class_index表示引用这个方法的对象在常量池数组的中索引,说明constant_pool[class_index]存放的就是调用该方法的对象名称
-
name_and_type_index指的该方法在常量池数组的中索引,即constant_pool[name_and_type_index]存放着该方法的名称
CONSTANT_Methodref_info {
u1 tag;
u2 class_index;
u2 name_and_type_index;
}
根据上面的描述,我们接着解析"0A"后面的字段,由CONSTANT_Methodref_info数据结构可知,"0A"往后再取2个u2类型数据的长度就截止了,其中class_index = 3, name_and_type_index = 14。
假设我们已经将后面的常量池字段全部解析完成了(文末附有整个常量池解析结构),可以得出constant_pool[3] = "java/lang/Object" ,constant_pool[14] = " "<init>":()V "
因此,第一个常量constant_pool[1]为:java/lang/Object."<init>":()V
CONSTANT_Methodref_info-
解析第二个常量
类型为CONSTANT_Class,对应结构体为CONSTANT_Class_info,由此知解析的第二个常量的数据段为07 00 0F
CONSTANT_Class_info {
u1 tag;
u2 name_index;
}
最后得到结果:该常量是一个类的名称,类名存放在constant_pool[15] = "TestClass" , 即constant_pool[2] = TestClass
解析第二个常量-
使用javap命令解析class文件
按照上面的方法,可以手动解析出剩下的15个常量。我们也可以通过javap完成对class文件的解析,javap命令使用方法和输入结果如下:
D:\TestClass>javap -verbose TestClass
Classfile /D:/TestClass/TestClass.class
Last modified 2017-2-7; size 269 bytes
MD5 checksum b2a3f1078d18eba859aaeac73bd7621d
Compiled from "TestClass.java"
public class TestClass
SourceFile: "TestClass.java"
minor version: 0
major version: 51
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #3.#14 // java/lang/Object."<init>":()V
#2 = Class #15 // TestClass
#3 = Class #16 // java/lang/Object
#4 = Utf8 TEST
#5 = Utf8 Ljava/lang/String;
#6 = Utf8 ConstantValue
#7 = String #17 // test string
#8 = Utf8 <init>
#9 = Utf8 ()V
#10 = Utf8 Code
#11 = Utf8 LineNumberTable
#12 = Utf8 SourceFile
#13 = Utf8 TestClass.java
#14 = NameAndType #8:#9 // "<init>":()V
#15 = Utf8 TestClass
#16 = Utf8 java/lang/Object
#17 = Utf8 test string
参考
- 深入理解Java虚拟机:JVM高级特性与最佳实践(第2版)
- Java虚拟机规范(Java SE 7).pdf