类文件是由一个.java文件经过编译后形成的.class文件。class文件中包含了虚拟机指令集和符号表以及若干其他辅助信息。class文件交给JVM去运行,JVM再在不同的系统环境中执行代码。可以说这也是Java实现跨平台的重要一步。class文件不光只有Java可以生成,只要是运行在JVM上的语言经过不同的语言编译器编译后都可生成class文件在JVM上运行。
class文件由以下表构成(按顺序):
1、Magic Number(魔数):用于确定该文件是一个class文件。class文件中该值为四个16字节,值为0xCAFEBABE。
2、紧接着Magic Number的4个字节为版本号。高版本的JDK兼容低版本的JDK,但反过来不可以。
3、常量池(14种类型的表构成常量池)。
4、访问标志。
5、类索引(this)、父类索引(super)、接口索引。
6、字段表。
7、方法表。
8、属性表。属性表比较特殊,其跟在字段表、方法表后面。
常量池是Class文件中所占空间最大的数据项目。常量池中入口处为一个u2(两字节)类型的数字,表示常量池中有多少项常量。常量池中有14种表来表示常量,每个表都有相应编号(在开头)。比如CONSTANT_Class_info代表该类的信息。该表中的name_index若为2则代表指向第二个常量。第二项开头的索引为1,则代表为CONSTANT_Utf8_info表,后边的length长度的数字为该类全限定名的ASC码。(P169~P170)若常量池开头开头为0x16时说明有21项常量(p171),但常量项的索引却从1开始(1~21),这是因为当不引用常量池中任何一个项目时,将索引值置0。对于其他集合类型,如接口索引集合、字段表、方法表索引都是从0开始。
常量池主要存储下列两种内容:
(1)字面量
字符串、final static常量(constantValue会指向常量池中该值)等。
(2)符号引用
1、类和接口的全限定名。即带包名的完整类名,并把包中的 . 换为 / 分隔。
2、字段的名称(字段名)和字段描述符(字段所属的数据类型,比如int、char等)。
3、方法的名称(方法名字)和方法描述符(方法的参数列表和返回类型)。
14种表的详细结构见p169、p172。
在常量池结束后,紧跟的两个字节为访问标志。该标志用于表示该class是类还是接口以及是否为public、abstract、synchronized等。
该标志有16个标志位(ACC_XXX),将符合条件的标志位的值叠加生成访问标志的值。
表的详细结构见p173。
紧接着访问标志为的为这三个索引,class文件中的继承关系即由这三个索引确定
(1)类索引(两字节)
类索引用于确定这个类的全限定名。也就是说名字光存在常量池中了还不够,得有人指向它。this_class指向常量池中自己类的CONSTANT_Class_info表(常量池中第this_class项),这个表中的index在去指向储存着真正类名的CONSTANT_Utf8_info表。
(2)父类索引(两字节)
该索引指向这个类父类的全限定名。由于Java没有多继承,所以除了Object类,该索引只有一个。如何寻找类名与类索引一致
(3)接口索引集合(多个两个字节组成的集合)
虽然Java不允许多继承,但是可以多实现接口。所以接口索引第一个两个字节代表的是实现了多少个接口,也就是接口的个数。implements语句后的接口按顺序从左到右排列在接口索引集合中。
表的详细结构见p175。
字段表用于描述接口或类中声明的字段,一个字段表对应一个字段。字段包括类级变量(static)以及实例级变量(new出来的)。字段表有以下几项:
access_flags(存放该字段的修饰符,如public、static、final、volatile等)、name_index(字段的名字的索引,指向常量池第name_index项)、descriptor_index(该字段的数据类型或对象类型的描述符,指向常量池第descriptor_index项)、attributes_count(这个字段表跟随attributes_count个属性)、attributes(属性表集合)。
举个例子,比如
public long x ;
该字段表的第一项为0x0001,代表public;第二项为字段名,即x;第三项为描述符,这里为J,代表数据类型为long。
final static int x = 3,
如果该字段为final static修饰的常量,那么字段表屁股会跟随一个名称为ConstantValue的属性表,其指向常量池中该字段的值(常量池中CONSTANT_Integer_Info类型表,值为3)。
表的详细结构见p176~p177。
如果理解了属性表,那么方法表也只是如法炮制。方法表描述类中的方法(包括static方法),一个方法表表示一个方法,多个方法表组成一个方法表集合。方法表有以下几项:
access_flags(存放该方法的修饰符,如public、static、final、synchronized等)、name_index(方法的名字的索引,指向常量池第name_index项)、descriptor_index(该方法的描述符,指向常量池第descriptor_index项)、attributes_count(这个字段表跟随attributes_count个属性)、attributes(属性表集合)。其中方法里面的代码会存在名为Code的属性表中
举个例子,比如:
public int show(int x, char[] args) {
System.out.print(args[0] + x);
return ++x;
}
方法描述符的顺序为先参数列表,后返回值。参数列表按照参数的严格顺序放在一组小括号中,紧接着的为方法返回值描述符。
该方法表的第一项为0x0001,代表public;第二项为方法名,即show;第三项为方法描述符,这里为(I[C)I。
还需注意两点:(1) 如果父类方法在子类中没有被重写,方法表中就不会出现来自父类方法的信息 。这一点在继承中尤为关键,千万不要以为子类中有父类方法的拷贝,子类是通过父类索引找到父类的class文件从而调用父类(继承得来)的方法。
class Father {
public int Conf;
public void init(int Conf) {
this.Conf = Conf;
this.init();
}
public void init() {
System.out.println("Father running.......");
}
}
public class Son extends Father{
public void init() {
System.out.println("Son running......");
}
public static void main(String[] args) {
Son f = new Son();
f.init(6);
}
}
上图是子类通过Java ByteCode Editor解析出的方法表集合。可以看出子类只有一个自己的init方法。
(2) 编译器也会添加自己的某些方法,如
表的详细结构见p178~p179。
。