以前了解JVM相关知识时,对类文件结构仅仅是一瞥而过,并未仔细去深究,实在不应该。于是最近找了个闲暇,来仔细理解一下。
先不谈各种定义,就现象而言,class文件只是开发者编译java文件产生的产物,且这个产物是供JVM阅读的。
class文件于Java程序员自然随手可拾,当使用文本编辑器打开的时候却是这样的:
cafe babe 0000 0034 003d 0a00 1100 2909
0010 002a 0900 1000 2b09 0010 002c 0900
1000 2d07 002e 0a00 0600 2908 002f 0a00
...
...
第一感觉就是除了"cafe"这4个字母比较眼熟,其他这些hex字符串光凭肉眼又哪里能知道是什么意思。
所以,肯定是打开的方式不对?
严格来说,这样打开并无问题,因为class文件本身就是字节码,本来页面就是如此。之所以不知道这些字节码代表的什么意思,只是因为不了解class文件的格式而已。
而我们能在各种资料书籍中找到各种对class文件格式的定义,比如Constant_Class_info,比如Field_info…诸如此类的东西,就是帮助剖析这些字节码。
虽然,javap命令就可以帮助开发者快速剖析。我们只要使用
javap -verbose [class文件路径]
就能得到详细的结果。
当然得到结果并不代表能够理解结果的意义,所以最终还是得知晓定义才行。
整体结构
就标准而言,整个class文件格式按顺序是存储着下面这个表格里面的内容:
字段表 | 长度 | 含义 |
---|---|---|
magic | 4 byte | 万年不变的"cafebabe" |
minor_version | 2 byte | 当前JDK副版本号 |
major_version | 2 byte | 当前JDK主版本号 |
constant_pool_count | 2 byte | 常量池数量,代表着此类文件中常量的数量 |
constant_pool[constant_pool_count-1] | n byte | 各种常量表,每种常量表的结构与长度有所差别 |
access_flags | 2 byte | 访问标志,即public/final等修饰符 |
this_class | 2 byte | 类索引 |
super_class | 2 byte | 父类索引 |
interfaces_count | 2 byte | 接口数量 |
interfaces[interfaces_count] | 2*n byte | 每个接口表仅2byte,即只存储索引值 |
fields_count | 2 byte | 字段数量 |
fields[fields_count] | n byte | 字段表结构相同,但因属性的缘故,长度有所差别 |
methods_count | 2 byte | 方法数量 |
methods[methods_count] | n byte | 与字段表结构类似 |
attributes_count | 2 byte | 属性数量 |
attributes[attributes_count] | n byte | 各种属性表,每种属性表的结构与长度有所差别 |
这张表在资料里面应该是很常见的。我们可以稍微捋一捋:
- 固定字段8个byte,与类本身并没有太大关系,每个class文件都有
- 常量池,长度不定,存储了各种常量
- 类信息,固定字段3*2byte,类访问标志、类索引、父类索引
- 接口,长度不定,存储了各个接口对应的索引
- 字段,长度不定,存储了各种字段
- 方法,长度不定,存储了各种方法
- 属性,长度不定,存储了各种属性
需要声明的是,这其中出现的索引,最终都会指向常量池的utf8字符串的值。
比如我们在使用javap命令后会看到这样的信息:
#2 = Fieldref #16.#42 // com/xter/design/builder/Point.x:I
#3 = Fieldref #16.#43 // com/xter/design/builder/Point.y:I
毫无疑问,这是常量池中的字段,而#16/#42正是常量池其他值的索引中的两个:
#16 = Class #55 // com/xter/design/builder/Point
#42 = NameAndType #18:#19 // x:I
#43 = NameAndType #20:#19 // y:I
而这三个常量,又指向其他的索引:
#18 = Utf8 x
#19 = Utf8 I
#20 = Utf8 y
#55 = Utf8 com/xter/design/builder/Point
可以看到最终是指向utf8类型的常量值。而javap很体贴地将最终指向的utf8值都注释在后面了。
常量池
就JDK1.8而言,常量有14种,每种常量都有自己的结构,但他们的相同之处是第1个byte代表自己的类型。
Constant_Base:
tag = bytes(1) # u1
data = bytes()
这里使用括号来表示对应的字段的字节数量,每个tag都是1个字节,表示为tag(1),每个tag的值也对应着一种常量类型,看表:
tag | info |
---|---|
7 | Constant_Class_info { tag(1), name_index(2) } |
9 | Constant_Fieldref_info { tag(1), class_index(2), name_and_type_index(2) } |
10 | Constant_Methodref_info { tag(1), class_index(2), name_and_type_index(2) } |
11 | Constant_InterfaceMethodref_info { tag(1), class_index(2), name_and_type_index(2) } |
8 | Constant_String_info { tag(1), string_index(2) } |
3 | Constant_Integer_info { tag(1), bytes(4) } |
4 | Constant_Float_info { tag(1), bytes(4) } |
5 | Constant_Long_info { tag(1), high_bytes(4), low_bytes(4) } |
6 | Constant_Double_info { tag(1), high_bytes(4), low_bytes(4) } |
12 | Constant_NameAndType_info { tag(1), name_index(2), descriptor_index(2) } |
1 | Constant_Utf8_info { tag(1), length(2), bytes(length) } |
15 | Constant_MethodHandle_info { tag(1), reference_kind(2), reference_index(2) } |
16 | Constant_MethodType_info { tag(1), descriptor_index(2) } |
18 | Constant_InvokeDynamic_info { tag(1), bootstrap_method_attr_index(2), name_and_type_index(2) } |
大多常量类型的结构都是包含着索引,也就是各种XX_index,仅有少部分常量类型,比如类似Constant_Integer_info的几个表数值的常量,内容才是bytes,再如Constant_Utf8_info,内容也是bytes,代表着一个字符串,许多索引最终的指向都会来到这个常量类型。
(常量虽然因为类型不同长度不一,但因为大多以2byte表示索引的原因,长度一般是3byte/5byte,也并非毫无规律)
另外需要注意的是,根据《Java虚拟机规范》,Long和Double字面量会占两个表成员项的空间,意味着当常量池中计算这两种字面量时,索引会由n+1变成n+2了,因此会出现“跳索引”的现象。
根据这个表,我们可以肉眼解析一下class文件的内容,还是以最初的class文件举例,其内容开头是这样的:
cafe babe 0000 0034 003d 0a00 1100 2909
0010 002a 0900 1000 2b09 0010 002c 0900
解析出来如下表:
字段表 | 长度 | 原文 | 表义 |
---|---|---|---|
magic | 4 byte | cafebabe | class文件头 |
minor_version | 2 byte | 0000 | 副版本号0 |
major_version | 2 byte | 0034 | 主版本号52,代表JDK1.8 |
constant_pool_count | 2 byte | 003d | 61,常量索引从1开始,即有60个常量 |
tag | 1byte | 0a | 10代表Constant_Methodref_info,当前常量索引为1 |
class_index | 2byte | 0011 | 17,指向索引为17的常量,且其常量类型为Constant_Class_info |
name_and_type_index | 2byte | 0029 | 41,指向索引为41的常量,且其常量类型为Constant_NameAndType_info |
tag | 1byte | 09 | 9代表**Constant_Fieldref_info **,当前常量索引为1 |
class_index | 2byte | 0010 | 16,指向索引为16的常量,且其常量类型为Constant_Class_info |
name_and_type_index | 2byte | 002a | 42,指向索引为42的常量,且其常量类型为Constant_NameAndType_info |
以此类推,直到常量池解析结束,应该有60个常量被解析出来。 |
类信息
当常量池阶段结束后,接下来是3*2byte的固定字段,分别是access_flags、this_class以及super_class,其中access_flags也有自己的一套规范:
名 | 值 | 转二进制 | 义 |
---|---|---|---|
ACC_PUBLIC | 0x0001 | 0000 0000 0000 0001 | 表示此类修饰符为public |
ACC_FINAL | 0x0010 | 0000 0000 0001 0000 | 表示此类修饰符final |
ACC_SUPER | 0x0020 | 0000 0000 0010 0000 | JDK1.2之后均带此标志 |
ACC_INTERFACE | 0x0200 | 0000 0010 0000 0000 | 表示此类为接口 |
ACC_ABSTRACT | 0x0400 | 0000 0100 0000 0000 | 表示此类修饰符为abstract,不能被实例化 |
ACC_SYNTHETIC | 0x1000 | 0001 0000 0000 0000 | 表示此类非Java源代码生成 |
ACC_ANNOTATION | 0x2000 | 0010 0000 0000 0000 | 表示注解类 |
ACC_ENUM | 0x4000 | 0100 0000 0000 0000 | 表示枚举类 |
看到1、2、4,第一感觉就是转成二进制应该会更明了。所以在上面的表里面直接转出来了。
这些类型相互之间可以组合,当然也有互斥,比如ACC_INTERFACE和ACC_ABSTRACT一般都是成对出现,组合起来都是位或的关系。
即:
一个具有ACC_PUBLIC和ACC_FINAL访问标志的类的最终访问标志值应该为:
0x0001 | 0x0010 = 0x0011
一个具有ACC_PUBLIC、ACC_INTERFACE和ACC_ABSTRACT访问标志的类的最终访问标志值应该为:
0x0001 | 0x0200 | 0x0400 = 0x0601
还是以之前的class文件为例,从access_flags开始读取到的6个byte为:
00 21 00 10 00 11
0x0021自然就是指0x0020|0x0001,也就是ACC_PUBLIC和ACC_SUPER,即此类是一个单纯以public为修饰符的类;
this_class与super_class不必多言,就是两个2byte的数值表示索引,这个索引会指向常量池。一个指向索引为0010=16的常量,一个指向0011=17的常量:
#16 = Class #55 // com/xter/design/builder/Point
#17 = Class #56 // java/lang/Object
#55 = Utf8 com/xter/design/builder/Point
#56 = Utf8 java/lang/Object
是的,这个类名就是Point,且直接继承自超类Object。
接口
类信息后,就是接口阶段,先读2byte得到接口数量,再通过数量去取各个接口的存储的索引值,这些索引必定指向一个类型为Constant_Class_info的常量。
假如此阶段读取到的值为:
00 01 00 0D
即接口数量为0001=1个,此接口指向常量池中索引值为000D = 13的Constant_Class_info常量。
字段
接口阶段后为字段信息,同样是先读2byte得到字段数量,而字段与常量一样,是具有自己的结构的,但与常量不同的是,不会分出那么多类型,字段的结构是固定的:
Field_info{
access_flags(2)
name_index(2)
descriptor_index(2)
attributes_count(2)
Attribute_info(attributes_count)
}
这里的access_flags与之前出现的类的访问标志类似,毕竟许多修饰符既可以修饰类本身,也可以修饰字段(变量)。有部分之前的表中没有出现的1、2、4,会在这里出现。看表:
名 | 值 | 转二进制 | 义 |
---|---|---|---|
ACC_PUBLIC | 0x0001 | 0000 0000 0000 0001 | 表示此字段修饰符为public |
ACC_PRIVATE | 0x0002 | 0000 0000 0000 0010 | 表示此字段修饰符为private |
ACC_PROTECTED | 0x0004 | 0000 0000 0000 0100 | 表示此字段修饰符为protected |
ACC_STATIC | 0x0008 | 0000 0000 0000 1000 | 表示此字段修饰符为static |
ACC_FINAL | 0x0010 | 0000 0000 0001 0000 | 表示此字段修饰符为final |
ACC_VOLATILE | 0x0040 | 0000 0000 0100 0000 | 表示此字段修饰符为volatile |
ACC_TRANSIENT | 0x0080 | 0000 0000 1000 0000 | 表示此字段修饰符为transient |
ACC_SYNTHETIC | 0x1000 | 0001 0000 0000 0000 | 表示此字段为编译器合成,非源代码产生 |
ACC_ENUM | 0x4000 | 0100 0000 0000 0000 | 表示此字段为枚举值 |
name_index与descriptor_index同样还是指向常量池的某个常量的索引。 |
字段比常量结构更复杂的是,其中还包含Attribute_info,即属性,属性既可包含在字段与方法之中,也能单独存在。
属性也拥有自己的结构Attribute_info,可以跳到下面的属性阶段查看。
依本文的例子Point.class为例,其中一个fied为:
Field # 0 Field_info -->
access_flags = 00 02
name_index = 18
descriptor_index = 19
attributes_count = 0
方法
字段后,为方法,同样先读2byte得到方法的数量,然后再看方法的结构,方法的结构与字段结构如出一辙:
Method_info{
access_flags(2)
name_index(2)
descriptor_index(2)
attributes_count(2)
Attribute_info(attributes_count)
}
方法同样有修饰符,又与之前的字段修饰符也有一定差别。看表:
名 | 值 | 转二进制 | 义 |
---|---|---|---|
ACC_PUBLIC | 0x0001 | 0000 0000 0000 0001 | 表示此方法修饰符为public |
ACC_PRIVATE | 0x0002 | 0000 0000 0000 0010 | 表示此方法修饰符为private |
ACC_PROTECTED | 0x0004 | 0000 0000 0000 0100 | 表示此方法修饰符为protected |
ACC_STATIC | 0x0008 | 0000 0000 0000 1000 | 表示此方法修饰符为static |
ACC_FINAL | 0x0010 | 0000 0000 0001 0000 | 表示此方法修饰符为final |
ACC_SYNCHRONIZED | 0x0020 | 0000 0000 0010 0000 | 表示此方法修饰符为synchronize |
ACC_BRIDGE | 0x0040 | 0000 0000 0100 0000 | 表示此方法由编译器产生 |
ACC_VARARGS | 0x0080 | 0000 0000 1000 0000 | 表示此方法有变长参数 |
ACC_NATIVE | 0x0100 | 0000 0001 0000 0000 | 表示此方法非Java方法 |
ACC_ABSTRACT | 0x0400 | 0000 0100 0000 0000 | 表示此方法修饰符abstract |
ACC_STRICT | 0x0800 | 0000 1000 0000 0000 | 表示此方法为strictfp,即FP-strict浮点模式 |
ACC_SYNTHETIC | 0x1000 | 0001 0000 0000 0000 | 表示此字段为编译器合成,非源代码产生 |
与字段一样,name_index与descriptor_index同样还是指向常量池的某个常量的索引。也同样有属性Attribute_info。 |
依本文的例子Point.class为例,其中一个method为:
Method # 0 Method_info -->
access_flags = 00 01
name_index = 24
descriptor_index = 25
attributes_count = 1
Attr # 0 Attribute_info -->
attribute_name_index = 26
attribute_length = 47
attribute_content = 00 01 00 01 00 00 00 05 2A B7 00 01 B1 00 00 00 02 00 1B 00 00 00 06 00 01 00 00 00 03 00 1C 00 00 00 0C 00 01 00 00 00 05 00 1D 00 1E 00 00
属性
方法之后就是属性,先读2byte得到属性的数量,再看属性的结构:
Attribute_info{
attribute_name_index(2)
attribute_length(4)
attribute_content = bytes(attribute_length )
}
attribute_name_index表示一个索引,会直接指向一个Constant_Utf8_info的字符串,attribute_length表示接下来的attribute_content 的长度,依此长度可读取attribute_content的内容。
这个结构只是属性的共同结构,实际上由于attribute_content的内容不同,分为很多种类型的属性结构。不过由于种类过多,这里就不再深究的,其各种类型的属性结构与字段、方法等大同小异,只是嵌套会更加令人头疼而已。
比如在方法method_info经常出现的属性Code_atrribute,Code_atrribute是用来描述一个函数方法的,其结构是这样:
Code_attribute{
attribute_name_index(2)
attribute_length(4)
max_stack(2)
max_locals(2)
code_length(4)
code(cdoe_length)
exception_table_length
exception_table(exception_table_length){
start_pc(2)
end_pc(2)
handler_pc(2)
catch_type(2)
}
attributes_count(2)
attribute_info(attributes_count)
}
相比之下,字段、方法的结构比属性要简单许多,而这还只是一种属性的结构而已。在Java SE 8的规范中,属性的种类数量已经达到23个。
所以这里不再对属性作更详细的解析,只是把属性的内容得到就行。
依本文的例子Point.class为例,其中一个attribute为:
Attr # 0 Attribute_info -->
attribute_name_index = 39
attribute_length = 2
attribute_content = 00 28
这里解析出来的00 28,其实属于ConstantValue_attribute,这种属性的结构除了公共的两个字段attribute_name_index和attribute_length 外,仅仅只有一个constantvalue_index,也就是指向一个常量的索引0x0028=40。
至此,一个class文件就解析完毕,这里的Point.class源码为:
package com.xter.design.builder;
public class Point {
private int x;
private int y;
private int z;
private String desc;
public void setX(int x) {
this.x = x;
}
public void setY(int y) {
this.y = y;
}
public void setZ(int z) {
this.z = z;
}
public void setDesc(String desc) {
this.desc = desc;
}
@Override
public String toString() {
return "Point{" +
"x=" + x +
", y=" + y +
", z=" + z +
", desc='" + desc + '\'' +
'}';
}
}
这里同样有一个用于解析class文件的py,当然没有javap好用,不过自己写一个解析流程的程序是有助于对class文件格式的理解的。
(之前好像因为放了PDF链接审核过不了了)