JVM(四)-类文件结构

Java是与平台无关的语言,这得益于Java源代码编译后生成的存储字节码的文件,即Class文件,以及Java虚拟机的实现。不仅使用Java编译器可以把Java代码编译成存储字节码的Class文件,使用JRuby等其他语言的编译器也可以把程序代码编译成Class文件,虚拟机并不关心Class的来源是什么语言,只要它符合一定的结构,就可以在Java中运行。Java语言中的各种变量、关键字和运算符的语义最终都是由多条字节码命令组合而成的,因此字节码命令所能提供的语义描述能力肯定会比Java语言本身更强大,这便为其他语言实现一些有别于Java的语言特性提供了基础,而且这也正是在类加载时要进行安全验证的原因。

类文件结构

  • Class文件是一组以8字节为基础单位的二进制流,
  • 各个数据项目严格按照顺序紧凑排列在class文件中,
  • 中间没有任何分隔符,这使得class文件中存储的内容几乎是全部程序运行的程序。

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

无符号数

属于基本数据类型,主要可以用来描述数字、索引符号、数量值或者按照UTF-8编码构成的字符串值,大小使用u1、u2、u4、u8分别表示1字节、2字节、4字节和8字节。

是由多个无符号数或者其他表作为数据项构成的复合数据类型,所有的表都习惯以“_info”结尾。表主要用于描述有层次关系的复合结构的数据,比如方法、字段。需要注意的是class文件是没有分隔符的,所以每个的二进制数据类型都是严格定义的。具体的顺序定义如下:

在class文件中,主要分为魔数、Class文件的版本号、常量池、访问标志、类索引(还包括父类索引和接口索引集合)、字段表集合、方法表集合、属性表集合。

魔数

  1. 每个Class文件的头4个字节称为魔数(Magic Number)
  2. 唯一作用是用于确定这个文件是否为一个能被虚拟机接受的Class文件。
  3. Class文件魔数的值为0xCAFEBABE。如果一个文件不是以0xCAFEBABE开头,那它就肯定不是Java class文件。

很多文件存储标准中都使用魔数来进行身份识别,譬如图片格式,如gif或jpeg等在文件头中都存有魔数。使用魔术而不是使用扩展名是基于安全性考虑的——扩展名可以随意被改变!!!

Class文件的版本号

紧接着魔数的4个字节是Class文件版本号,版本号又分为:

  1. 次版本号(minor_version): 前2字节用于表示次版本号
  2. 主版本号(major_version): 后2字节用于表示主版本号。

这个的版本号是随着jdk版本的不同而表示不同的版本范围的。Java的版本号是从45开始的。如果Class文件的版本号超过虚拟机版本,将被拒绝执行。

  0X0034(对应十进制的50):JDK1.8      
  0X0033(对应十进制的50):JDK1.7      
  0X0032(对应十进制的50):JDK1.6      
  0X0031(对应十进制的49):JDK1.5  
  0X0030(对应十进制的48):JDK1.4  
  0X002F(对应十进制的47):JDK1.3  
  0X002E(对应十进制的46):JDK1.2 
ps:0X表示16进制

回到顶部

常量池

 紧接着魔数与版本号之后的是常量池入口.常量池简单理解为class文件的资源从库

  1. 是Class文件结构中与其它项目关联最多的数据类型
  2. 是占用Class文件空间最大的数据项目之一
  3. 是在文件中第一个出现的表类型数据项目

由于常量池中常量的数量是不固定的,所以在常量池的入口需要放置一项u2类型的数据,代表常量池容量计数值(constant_pool_count)。
从1开始计数。Class文件结构中只有常量池的容量计数是从1开始的,第0项腾出来满足后面某些指向常量池的索引值的数据在特定情况下需要表达"不引用任何一个常量池项目"的意思,这种情况就可以把索引值置为0来表示(留给JVM自己用的)。但尽管constant_pool列表中没有索引值为0的入口,缺失的这一入口也被constant_pool_count计数在内。例如,当constant_pool中有14项,constant_poo_count的值为15。
常量池之中主要存放两大类常量:

  1. 字面量: 比较接近于Java语言层面的常量概念,如文本字符串、被声明为final的常量值等
  2. 符号引用: 属于编译原理方面的概念,包括了下面三类常量:
    •   类和接口的全限定名
    •   字段的名称和描述符
    •   方法的名称和描述符

Java代码在进行Java编译的时候,并不像C和C++那样有"连接"这一步骤,而是在虚拟机加载Class文件的时候进行动态连接。也就是说,在Class文件中不会保存各个方法和字段的最终内存布局信息,因此这些字段和方法的符号引用不经过转换的话是无法被虚拟机使用的。当虚拟机运行时,需要从常量池获得对应的符号引用,再在类创建时或运行时解析并翻译到具体的内存地址之中。
constant_pool_count:占2字节,本例为0x0016,转化为十进制为22,即说明常量池中有21个常量(只有常量池的计数是从1开始的,其它集合类型均从0开始),索引值为1~21。第0项常量具有特殊意义,如果某些指向常量池索引值的数据在特定情况下需要表达“不引用任何一个常量池项目”的含义,这种情况可以将索引值置为0来表示
constant_pool:表类型数据集合,即常量池中每一项常量都是一个表,共有14种(JDK1.7前只有11种)结构各不相同的表结构数据。这14种表都有一个共同的特点,即均由一个u1类型的标志位开始,可以通过这个标志位来判断这个常量属于哪种常量类型,常量类型及其数据结构如下表所示:

下表给出了常量池中11种数据类型的结构:

    常量 项目   类型 描述
CONSTANT_Utf8_info

tag

u1 值为1
length u2 UF-8编码的字符串占用的字节数
bytes u1 长度为length的UTF-8编码的字符串
CONSTANT_Integer_info tag u1 值为3
bytes u4 按照高位在前存储的int值
CONSTANT_Float_info tag u1 值为4
bytes u4 按照高位在前存储的float值
CONSTANT_Long_info tag u1 值为5
bytes u8 按照高位在前存储的long值
CONSTANT_Double_info tag u1 值为6
bytes u8 按照高位在前存储的double值
CONSTANT_Class_info tag u1 值为7
index u2 指向全限定名常量项的索引
CONSTANT_String_info tag u1 值为8
index u2 指向字符串字面量的索引
CONSTANT_Fieldref_info tag u1 值为9
index u2 指向声明字段的类或接口描述符CONSTANT_Class_info的索引项
index u2 指向字段名称及类型描述符CONSTANT_NameAndType_info的索引项
CONSTANT_Methodref_info tag u1 值为10
index u2 指向声明方法的类描述符CONSTANT_Class_info的索引项
index u2 指向方法名称及类型描述符CONSTANT_NameAndType_info的索引项
CONSTANT_InrerfaceMethodref_info tag u1 值为11
index u2 指向声明方法的接口描述符CONSTANT_Class_info的索引项
index u2 指向方法名称及类型描述符CONSTANT_NameAndType_info的索引项
CONSTANT_NameAndType_info tag u1 值为12
index u2 指向字段或方法名称常量项目的索引
index u2 指向该字段或方法描述符常量项的索引

    这11种常量类型各自均有自己的结构。在CONSTANT_Class_info型常量的结构中有一项name_index属性,该常属性中存放一个索引值,指向常量池中一个CONSTANT_Utf8_info类型的常量,该常量中即保存了该类的全限定名字符串。而CONSTANT_Fieldref_info、CONSTANT_Methodref_info、CONSTANT_InterfaceMethodref_info型常量的结构中都有一项index属性,存放该字段或方法所属的类或接口的描述符CONSTANT_Class_info的索引项。另外,最终保存的诸如Class名、字段名、方法名、修饰符等字符串都是一个CONSTANT_Utf8_info类型的常量,也因此,Java中方法和字段名的最大长度也即是CONSTANT_Utf8_info型常量的最大长度,在CONSTANT_Utf8_info型常量的结构中有一项length属性,它是u2类型的,即占用2个字节,那么它的最大的length即为65535。因此,Java程序中如果定义了超过64KB英文字符的变量或方法名,将会无法编译。

譬如utf-8类型的表结构数据

譬如fieldref类型的表结构数据

譬如class类型的表结构数据

譬如nameandtype类型的表结构数据

ps:什么是描述符?
成员变量(包括静态成员变量和实例变量) 和方法都有各自的描述符。 
对于字段而言,描述符用于描述字段的数据类型; 
对于方法而言,描述符用于描述字段的数据类型、参数列表、返回值。
在描述符中,基本数据类型用大写字母表示,对象类型用“L对象类型的全限定名”表示,数组用“[数组类型的全限定名”表示。 
描述方法时,将参数根据上述规则放在()中,()右侧按照上述方法放置返回值。而且参数之间无需任何符号。

访问标志(2字节)

常量池之后的数据结构是访问标志(access_flags),这个标志主要用于识别一些类或接口层次的访问信息,主要包括:

  • 是否final
  • 是否public,否则是private
  • 是否是接口
  • 是否可用invokespecial字节码指令
  • 是否是abstact
  • 是否是注解
  • 是否是枚举

 access_flags一共有16个标志位可以使用,当前只定义了其中8个(JDK1.5增加后面3种),没有使用到标志位一律为0。

类索引、父类索引和接口索引集合

这三项数据主要用于确定这个类的继承关系。
其中类索引(this_class)和父类索引(super_class)都是一个u2类型的数据,而接口索引(interface)集合是一组u2类型的数据。(多实现单继承)
类索引(this_class),用于确定这个类的全限定名,占2字节
父类索引(super_class),用于确定这个类父类的全限定名(Java语言不允许多重继承,故父类索引只有一个。除了java.lang.Object类之外所有类都有父类,故除了java.lang.Object类之外,所有类该字段值都不为0),占2字节
接口索引计数器(interfaces_count),占2字节。如果该类没有实现任何接口,则该计数器值为0,并且后面的接口的索引集合将不占用任何字节,
接口索引集合(interfaces),一组u2类型数据的集合。用来描述这个类实现了哪些接口,这些被实现的接口将按implements语句(如果该类本身为接口,则为extends语句)后的接口顺序从左至右排列在接口的索引集合中
this_class、super_class与interfaces按顺序排列在访问标志之后,它们中保存的索引值均指向常量池中一个CONSTANT_Class_info类型的常量,通过这个常量中保存的索引值可以找到定义在CONSTANT_Utf8_info类型的常量中的全限定名字符串

字段表集合

fields_count:字段表计数器,即字段表集合中的字段表数据个数,占2字节。本测试类其值为0x0001,即只有一个字段表数据,也就是测试类中只包含一个变量(不算方法内部变量)
fields:字段表集合,一组字段表类型数据的集合。字段表用于描述接口或类中声明的变量,包括类级别(static)和实例级别变量,不包括在方法内部声明的变量
在Java中一般通过如下几项描述一个字段:字段作用域(public、protected、private修饰符)、是类级别变量还是实例级别变量(static修饰符)、可变性(final修饰符)、并发可见性(volatile修饰符)、可序列化与否(transient修饰符)、字段数据类型(基本类型、对象、数组)以及字段名称。在字段表中,变量修饰符使用标志位表示,字段数据类型和字段名称则引用常量池中常量表示。

类型

名称

数量

说明

u2

access_flags

1

修饰符标记位

u2

name_index

1

代表字段的简单名称,占2字节,是一个对常量池的引用 

u2

descriptor_index

1

代表字段的类型,占2个字节,是一个对常量池的引用

u2

attributes_count

1

属性计数器

attribute_info

attributes

attributes_count

属性表集合

字段表包含的固定数据项到descriptor_index结束,之后跟随一个属性表集合用于存储一些附加信息。 

字段表集合中不会列出从父类或父接口中继承的字段,但是可能列出原本Java代码之中不存在的字段,如:内部类为了保持对外部类的访问性,自动添加指向外部类实例的字段。Java语言中字段是不能重载的,2个字段无论数据类型、修饰符是否相同,都不能使用相同的名称;但是对于字节码,只要字段描述符不同,字段重名就是合法的。

方法表集合

methods_count:方法表计数器,即方法表集合中的方法表数据个数。占2字节,其值为0x0002,即测试类中有2个方法
methods:方法表集合,一组方法表类型数据的集合。方法表结构和字段表结构一样。

2个字节为属性计数器,其值为0x0001,说明这个方法的属性表集合中有一个属性(详细说明见后面“属性表集合”)
属性名称为接下来2个字节0x0009,指向常量池中第9个常量,Code。
接下来4个字节为0x0000002F,表示Code属性值的字节长度为47。
接下来2个字节为0x0001,表示该方法的操作数栈的深度最大值为1。
接下来2个字节依然为0x0001,表示该方法的局部变量占用空间为1。
接下来4个字节为0x00000005,则紧接着的5个字节0x2AB70001B1为该方法编译后生成的字节码指令。
接下来2个字节为0x0000,说明Code属性异常表集合为空。
接下来2个字节为0x0002,说明Code属性带有2个属性,
接下来2个字节0x000A即为Code属性第一个属性的属性名称,指向常量池中第10个常量:LineNumberTable。
接下来4个字节为0x00000006,表示LineNumberTable属性值所占字节长度为6。
接下来2个字节为0x0001,line_number_table中只有一个line_number_info表,start_pc为0x0000,line_number为0x0003,LineNumberTable属性结束。
接下来2位0x000B为Code属性第二个属性的属性名,指向常量池中第11个常量:LocalVariableTable。该属性值所占的字节长度为0x0000000C=12。
接下来2位为0x0001,说明local_variable_table中只有一个local_variable_info表,按照local_variable_info表结构,start_pc为0x0000,length为0x0005,name_index为0x000C,指向常量池中第12个常量:this,descriptor_index为0x000D,指向常量池中第13个常量:LTestClass;,index为0x0000。

ps:

如果子类没有重写父类的方法,方法表集合中就不会出现父类方法的信息;有可能会出现由编译器自动添加的方法(如:最典型的<init>,实例类构造器)在Java语言中,重载一个方法除了要求和原方法拥有相同的简单名称外,还要求必须拥有一个与原方法不同的特征签名(,由于特征签名不包含返回值,故Java语言中不能仅仅依靠返回值的不同对一个已有的方法重载;但是在Class文件格式中,特征签名即为方法描述符,只要是描述符不完全相同的2个方法也可以合法共存,即2个除了返回值不同之外完全相同的方法在Class文件中也可以合法共存。

注意:Java代码的方法特征签名只包括方法名称、参数顺序、参数类型。  而字节码的特征签名还包括方法返回值和受异常表。

属性表集合

起始2个字节为0x0001,说明有一个类属性。
接下来2个字节为属性的名称,0x0010,指向常量池中第16个常量:SourceFile。
接下来4个字节为0x00000002,说明属性体长度为2字节。
最后2个字节为0x0011,指向常量池中第27个常量:TestClass.java,即这个Class文件的源码文件名为TestClass.java

与Class文件中其它数据项对长度、顺序、格式的严格要求不同,属性表集合不要求其中包含的属性表具有严格的顺序,并且只要属性的名称不与已有的属性名称重复,任何人实现的编译器可以向属性表中写入自己定义的属性信息。虚拟机在运行时会忽略不能识别的属性,为了能正确解析Class文件,虚拟机规范中预定义了虚拟机实现必须能够识别的9项属性(预定义属性已经增加到21项):

属性名称

使用位置

含义

Code

方法表

Java代码编译成的字节码指令

ConstantValue

字段表

final关键字定义的常量值

Deprecated

类文件、字段表、方法表

被声明为deprecated的方法和字段

Exceptions

方法表

方法抛出的异常

InnerClasses

类文件

内部类列表

LineNumberTale

Code属性

Java源码的行号与字节码指令的对应关系

LocalVariableTable

Code属性

方法的局部变量描述(局部变量作用域)

SourceFile

类文件

源文件名称

Synthetic

类文件、方法表、字段表

标识方法或字段是由编译器自动生成的

猜你喜欢

转载自blog.csdn.net/haoxin963/article/details/81980410