JVM总结(三):类文件结构

时间:2023-03-08 17:54:33
JVM总结(三):类文件结构

这一节我们来总结一下类文件结构方面的知识。目录如下:

类文件结构

字节码的意义

为什么存在字节码? 
  字节码是构成Java平台无关性的基石。实现语言无关性的基础是虚拟机和字节码存储格式。 
  Java语言中的各种变量、关键字和运算符的语义最终是由多条字节码命令组成,因此字节码命令所能提供的语义描述能力肯定会比Java语言本身更强大。

Class类文件的结构

Class类文件的存储形式

  Class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在Class文件中,中间没有添加任何分隔符,所以整个Class文件中存储的内容基本上程序运行中必要的数据。 
  当遇到需要占用8位字节以上空间的数据项时,则会按照高位在前的方式分割成若干个8位字节进行存储。

Class文件的格式

Class文件的格式:类似于C语言结构体的伪结构来存储,这种伪结构只有两种数据类型:无符号数和表,后面的解析以这两种数据类型为基础。 
  1)无符号数:属于基本的数据类型,无符号数可以描述数字、索引引用、数量值或者是按照UTF-8编码构成字符串值。 
  2)表:由多个无符号数或者其他表作为数据项构成的复合数据类型。表用于描述有层次关系的复合结构的数据。整个Class文件本质上就是一张表。 
JVM总结(三):类文件结构  无论是无符号数还是表,当需要描述同一类型但数量不定的多个数据时,经常会使用一个前置的容量计数器加若干个连续的数据项的形式,这时候称这一系列连续的某一类型的数据为某一类型的集合。 
  Class文件中的数据项无论是顺序还是数量,都是被严格限定的,哪个字节代表什么意思,长度是什么样的,先后顺序如何,都是不允许改变的。

Class类文件结构详解

1)magic:魔数,头四个字节,它的作用是用于确定这个文件是否为一个能被虚拟机接受的Class文件。很多文件存储标准中都使用魔数来进行身份识别,如图片格式等。 
  使用魔数而不是扩展名来进行识别主要是基于安全考虑,因为文件扩展名是可以随意修改的。 
  Class文件的魔数值为:0xCAFEBABE。

2)Minor Version、Major Version(Class文件的版本号):在魔数之后的4个字节。第5、6个字节是次版本号(Minor Version),第7、8个字节是主版本号(Major Version)。 
  Java版本号是从45开始的,JDK1.1之后的每个JDK大版本发布主版本号向上加1(JDK 1.0~1.1 使用45.0~45.3的版本号),高版本的JDK能向下兼容以前版本的Class文件,但不能向后兼容。

3)constant-pool常量池:常量池是Class文件结构中与其他项目关联最多的数据类型,也是占用Class文件空间最大的数据项目之一,同时它还是Class文件中第一个出现的表类型数据项目。 
  由于常量池中的常数的数量是不固定的,所以在常量池的入口处放置一项u2(代表两个字节)类型的数据,代表常量池容量计数值(constant-pool-count)。值得注意的是:这个容量计数是从1而不是从0开始的。原因是为了满足后面某些指向常量值得索引值的数据在特定情况下需要表达“不引用任何一个常量池项目”的意思。 
常量池中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References)。字面量比较接近于Java语言层面的常量概念,如文本字符串、被声明为final的常量值。而符号引用则属于编译原理方面的概念,包括:a、类和接口的全限定名(Fully Qualified Name);b、字段的名称和描述符(Descriptor);c、方法的名称和描述符。 
  Java代码在进行Javac编译时,不像C或者C++那样的“连接”这一步骤,而是在虚拟机加载Class文件的时候进行动态加载。所以,在Class文件中不会保存各种方法和字段的最终内存布局信息,因此这些字段和方法的符号引用不经过转换的话就无法让虚拟机使用。当虚拟机运行时,需要从常量池获得对应的符号引用,再在类创建时或者运行时解析并翻译到具体的内存地址中。 
常量池中的每一项常量都是一个表,共有11种结构各不相同的表结构数据。这11种表都有一个共同的特点,就是表开始的第一位是一个u1类型的标志位(tag,取值为1~12,缺少标志为2的数据类型),代表这个常量属于哪种常量类型。 
JVM总结(三):类文件结构  如:CONSTANT_Class_info型常量的结构:(u1)tag + (u2)name_index,标志位+索引值,索引值指向常量池中的一个CONSTANT_Utf-8_info类型的常量,此常量代表这个类(或者接口)的全限定名。 
  CONSTANT_Utf8_info型常量的结构:(u1)tag+(u2)length+(u1)bytes,标志位+字符串长度的字节+数量为length的bytes值。 
  Class文件中方法、字段等都需要引用CONSTANT_Utf8_info型常量来描述名称,所以CONSTANT_Utf8_info型常量的最大长度也就是Java中方法和字段名的最大长度,最大值是65535.所以Java程序中如果定义了超过64KB英文字符的变量或方法名,将会无法编译。 
  javap可以用来分析Class文件字节码,用javap -verbose参数输出Class文件的字节码内容。

4)access_flags(访问标志):在常量池结束后,紧接着的2个字节代表访问标志(access_flags),这个标志用来识别一些类或接口层次的访问信息,包括:这个Class是类还是接口,是否定义为public类型,是否定义为abstract类型,如果是类。是否被声明为final等等。 
JVM总结(三):类文件结构

access_flags中一共有32个标志位可以使用,当前只定义了其中的8个,其他的标志位要求一律为0.

5)this_class、super_class、interfaces(类索引、父类索引和接口索引集合):this_class和super_class都是一个u2类型的数据,而接口索引集合是一组u2类型的数据的集合。Class文件中由这三项数据来确定这个类的继承关系。 
  类索引用于确定这个类的全限定名,父索引用于确定这个类的父类的全限定名。由于java语言不允许多重继承,所以父索引只有一个,且不为空。接口索引集合描述了这些类实现了哪些接口,它们按照继承的顺序依次从左到右排列在接口集合中。 
  类索引、父类索引和接口索引集合都按照顺序排列在访问标志之后。类索引和父类索引引用两个u2类型的索引值表示,他们各自指向一个类型为CONSTANT_Class_info的类描述符常量,通过其内的索引值找到定义在CONSTANT_Utf8_info类型的常量中的全限定名字符串。而对于接口索引集合,入口的第一项是u2类型的接口计数器(interfaces_count)表示索引表的容量。

6)filed_info(字段表集合):用于描述接口或类中声明的变量。字段(filed)包括了类级变量或实例化变量,但不包括在方法内部声明的变量。 
  字段中包括的信息:字段的作用域(public、private、protected修饰符)、是类级变量还是实例级变量(static修饰符)、可变性(final)、并发可见性(volatile修饰符)、可否序列化(reansient修饰符)、字段数据类型(基本类型、对象、数组)、字段名称等。在这些信息中,各个修饰符都是布尔值,要么有,要么没有。而字段叫什么名字、字段被定义成什么数据类型,这些都是无法固定的,只能引用常量池中的常量来描述。 
字段表结构: 
field_info { 
  u2 access_flags; 
  u2 name_index; 
  u2 descriptor_index; 
  u2 attributes_count; 
  attribute_info attributes[attributes_count]; 
}
字段访问标志: 
JVM总结(三):类文件结构  跟随access_flags标志的是两项索引值,name_index和descriptor_index。它们都是对常量池的引用,分别代表着字段的简单名称及字段和方法的描述符。 
描述符标识字符含义: 
JVM总结(三):类文件结构  对于数组类型,每一个维度将使用一个前置的“【”来描述。如“【I”:一个整型数组。 
  用描述符来描述方法时,按照先参数列表,后返回值的顺序描述,参数列表按照严格顺序放在一组小括号之内。如方法void inc()的描述符为“()V”。 
  字段表集合中不会列出从超类或者父接口中继承而来的字段,但有可能列出原本java代码之中不存在的字段,如在内部类中为了保持对外部类de访问性,会自动添加指向外部类实例的字段。

7)methods(方法表集合):大体内容如下 
field_info { 
u2 access_flags; 
u2 name_index; 
u2 descriptor_index; 
u2 attributes_count; 
attribute_info attributes[attributes_count]; 

方法访问标志:

JVM总结(三):类文件结构

8)attribute_info(属性表集合):在Class文件、字段表、方法表中都可以携带自己的属性表集合,以用于描述某些场景专有的信息。 
虚拟机规范预定义的属性: 
JVM总结(三):类文件结构在这里要着重讲解一下Code的内容: 
Code_attribute { 
u2 attribute_name_index; 
u4 attribute_length; 
u2 max_stack; 
u2 max_locals; 
u4 code_length; 
u1 code[code_length]; 
u2 exception_table_length; 
{ u2 start_pc; 
u2 end_pc; 
u2 handler_pc; 
u2 catch_type; 
} exception_table[exception_table_length]; 
u2 attributes_count; 
attribute_info attributes[attributes_count]; 
}

举例详解

下面我们通过举一个例子来明确一下类文件结构

一、写程序

首先,我们来写一个简单的Java程序:

public class TestExample {
public static void main(String[] args){
 
String name = "zhouyuqin";
int age = 20;
 
System.out.println("name:" + name + "age:" + age);
}
}

二、查看生成的相应的Class文件的16进制形式

第二步,我们通过javap来查看java编译器为我们生成的字节码。 
通过在终端中使用javap -c TestExample(程序名来查看生成的字节码)。当程序如上所示时,生成的字节码如下: 
 JVM总结(三):类文件结构

JVM总结(三):类文件结构具体16进制代码如下: 
JVM总结(三):类文件结构

JVM总结(三):类文件结构

三、深入解析

第三步:深入解析: 
1、魔数: 
 JVM总结(三):类文件结构一般为0xcafebabe。 
2、版本号: 
 JVM总结(三):类文件结构第5和第6个字节是次版本号(00 00),第7和第8个字节是主版本号(00 33即版本为JDK1.7) 
3、常量池: 
 JVM总结(三):类文件结构由Javap命令输出的常量池可知该常量池总的有(00 2f,即46项常量),具体综合上图自行分析。 
4、访问标志 
 JVM总结(三):类文件结构访问标志为ACC_public和ACC_super标记为真,其余六个标记为假,查表可知访问标记access_flags=0x0021(0x0001|0x0020). 
5、类索引、父类索引和接口索引集合 
 JVM总结(三):类文件结构如图所示:类索引为0x000c (即12),父类索引为0x000d(即13),接口索引为0x0000(即0)。 
6、字段表集合 
 JVM总结(三):类文件结构由程序可知,该实验程序中不包含字段,故字段表集合为0x0000。 
7、方法表集合 
 JVM总结(三):类文件结构由方法表集合的0x0002可知,该程序中拥有两个方法(构造方法和main方法) 
1)构造方法 
 JVM总结(三):类文件结构0x0001:access_flags,方法为public 
0x000e:name_index,方法名字在常量池中的索引为14. 
0x000f:descriptor_index,描述符索引在常量池中的索引为15 
0x0001:attributes_count,属性表属性为1; 
0x0010:attributes索引为10,查常量表可知该值为Code,长度为0x0000001d(即29个字节) 
2)main函数 
 JVM总结(三):类文件结构0x0009:access_flags,方法为public+static(0x0001|0x0008) 
0x0012:name_index,方法名字在常量池中的索引为18. 
0x0013:descriptor_index,描述符索引在常量池中的索引为19 
0x0001:attributes_count,属性表属性为1; 
0x0010:attributes索引为10,查常量表可知该值为Code,长度为0x0000004d(即77个字节) 
8、最后 
 JVM总结(三):类文件结构00 01:1个类的attr 
00 14:SourceFile 
00 00 00 02:len=2 
00 15:21,Test.java