转载地址:http://www.cnblogs.com/beliefbetrayal/archive/2012/02/01/2334100.html
JVM 学习笔记目录:
JVM探索之Class文件结构解析(一):Class文件的格式与定义
JVM探索之Class文件结构解析(二) :常量池
JVM探索之Class文件结构解析(三):访问修饰符、类索引、父类索引与接口索引集合
Class文件的格式与定义
Class文件是一组以8位字节(1Byte=8bit,计算机也有1Byte=16bit或1Byte=32bit的)为基础单位的二进制流,各个数据项目严格按照顺序紧凑的排列在Class文件之中,中间没有添加任何分隔符。当遇到需要占用8位字节以上空间的数据项时,则会按高位在前的方式分割成若干个8位字节进行存储。
Class文件结构中只有2种数据类型:无符号数和表。无符号数, 属于基本的数据类型,以u1、u2、u4、u8来分别表示1个字节、2个字节、4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值和UTF-8编码构成的字符串;表,是由多个无符号数或其他表作为数据项构成的复合数据类型,所有的表都习惯已"_info"结尾(整个Class文件可以看成是一张表)。
上图是Class文件的结构表,无论是无符号数还是表类型,当需要描述同一类型但数量不定的多个数据时。经常会使用一个前置的容量计数器加上若干个连续的数据项形式。例如:contant_pool_count、interfaces_count、fields_count、methods_count等都是前置容量计数器。
为了方便解释这些数据类型,提供一个非常简单的类作为例子来讲解:
按 Ctrl+C 复制代码
package com.beliefbetrayal.clazz; public class ClassFileTest { private int m; public int getM() { return m; } public void setM(int m) { this.m = m; } }
按 Ctrl+C 复制代码
这个例子非常非常简单,定义一个类,它有一个成员变量和对应的get和set方法。现在我们找到该类对应的Class文件,使用WinHex(将二进制文件以16进制的形式打开,1Byte=8bit,所以2个16进制数表示一个字节)将Class文件打开。将会显示如下信息:
图为Class文件的部分信息
Class文件结构的第一个数据项目"magic",它的类型为u4(4个字节),所以0xCAFEBABE为它的值,该数据项称为"魔数",它的作用是用于确定这个文件是否为一个能被虚拟机接收的Class文件。而虚拟机使用魔数而不是用扩展名的机制来判断Class文件是出于安全的考虑,因为扩展名是可以随意修改的。你可以查看所有符合要求的Class文件它的魔数都是"0xCAFEBABE"。
Class文件结构第二个和第三个数据项目为:"minor_version"和"major_version" 分别表示Class文件的次要版本和主要版本信息。它们的类型都为u2(2个字节)所以0x0000表示次要版本,0x0032表示主要版本。Class文件的版本号是从45开始的,JDK1.1之后每一个JDK大版本发布主版本号就向上加1。JDK1.1能支持的版本号为45.0~45.65535的Class文件,JDK1.2能支持的版本号为46.0~46.65535以此类推,现在最新的JDK1.7能支持51.0~51.65535版本号的Class文件。还需要注意的一点是:高版本的JDK可以向下兼容以前版本的Class文件,但不能运行以后版本的Class文件。0x0032换算成十进制为50,按照上面的推导,该Class文件是可以被JDK1.6或以上的版本的虚拟机执行的Class文件(也可以反映出该Class文件是由JDK1.6版本的编译器编译的)。
下面分析:Class文件中的常量池(contant_pool)
JVM探索之Class文件结构解析(二):常量池
常量池
上面介绍了Class文件的“魔数”和“主次版本号”,常量池数据项目的入口是紧接着“主次版本号”数据项目的。Class文件的常量池是Class文件结构中与其他项目关联最多的数据类型,也是占用Class文件空间最大的数据项目之一,同时也是Class文件中第一个表类型的数据项目。为了方面讲解和查看下面给出Class文件结构表和实例Class文件:
示例Class:
package com.beliefbetrayal.clazz;
public class ClassFileTest {
private int m;
public int getM() {
return m;
}
public void setM(int m) {
this.m = m;
}
}
无论是无符号数还是表类型,当需要描述同一类型但数量不定的多个数据时。经常会使用一个前置的容量计数器加上若干个连续的数据项形式。因为Class的常量池中的常量是不固定的,所以常量池的入口需要前置一个容量计数器“contant_pool_count”用于记录常量池中常量的数目,它是u2(2个字节)类型的。 用WinHex打开该Class文件获得下图:
图为Class文件的部分信息1
该常量池的容量计数器的索引是从1开始的而不是从0开始,将0索引空出来时为了满足后面某些指向常量池的索引值的数据在特定情况下需要表达“不引用任何一个常量”的意思。因为量池的容量计数器为u2,所以它的值为"0x0018",换算成10进制数为24,代表了常量池中有23个常量索引为1~23。为了验证准确性,我们可以使用JDK为我们提供的javap工具,下图为使用javap分析Class文件的部分截图(只列出了常量池部分):
可也从图中的信息看出,该Class文件确实有23个常量。下面来具体分析常量池,常量池之中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic Reference)。字面量一般为“文本字符串”、“被声明为final的常量值”等等。符号引用一般为“类和接口的全限定名(Fully Qualified Name)”、“字段的名称和描述符(Descriptor)”和“方法的名称和描述符”。
常量池中的每一项常量都是一个表类型,在JDK6.0之前有11种结构的表数据,这11种表都有一个共同的特点,就是表开始的第一位是一个u1类型的标志位(tag,取值为1~12,标志2空缺),它代表了当前这个常量属于那种常量类型,11种常量类型所代表的具体含义如下图所示:
回头查看“Class文件的部分信息1”图,第一个常量,它的标志位为"0x07",转换为10进制数为7,查看“常量池项目类型”表可以发现该常量属于CONSTANT_Class_info类型,次类型的常量代表一个类或接口的符号引用。CONSTANT_Class_info的结构图如下:
tag标志位已经解释过了,name_index是一个索引值,它指向常量池中一个CONSTANT_Utf8_info类型的常量,此常量代表了这个类(接口)的全限定名,查看“Class文件的部分信息1”图,name_index的值为"0x0002",转换为10进制数为2,指向常量池中第二个常量。继续查看“Class文件的部分信息1”图,查找第二个常量的标志为"0x01",转换为10进制数为1,查看“常量池项目类型”表可知确实是一个CONSTANT_Utf8_info类型的常量。 CONSTANT_Utf8_info类型的常量结构:
length表示这个UTF-8编码的字符串长度是多少字节,它后面紧跟着长度为length的UTF-8编码方式的字符串。查看“Class文件的部分信息1”图,length的值为"0x0026",转换成10进制数为38,表示后面紧跟着长度为38个字节长度的字符串,参看“ Class文件的部分信息2”图:
图为Class文件的部分信息2
查看图片的右边,可以看到被选中的38个字节长度的值为:com/beliefbetrayal/clazz/ClassFileTest这正是我们示例类的全限定名。分析了这么多,我们现在查看用javap工具分析出来的常量池信息(查看常量池信息),可以看到“constant #1 = class #2”这条信息指明了该常量指向#2常量,“constant #2 = Asciz com/beliefbetrayal/clazz/ClassFileTest”这条信息指明了该常量的值为“com/beliefbetrayal/clazz/ClassFileTest”可以看到我们的分析都是正确的。
根据“Class文件的部分信息2”图第三个常量的标志位为"0x07"转换为10进制为7,查看“常量池项目类型”表可以发现该常量属于CONSTANT_Class_info类型,次类型的常量代表一个类或接口的符号引用。根据“CONSTANT_Class_info的结构图”,name_index值为"0x0004",转换为10进制数为4,表示指向第四个常量。 根据“Class文件的部分信息2”图第四个常量的标志为"0x01"转换为10进制数为1,查看“常量池项目类型”表可知是一个CONSTANT_Utf8_info类型的常量,根据CONSTANT_Utf8_info类型结构图,length的值为"0x0010"转换为10进制数为16,表示后面紧跟着16个字节长度的字符串,如下图:
图为Class文件的部分信息3
查看图片的右边,可以看到被选中的16个字节长度的值为:java/lang/Object。 我们现在再回去查看用javap工具分析出来的常量池信息(查看常量池信息),可以很开心的看到我们的分析又被验证了^_^。再接再厉,分析第5个常量,它的标志为"0x01",转换为10进制数为1 ,查看“常量池项目类型”表可知是一个CONSTANT_Utf8_info类型的常量,根据CONSTANT_Utf8_info类型结构图,length的值为"0x0001"转换为10进制数为1,表示后面紧跟着1个字节长度的字符串,如下图:
图为Class文件的部分信息4
查看图片的右边,可以看到被选中的1个字节长度的值为:“m”它是我们定义的成员变量的名称。到此为止我们分析了ClassFileTest常量池中23个常量中的5个,其余的常量都可以使用类似的方法计算出来。手工推导出来后与javap工具分析出来的信息对比可以验证推导的正确性。下面将常量池中的11种常量类型的结构表列出来:
查看用javap工具分析出来的常量池信息(查看常量池信息),仔细观察会发现有许多常量出现的莫名其妙,如:“I”, “LineNumberTable”等,这些自动生成的常量会被后面字段表,方法表和属性表引用,他们会被用来描述一些不方便使用“固定字节”来表达的内容。
常量池的介绍结束了,我们不需要将每一个常量都手工计算出来,主要是要掌握好方法,JDK已经为我们提供了功能强大的Class文件分析工具javap。
JVM探索之Class文件结构解析(三):访问修饰符、类索引、父类索引与接口索引集合
JVM探索之路之Class文件结构解析(三)
先将分析需要的资源信息列出来:
case:
package com.beliefbetrayal.clazz;
public class ClassFileTest {
private int m;
public int getM() {
return m;
}
public void setM(int m) {
this.m = m;
}
}
Class文件结构表:
javap工具分析Class文件信息常量池部分:
在上一次分析Class文件的博文中已经详细分析了如何手工解析Class文件中的constant_pool信息,现在接着分析Class文件结构中紧随constan_pool的访问标示符(access_flag)。使用WinHex打开ClassFileTest.class文件,并找到contant_pool结束的位置:
查看"javap工具分析Class文件信息常量池部分"找到最后一个常量,其值为"ClassFileTest.java",注意";"并不是Class文件中数据,在WinHex中找到它对应的值,以确定常量池结束的位置。查看"Class文件结构表",可知紧随constan_pool的是2个字节代表的访问标志,这个标志的作用是用于识别一些类或者接口层次的访问信息,例如:这个Class是类还是接口,是否定义为public,是否定义为abstract,如果是类的话,是否被定义为final类型的之类的信息。具体的标志位及标志的含义如下表:
访问标志 |
||
标志名称 |
标志值 |
含义 |
ACC_PUBLIC |
0x0001 |
是否为public类型 |
ACC_FINAL |
0x0010 |
是否被声明为final,只有类可以设置,接口不能设置该标志 |
ACC_SUPER |
0x0020 |
是否允许使用invokespecial字节码指令(查了一下该命令的作用为"调用超类的构造方法,实例的构造方法,私有方法"),JDK1.2以后的编译器编译出来的class文件该标志都为真 |
ACC_INTERFACE |
0x0200 |
标识这是一个接口 |
ACC_ABSTRACT |
0x0400 |
是否被声明为abstract类型,对于接口和抽象类来说此标志为真,其他类为假 |
ACC_SYNTHETIC |
0x1000 |
标识这个类并非由用户代码生成 |
ACC_ANNOTATION |
0x2000 |
标识这是一个注解 |
ACC_ENUM |
0x4000 |
标识这是一个枚举 |
查看"ClassFileTest.java"源代码,可以看到该类被声明为"public"的,JDK1.6编译出来的文件,JVM中没有使用标志为一律为0,所以只有ACC_PUBLIC与ACC_SUPER标志位不为0,因此它access_flag的值为0x0001|0x0020=0x0021("|"布尔或操作符),这里省略了其他6个标志的计算,因为"|"操作符,只有全为0才为0,所以虽然要计算8个标志为的值,但是可以简化为2个。
可以看到上图,紧接着2个字节的值为"0x0021",与我们计算的一致。access_flag分析完后,紧接着access_flag的是2个字节的类索引(this_class),2个字节的父类索引(super_class)和一组2个字节的数据集合接口索引(interfaces)。Class文件中就是由这三项数据确定这个类的继承关系。其中类索引this_class用于确定这个类的全限定名:
如图,类索引this_class的值为"0x0001",它是一个指向常量池的索引,"0x0001"转化为十进制为1,表示常量池第一个常量,查看"javap工具分析Class文件信息常量池部分"找到第一项常量,它指向第二项常量,找到第二项常量,可以看到"com/beliefbetrayal/clazz/ClassFileTest"的字符串,这不正是我们定义的类的全限定名。分析完类索引,接着分析父类索引(super_class),众所周知Java语言不允许多继承,索引父类只有一个,除了java.lang.Object类,其他类都有父类:
如图,父类索引super_class值为"0x0003",转换为十进制为3,它与类索引一项指向常量,查看"javap工具分析Class文件信息常量池部分"找到第三个常量,第三个常量又指向第四个常量,看到了"java.lang.Object"的字符串值,这就是为什么显示extends java.lang.Object也会继承他的原因,编译器干的好事^_^。接着分析接口索引(interfaces),查看"Class文件结构表",可以看到它有一个2个字节"interfaces_count"前置计数器,用于计算该类实现了多少个接口,而"interfaces"接口索引用于表式类实现的接口,按照implement语句从左到右排列到接口索引集合中:
因为"ClassFileTest.java"源代码并没有实现任何接口,所以"interfaces_count"前置计数器为0,如图所示它的值如分析的一样"0x0000",因为 "interfaces_count"前置计数器为0,Class文件就不必留空间记录接口索引集合(interfaces),索引接口索引集合(interfaces)信息不会出现在Class文件中。