什么是Class文件?

 

Class文件是一组以八位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑的排列在Class文件之中,中间没有添加任何分隔符,这使整个Class文件种存储的内容机会全部都是程序运行所需要的,没有空隙存在。

 

Class格式:根据Java虚拟机规范规定,Class文件格式采用一种类似于C语言结构体的伪结构来存储数据类型,这种伪结构只有两种数据类型:无符号数和表。

1.无符号数:无符号数属于基本数据类型,可以用来描述数字,索引引用,字符串

2.表:是由多个无符号数或其他表作为数据项构成的复合数据类型。

 

 

 

整个Class文件本质就是一张表,它的构成如下:

无论是无符号数还是表,当需要描述同一类型但数量不定的多个数据时,经常会使用一个前置的数标识数量,后面跟上具体的数据项,这公司称这一系列连续的某一类型数据为某一类型集合。

 

强调:因为我们的Class文件之间没有任何间隙,每一个八位二级制数都有其意义,所以格式要求相当严格,长度,顺序,都不允许改变。

 

 

下面,我们来解释这张表,读懂了这张表,Class文件也就读懂了!

提示:u1代表一个字节(也就是八位二进制数),u2代表两个字节以此类推。

 

我们先来看看Class文件打开后到底是什么样子。

 上面Class文件对应的Java代码源码如下:

public class TestClass{
	private int m;

	public int inc(){
	
		return m+1;
	}

}

 

 

一.magic(魔数)

魔数:每个Class文件的前四个字节称为魔数,它唯一的作用就是确定这个文件是否为一个能被虚拟机解读的Class文件

CA FE BA BE(咖啡宝贝),就是魔数。

 

二.Class文件版本

接下来的四个字节说明了存储的Class文件的版本号,第五,第六两个字节是次版本号,7,8两个是主版本号。高版本的JVM可以兼容低版本的Class文件,但是低版本的JVM无法执行高版本的Class(向下兼容)。

我们看到上面的Class文件的5,6,7,8是 00 00 00 34十六进制的34就是十进制的52,Jdk1.8的主版本号是52。其它如下图:

 其实我们只用记住JDK1.8是52,JDK1.7是50就足够了。

 

三.常量池

紧接着版本号之后的是常量池入口,常量池可以理解为Class文件中的资源仓库。它是Class文件中与其它项目关联最多的数据类型,也是占用Class文件空间最大的数据项目之一,同时他还是在Class文件中第一个出现的表类型数据项目

 

我们可以使用Java给我们提供的工具,直接得到由Class文件翻译出来的常量池,如下:

 

 在空中控制台输入javap -verbose 类名,就可以得到如上的结果。

 

1.由于常量池中常量的数量是不固定的,所以常量池入口处放置了一项u2类型的数据,代表常量池容量计数值(constant_pool_count)。常量池中常量的数量为(constant_pool_size-1),且下标从1开始。(将索引0位置用来表示“不引用任何一个常量池项目”)。

2.常量池中主要存放两大类常量:字面量(类似于Java中的常量)和符号引用。

字面量:如文本字符串,声明为final的常量等;

符号引用类型:类和接口的全限定名,字段名称和描述符,方法的名称和描述符

 

3.常量池的每一项都是一个表,这十四个表的共同特点就是开始第一位是一个u1类标志位,代表当前这个常量属于那种常量类型(共14项):

 

 

 

 

CONSTANT_Class_info:

tag:上面说过,就是标志位,它用于区分常量的类型。

name_index:是一个索引值,它指向常量池中一个CONSTANT_Utf8_info的类型常量。

 

其他的常量类型,都差不多和上面类似,在这里就不再赘述了。

四.访问标志(Access_flags)

Access_flags中一共有16个标志位可以使用,当前之定义了其中8个。如下:

 

在常量池结束以后,紧跟着两个字节代表访问标志(access_flag),这个标志用于识别一些类或者接口层次的访问信息,包括:这个Class是类还是接口;是否定义为public类型;是否定义为abstract类型;如果是类的话,是否被声明位final等。

你可能回说,一个类可能既是抽象类,也是public如何判断呢?

我们将查上表可知:  抽象类=0x0400  public=0x0001,将他们做或运算得到0x04001,这就通过和Class字节码的对比就可以却定当前类的访问标志。

 

 五.类索引,父类索引,接口索引集合

1.类索引(this.class):是一个u2类型的整形数据。指向一个CONSTANT_Class_info类描述的常量。(通过CONSTANT_Class_info类型的常量索引可以找到定义在CONSTANT_Class_info类型的常量中的权限顶名字符串)

2.父类索引(super_class):是一个u2类型的整形数据。指向一个CONSTANT_Class_info类描述的常量。

 

3.接口索引集合:一组u2类型数据的集合(记着啊,接口数量是不确定的,所以用集合表示,且前面需要用一个u2来表示集合的大小也就是接口数目)。

 

 

 六.字段表集合

字段表:用于描述接口或类中声明的变量。

字段(filed):包括类级变量以及实例级变量(方法之外的变量,但是没有static修饰)。

 

 字段表的结构:

 

1.access_flags:字段访问标志,它与类中的access_flag项目非常类似,下表是其可以设置的标志位和含义:


2.name_index(当前字段名称):对常量池的引用,代表字段的简单名称。

3.Descriptor_index(字段描述符):对常量池的引用,代表当前字段的类型。

4.attributes_count(属性表集合):用于存储当前字段的一些额外的信息,字段都可以在属性表中描述零至多项二外信息。

 

5.当前不讨论(需要后面知识)。

 

 

七.方法表集合。

 

 

可以发现,放发表集合与上面的字段表集合一模一样,所以各个项目的名称我就不一一解释了。

 

你可能会疑惑,方法里面的代码去哪里了?方法里面的Java代码,经过编译器编译成字节码指令后,存放在方法属性表集合中一个名为“code”的属性表里面(属性表是Class文件格式中最具扩性的一种数据项目)。

1.就算你的Class没有自定义类,你的方法表集合长度也不会为0,因为每个类都有自己默认添加的空构造方法。

 

2.Java中要重载(Overload)一个方法,除了要求具有相同的简单名称之外,还要求必须有一个与原方法不同的特征签名,特征签名就是一个方法中各个参数在常量池中的字段符号引用的集合,就是因为返回值不会包含在特征签名中,因此Java语言里面是无法仅仅依靠不同的返回值来对一个方法进行重载的。但是在Class文件格式中,特征签名的范围更一些,只要描述符不完全相同就可以共存。也就是说,如果两个方法有相同的名称和特征签名,但返回值不同,那么就可以合法的存在与一个Class文件中。

 

3.父类中的方法不会出现在方法表集合中,但是如果重写了父类中的方法就会出现在方法表中。

 

 

八.属性表集合

属性表:Class文件,字段表,方法表都可以携带自己的属性表集合,以描述某些场景下特有的信息。

 

与Class文件中其它数据项目要求严格的顺序,长度和内容不同,属性表集合的限制稍微放宽松了一些,不再要求各个属性表具有严格的顺序,并且只要不与以有属性名重复任何人实现的编译器都可以向属性表表中写入自己定义的属性信息。(《Java虚拟级规范(Java SE 7)》版中,预定义属性已经增加到21项)。(预定已属性就是人家已经起好了属性的名字和指定好了其代表的特定意义,放在属性表里供你使用的)。

 

1.Code属性

作用:Java程序方法体中的代码经过Javac编译处理后,最终变为字节码指令存储在Code属性内。Code属性出现在方法表的属性集合之中,的那并非所有的方法表都必须存在这个属性,譬如接口或者抽象类中的抽象方法就不存在Code属性。

 

下来来解释一下Code属性表中各个组成部分的含义:

 

1>attribute_name_index:使是一项指向CONSTANT_Utf8_info型常量的索引,常量值固定为“Code”,它代表了该属性的属性名称。

 

2>attribute_length:指示属性表的长度,属性值长度=属性表长度-u2-u4;

 

3>max_stack:表示了操作数栈(Operand Stacks)深度的做大值。在方法执行的任意时刻,操作数栈都不会超过这个深度。虚拟机运行时需要根据这个值来分配栈帧中操作栈的深度。

 

4>max_locals:代表局部变量表所需要的存储空间。在这里,max_locals的单位是Slot,Slot是虚拟机位局部变量分配内存的最新奥单位,长度不超过32位的局部变量占一个Slot,而64位(double,long)的数据需要两个Slot。方法参数(包括实例方法中隐藏的参数“this”),显式异常处理器的参数(就是try-catch语句中catch块所定义的异常),方法体中定义的局部变量都需要存储在局部变量表中。另外,并不是在方法中用到多少个局部变量,把这些局部变量所占的Slot之和作为max_locals的值,原因是局部变量表中的Solt可以重用,当代码执行超出一个局部变量的作用域时,这个局部变量所占的Solt可以被其他局部变量所使用,Javac编译器会根据变量的作用域来分配Slot给变量使用,然后计算出max_locals的大小。

 

5>code_length和code用来存储Java程序编译后生成的字节码指令。code_length代表字节码长度,code时用于存储字节码指令的一系列字节流。既然称为字节码指令,那么每个指令就是一个u1类型的单字节,当虚拟机读取到code中的一个字节码时,就可以对应找出这个字节码代表的什么指令,并执行。code_length的长度虽然是u4但是虚拟级规范中明确规定一个方法不允许超过65535(u2)条字节码指令。所以有两位是多余的。

 

总结:

Code属性是Class中最重要的一个属性,如果把一个Java程序的信息分为代码(Code,方法体里面的Java代码)和元数据(Metadata,包括类,字段,方法定义及其他信息)两部分,那么在这个Class文件中Code属性用于描述代码,所有的其他数据项都用于描述元数据。

 

其余部分属性如下:

 

2.Exception属性

这里的Exception属性是与Code属性平级的一项属性。Exception属性作用是列出方法中可能抛出的受查异常,也就是throws关键字后面列举的异常。

 

3.LineNumberTable属性

用于描述Java源代码行号与字节码行号之间的对应关系。它并不是运行时必须的属性,但默认会生成到Class文件中,可以关闭。如果选择不生成LineNumberTable属性,对程序运行时最主要的影响就是当程序抛出异常时堆栈中不会显示出错行号,并且在调试程序的时候,也无法按照源码来设置断点。

 

4.LocalVariableTable

用于描述栈帧中局部变量表中的变量与Java源码中定义的变量之间的关系,它不是运行时的必需属性,但默认会生成到Class文件之中,如果不设置最大的影响就是当其他人引用这个方法时,所有参数名称都将会丢失,IDE将会使用诸如arg0,arg1之类的占位符代替原有的参数名,这对程序运行并没有影响。

 

5.SourceFile属性

SourceFile属性用于记录生成这个Class文件的源码文件名称。这个属性也是可选的,在Java中对大多数类来说,类名和文件名是一致的,但是有一些特殊情况(如内部类)例外。如果不生成这项属性,当抛出异常时,堆栈中将不会显示出错代码所属的文件名。这个属性是一个定常属性。

 

6.ConstantValue属性

ConstantValue属性的作用是通知虚拟机自动为静态变量赋值。只有被static关键字修饰的变量才可以使用这项属性。

 

7.InnerClasses属性

用于记录内部类与宿主之间的关联。如果一个类中定义了内部类,那编译器将会为它以及它所包含的内部类生成InnerClasses属性。

 

8.Deprecated属性

用于表示某个类,字段或者方法,已经被程序作者定为不再推荐使用,它可以通过在代码中使用@deprecated注释进行设置。

 

9.Synthetic属性

用于表示此字段并不是由Java源码直接产生的,而是由编译器自动添加的。

 

 

这部是全部的属性表集合,但是我们常用的属性都在这里了,我这里只做了一个功能上的简单描述,如果想要深入了解的小伙伴可以继续探究。

 

 

 

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐