Class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在Class文件之中,中间没有添加任何分隔符,这使得整个Class文件中存储的内容几乎全部是程序运行的必要数据,没有空隙存在。当遇到需要占用8位字节以上空间的数据项时,则会按照高位在前的方式分割成若干个8位字节进行存储。
以下面的程序分别读一下class文件
public class TestClass {
private int m ;
public int inc() {
return m + 1;
}
}
打开class文件,以16进制的方式显示文件,我这里使用的是notepad++,然后安装了HEX-editor插件得到了下面的文件,当然也可以通过java程序读生成的class文件。(我这个得到的文件里面的数字和《深入java虚拟机》里面的不太一样,不太清楚是什么原因,但是大致上还是可以读的)
在读文件之前,顺便说一下,jdk中提供了一个javap
工具,可以对class文件解读,来试试看效果
javap -verbose TestClass
这个图片里面的内容中的大部分都会涉及到,部分不会涉及到,因为以目前的功底还看不懂。
一个class文件主要分成了下面这几个部分,下文也是从这几个部分一个一个读文件的。
魔数(magic/u4)+次版本号(minor_version/u2)+主版本号(major_version/u2)
这部分几乎是固定的,cafebabe是所有标准的class文件都有的,称之为魔数(magic),没什么研究的必要。0000 0034这几个其中0000表示jdk的次版本号(minor_version),0034表示jdk的主版本号(major_version),恰好对应用javap- verbose
得到的中的minor version: 0 major version: 52
。版本号主要对照下面的表(我的jdk版本是1.8):
常量数量(constant_pool_count/u2)
0x0016=22,表示的是该class中一共有22-1=21个常量(有些常量是虚拟机自动生成的) 使用javap -verbose
得到的也可以看到一共有从#1到#21,可见是21个常量
常量池(constant_pool)
下面图片中选中的部分就是这21个常量:
要看懂这部分的内容主要是参照下面的表格(其中类型中的u1,u2分别表示1byte,2byte):
这里举例子读一个:
0a
等价于十进制的10,查表得tag=10的为CONSTANT_Methodref_info。也可以得到接下来的四个字节为2个索引。
故继续读接下来的四个字节,分别为00 04 00 12
为十进制的4和18,所以这部分读出来的结果就是
#1 = Methodref #4.#18
与javap -verbose
得到的结果是一样的。
通过对照表格和ASCII编码等,既可以得到全部的21个常量
#1 = Methodref #4.#18 // java/lang/Object."<init>":()V
#2 = Fieldref #3.#19 // jvm/TestClass.m:I
#3 = Class #20 // jvm/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 Ljvm/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 jvm/TestClass
#21 = Utf8 java/lang/Object
访问标志(access_flags/u2)
在21个常量之后的两个字节为00 21
这个表示的class文件对应的类(接口)的一些访问信息。查下面的可以得到0021对应的应该是ACC_PUBLIC
和ACC_SUPER
这两个的结合,通过这个信息就可以知道,这个类为public类型的,同时既不是抽象类也不是接口,同时也不是注解或者枚举。对应的javap -verbose
得到的就是常量池上面的flags: ACC_PUBLIC, ACC_SUPER
类索引(this_class/u2) 父类索引(super_class/u2) 接口索引(interface_count/u2)
类索引和父类索引都是一个u2类型的数据,而接口索引集合是一组u2类型的数据的集合,Class文件中由这三项数据来确定这个类的继承关系。类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名。由于Java语言不允许多重继承,所以父类索引只有一个,除了java.lan g .Object之外,所有的Java类都有父类,因此除了java.lan g .Object外,所有Java类的父类索引都不为0。接口索引集合就用来描述这个类实现了哪些接口,这些被实现的接口将按implements语句(如果这个类本身是一个接口,则应当是extends语句)后的接口顺序从左到右排列在接口索引集合中。
在访问标志之后的6个字节为00 03 00 04 00 00
分别表示的是类的名字为常量池03号的类,继承的类为04号的类,接口的个数为0。查看常量池,可得,03号类对应的类的名字在20号中,为jvm/TestClass
,04号对应的类的名字的21号中,为java/lang/Object
。由于这个文件中没有继承接口,所以关于接口(interfaces)的信息这里就没有体现 。
字段数量(fields_count/u2)和字段信息(fields)
接下来的两个字节为00 01
表示的是这个文件下有1个字段,查看源代码,也可以看出只申明了一个m字段,类型是int,访问权限是private。
关于这个m属性的信息在class文件中也是有体现的,接下来的8个字节为00 02 00 05 00 06 00 00
分别对应的是下面的信息:
一一对照,可以得到访问权限(access_flags)是00 02,名字对应的索引(name_index)为00 05,描述所对应的索引(descriptor_index)为00 06,属性数目(attributes_count)为00 00,因为属性数目为0,所以属性信息这里也没有显示。关于属性的范围内权限,可以查看下表,得到的为private,名字对应常量池中的5号,为m,类型对应的常量池中的6号,为()I,()I所代表的含义通过查下表可得为int,所以就可以得到private int m;
这个语句。
方法数量(methods_count/u2)和方法信息(methods)
在部分读的方法和字段的方法几乎是相同的。在字段之后的两个字节表示方法的数量,此处为00 02
,即是有两个方法,但是查看源文件,明明只有声明一个方法为什么会有两个方法呢?熟悉java的,应该都知道java会自动生成一个构造函数,所以另一个方法就是构造函数。
在方法数目之后为00 01 00 07 00 08 00 01
,与字段相同,但是方法的访问权限或者访问控制更多。参照上表和下表,可知,访问权限位public(0001),名字为(0007),返回类型void(0008)(这里有一个疑问,明明构造函数没有返回值,为什么这里用的是()V),属性的数目为1个(0001),接下来的从地址偏移000000e0b-000001200描述的都是这个属性表中的内容。
关于属性表中的信息又可以分为几个部分
Code
对应着表格,可以读出这部分的内容,00 09
表示这Code这个词(Attribute_name_index),指向常量池中的Code单词,为固定值,代表着该属性的名称。00 00 00 2f
代表着该属性值的长度(attribute_length),这个默认生成的构造函数的长度为接下来的37个字节。00 01
表示操作数栈(max_stack)深度的最大值,虚拟机在加载类的时候就是根据这个值分配张帧中的操作数栈深度。接下来的00 01
表示局部变量表的存储空间(max_locals),这里的单位为Slot(四个字节),接下来的00 00 00 05
表示代码编译之后形成的字节码的长度(code_length),这里表示接下来的五个字节就是函数对应的字节码,为2a b7 00 01 b1
,字节码对应的指令可以通过http://blog.csdn.net/coslay/article/details/49725879这里去查看,逐个对应之后得到,就可以得到Code中的字节码的含义。接下来的00 00
表示的是抛出的异常的长度(exception_table_length),这里没有抛出异常,所以也就没有exception_table属性,在接下来为00 02
对照表格这个应该是attribute_count的值,但是这个值不为0,意味着接下来应该还有2个字节是attribute的内容,但是接下来的并不是attribute的值,所以这两个字节的含义暂时不是很懂,先放着。
对应着javap -verbose
得到的就是
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable
LineNumberTable属性用于描述Java源码行号与字节码行号(字节码的偏移量)之间的对应关系。它并不是运行时必需的属性,但默认会生成到Class文件之中,如果选择不生成LineNumberTable属性,对程序运行产生的最主要的影响就是当抛出异常时,堆栈中将不会显示出错的行号,并且在调试程序的时候,也无法按照源码行来设置断点。
00 0a
表示名字的索引(attribute_name_index),这里就是LineNumberTable,00 00 00 06
表示的是属性的长度(attribute_length),为6,00 01
表示的是表格的长度,这里是1,接下来是line_number_info。line_number_info表包括了start_pc和line_number两个u2类型的数据项,前者是字节码行号,后者是Java源码行号。这里00 00
和00 07
分别代表的就是字节码行号和Java源码行号。对应java -verbose
就是
LineNumberTable:
line 7: 0
LocalVariableTable
LocalVariableTable属性用于描述栈帧中局部变量表中的变量与Java源码中定义的变量之间的关系,它也不是运行时必需的属性,但默认会生成到Class文件之中,如果没有生成这项属性,最大的影响就是当其他人引用这个方法时,所有的参数名称都将会丢失,IDE将会使用诸如arg0、arg1之类的占位符代替原有的参数名。
start_pc和length属性分别代表了这个局部变量的生命周期开始的字节码偏移量及其作用范围覆盖的长度,两者结合起来就是这个局部变量在字节码之中的作用域范围。
name_index 和descriptor_index 都是指向常量池中CONSTANT_Utf8_info型常量的索引,分别代表了局部变量的名称以及这个局部变量的描述符。
index 是这个局部变量在栈帧局部变量表中Slot的位置。当这个变量数据类型是64位类型时(double和long),它占用的Slot为index 和index +1两个。
故读出来就是
LocalVariableTable:
Start Length Slot Name Signature
0 7 0 this Ljvm/TestClass;
接下来的为00 01 00 0e 00 0f 00 01
表示的就是第二个方法inc方法了,读的方式一模一样,这里就不赘述了。
SourceFile
这里说一下,倒数第九和第十这个两个字节暂时还不知道是什么意思。
SourceFi le属性用于记录生成这个Class文件的源码文件名称。对于SourceFile的读取主要参照下表,为最后的八个字节,最后读出来是SourceFile: "TestClass.java"
到这里基本上整个class文件就读完了。但是说一下,这部分内容个人感觉了解就好,或者知道是怎么回事就好了,因为毕竟这部分内容根本不可能靠人工去读,现在也有很多成熟的工具来读这部分的内容。