澳门新浦京娱乐游戏Java类文件结构

澳门新浦京娱乐游戏 4

Class文件

作为类或者接口信息的载体,每个Class文件都完整的定义了一个类。为了使Java程序可以“编写一次,处处运行”,Java虚拟机规范对Class文件进行了严格的规定。构成Class文件的基本数据单位是字节,这些字节之间不存在任何分隔符,这使得整个Class文件中存储的内容几乎全部是程序运行的必要数据,单个字节无法表示的数据由多个连续的字节来表示。

根据Java虚拟机规范,Class文件采用一种类似于C语言结构体的伪结构来存储数据,这种伪结构中只有两种数据类型:无符号数和表。Java虚拟机规范定义了u1u2u4u8来分别表示1个字节、2个字节、4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者是字符串。表是由多个无符号数或者其它表作为数据项构成的符合数据类型,表用于描述有层次关系的符合结构的数据,因此整个Class文件本质上就是一张表。在ClassAnalyzeru1u2u4u8分别对应于byteshortintlongClass文件被描述为如下Java类。

public class ClassFile {

    public U4 magic;                            // magic
    public U2 minorVersion;                     // minor_version
    public U2 majorVersion;                     // major_version
    public U2 constantPoolCount;                // constant_pool_count
    public ConstantPoolInfo[] cpInfo;           // cp_info
    public U2 accessFlags;                      // access_flags
    public U2 thisClass;                        // this_class
    public U2 superClass;                       // super_class
    public U2 interfacesCount;                  // interfaces_count
    public U2[] interfaces;                     // interfaces
    public U2 fieldsCount;                      // fields_count
    public FieldInfo[] fields;                  // fields
    public U2 methodsCount;                     // methods_count
    public MethodInfo[] methods;                // methods
    public U2 attributesCount;                  // attributes_count
    public BasicAttributeInfo[] attributes;     // attributes

}

  1、魔数和Class文件的版本

  每个Class文件的头四个字节称为魔数,它的唯一作用就是确定这个文件是否为一个能被虚拟机接受的Class文件。很多文件存储标准都是使用魔数而不是扩展名来进行识别,主要是基于安全方面考虑。紧接着魔数的四个字节存储的是Class文件的版本号:第五和第六是此版本号,第七和第八是主版本号,以下面的类为例:

public class TestClass
{
    private String m;
    public String test()
    {
        return m;
    }
}

  查看生成的class文件:

  澳门新浦京娱乐游戏 1

  可以看到代表次版本号的第五个和第六个字节为0x0000,主版本号为0x0032,也就是十进制的50。

Class文件中存储着Java虚拟机指令集和符号表以及若干辅助信息。它使用的是一种平台无关的字节码储存格式,不同的虚拟机实现都可以载入执行这种平台无关的字节码。Java虚拟机不与任何语言绑定,只与Class文件这种特定二进制文件格式关联,原则上任何语言都可以编译成Class文件在Java虚拟机上运行。

最近在写一个私人项目,名字叫做ClassAnalyzerClassAnalyzer的目的是能让我们对Java Class文件的设计与结构能够有一个深入的理解。主体框架与基本功能已经完成,还有一些细节功能日后再增加。实际上JDK已经提供了命令行工具javap来反编译Class文件,但本篇文章将阐明我实现解析器的思路。

三、字节码指令

  Java虚拟机的指令由一个字节长度的、代表着某种特定操作含义的数字(称为操作码)以及跟随其后的零至多个代表操作所需参数而构成。由于Java虚拟机采用面向操作数栈而不是寄存器架构,所以大多数的指令都不包含操作数,只有一个操作码。由于限制了Java虚拟机操作码的长度为一个字节,这意味着指令集的操作码总数不可能超过256条。

三、字节码指令


Java虚拟机的指令是由一个字节长度的、代表着某种特定操作含义的数字(操作码,Opcode)以及跟随其后的零个至多个代表此操作所需要的参数(操作数,Operands)构成。

常用指令:

  • 加载存储指令,将数据在栈帧中的局部变量表和操作数栈之间来回传输
  • 运算指令,对两个操作数栈上的值进行某种特定运算,并把结果重新写入操作栈,包括加减乘除逻辑与或非
  • 类型转换指令,将两种不同的数值类型进行相互转换,这些转换一般用于实现用户代码中的显式类型转换操作,或者用来处理字节码指令集中数据类型相关指令代码无法与数据类型一一对应的问题。另,类型转换指令永远不可能导致虚拟机运行异常。
  • 对象创建与访问指令,创建对象数组访问对象等
  • 操作数栈管理指令,Java虚拟机提供了一些用于直接操作操作数栈的指令,包括入栈,出栈,栈顶端两个数据交换
  • 控制转移指令,可以让Java虚拟机有条件或无条件地从指定的位置指令而不是控制转移指令的下一条指令继续执行程序,包括条件分支ifxxx,复合条件分支,无条件分支goto等。
  • 方法调用和返回指令,调用对象实例方法,调用接口方法,调用类方法,运行时动态解析出调用点限定符所引用的方法,返回指令根据返回值类型区分
  • 异常处理指令,Java程序中显式抛出异常的操作(throw)都是由athrow指令实现,异常处理不是字节码指令实现,而是采用异常表实现
  • 同步指令,方法级同步和方法内部一段指令序列的同步(通过管程Monitor支持),执行线程要求先持有管程,然后才能执行方法,当方法执行完成后释放管程;方法执行期间,执行线程持有管程,任何一个线程都无法再获取同一个管程

如何解析

组成Class文件的各个数据项中,例如魔数、Class文件的版本等数据项、访问标志、类索引、父类索引,它们在每个Class文件中都占用固定数量的字节,在解析时只需要读取相应数量的字节。除此之外,需要灵活处理的主要包括4部分:常量池、字段表集合、方法表集合和属性表集合。字段和方法都可以具备自己的属性,Class本身也有相应的属性,因此,在解析字段表集合和方法表集合的同时也包含了属性表的解析。

常量池占据了Class文件很大一部分的数据,用于存储所有的常量信息,包括数字和字符串常量、类名、接口名、字段名和方法名等。Java虚拟机规范定义了多种常量类型,每一种常量类型都有自己的结构。常量池本身是一个表,在解析时有几点需要注意。

  • 每个常量类型都通过一个u1类型的tag来标识。
  • 表头给出的常量池大小(constantPoolCount)比实际大1,例如,如果constantPoolCount等于47,那么常量池中有46项常量。
  • 常量池的索引范围从1开始,例如,如果constantPoolCount等于47,那么常量池的索引范围为1~46。设计者将第0项空出来的目的是用于表达“不引用任何一个常量池项目”。
  • CONSTANT_Utf8_info型常量的结构中包含u1类型的tagu2类型的length和由lengthu1类型组成的bytes,这length字节的连续数据是一个使用MUTF-8Modified UTF-8)编码的字符串。MUTF-8UTF-8并不兼容,主要区别有两点:一是null字符会被编码成2字节(0xC00x80);二是补充字符是按照UTF-16拆分为代理对分别编码的,相关细节可以看这里(变种UTF-8)。

属性表用于描述某些场景专有的信息,Class文件、字段表和方法表都有相应的属性表集合。Java虚拟机规范定义了多种属性,ClassAnalyzer目前实现了对常用属性的解析。和常量类型的数据项不同,属性并没有一个tag来标识属性的类型,但是每个属性都包含有一个u2类型的attribute_name_indexattribute_name_index指向常量池中的一个CONSTANT_Utf8_info类型的常量,该常量包含着属性的名称。在解析属性时,ClassAnalyzer正是通过attribute_name_index指向的常量对应的属性名称来得知属性的类型。

字段表用于描述类或者接口中声明的变量,字段包括类级变量以及实例级变量。字段表的结构包含一个u2类型的access_flags、一个u2类型的name_index、一个u2类型的descriptor_index、一个u2类型的attributes_countattributes_countattribute_info类型的attributes。我们已经介绍了属性表的解析,attributes的解析方式与属性表的解析方式一致。

Class的文件方法表采用了和字段表相同的存储格式,只是access_flags对应的含义有所不同。方法表包含着一个重要的属性:Code属性。Code属性存储了Java代码编译成的字节码指令,在ClassAnalyzer中,Code对应的Java类如下所示(仅列出了类属性)。

public class Code extends BasicAttributeInfo {

    private short maxStack;
    private short maxLocals;
    private long codeLength;
    private byte[] code;
    private short exceptionTableLength;
    private ExceptionInfo[] exceptionTable;
    private short attributesCount;
    private BasicAttributeInfo[] attributes;
    ...

    private class ExceptionInfo {
        public short startPc;
        public short endPc;
        public short handlerPc;
        public short catchType;
          ...
    }
}

Code属性中,codeLengthcode分别用于存储字节码长度和字节码指令,每条指令即一个字节(u1类型)。在虚拟机执行时,通过读取code中的一个个字节码,并将字节码翻译成相应的指令。另外,虽然codeLength是一个u4类型的值,但是实际上一个方法不允许超过65535条字节码指令。

四、参考资料

  1、深入理解Java虚拟机

一、 Class类文件结构


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

1.1 无符号数

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

1.2 表

表是由多个无符号数或者其他表作为数据项构成的复合数据类型。表用于描述有层次关系的复合结构的数据,整个Class文件本质上就是一张表。Class中以_info结尾代表一张表。

代码实现

ClassAnalyzer的源码已放在了GitHub上。在ClassAnalyzer的README中,我以一个类的Class文件为例,对该Class文件的每个字节进行了分析,希望对大家的理解有所帮助。

  5、字段表集合

  字段表用于描述接口或类中声明的变量。字段包括类级变量以及实例变量,但不包括在方法内部声明的局部变量。可以包括的信息有:字段的作用域、是实例变量还是类变量(static
修饰符)、可变性(final)、并发可见性(volatile修饰)、是否可被序列化(transient修饰符)、字段数据类型、字段名称。上述的这些信息,各个修饰符都是布尔值。

二、Class字节码解析


Class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格的按照顺序紧凑的排列在Class文件中。当遇到占用8字节以上空间的数据项时,则会按照高位在前的方式分割成若干个8位字节进行存储。Class内部不包含任何分隔符,数据存储顺序数量都被严格限定,不允许任何改动。下面看看具体数据项的含义:

2.1 魔数(Magic Number)

每个Class文件的头4个字节称为魔数(Magic
Number),它唯一作用就是用来确定文件是否能被虚拟机接受。

很多文件存储标准中都用魔数进行身份标识,如图片gif,jpeg都在文件头部中存储着魔数。使用魔数而不是用扩展名来进行识别主要是基于安全考虑,因为扩展名可以被随意改动。

2.2 版本号

接下来的4个字节存储着Class文件的版本号,第五第六个字节为次版本号(Minor
Version),第七第八为主版本号(Major
Version)。版本号主要用于版本控制,高版本的JDK能向下兼容以前版本的Class文件,但不能运行以后版本的Class文件。

2.3 常量池入口

紧接着版本号之后的就是常量池入口,常量池入口后面还必须有一个u2数据项作为常量池容量计数器(因为常量池数量不固定)。

常量池是一个表类型的数据项,相当于Class文件的资源仓库,与Class文件其他项目关联最多,占用Class空间最大的数据项之一,且是第一个出现的表类型数据项目。

常量池主要存储两大类常量:字面量(Literal)和符号引用(Symbolic
References)

字面量相当于Java语言中的常量概念,比如字符串,声明为final的常量值。
符号引用则属于编译原理方面的概念包括三类常量:

  • 类和接口的全限定名(Fully Qualified Name)
  • 字段的名称和描述符(Descriptor)
  • 方法的名称和描述符

Class文件不会保存各个方法字段的最终内存布局信息,因为这些字段、方法和符号引用不经过运行期转换(动态连接)的话无法得到真正内存入口地址,也就无法被虚拟机使用。当虚拟机运行时,需要从常量池获得对应的符号引用,再在类创建或运行时解析翻译到具体的内存地址之中。

常量池中的每一项常量都是一个表(JDK1.7中有14种)。包括UTF-8编码的字符串表,整型字面量表,浮点型字面量表,长整型字面常量表,类和接口的符号引用表,字段符号引用表,类中的方法符号引用表,接口中方法符号引用表等等。这些表都会有各自不同的结构。

2.4 访问标志

常量池之后就是由两个字节代表的访问标识(access
flags)这些标识用于识别一些类或者接口层次的访问信息,包括这个Class是类还是接口;是否定义为public;是否定义为abstract类型;是否被final修饰。

2.5 类索引、父类索引、接口索引

访问标志位之后就是u2类型的类索引,父类索引和接口索引集合。Class文件由这三项数据确定这个类的继承关系。这三项数据(u2类型的索引值)各指向类型为CONSTANT_Class_info的类描述符常量。

2.6 字段表集合

字段表用于描述接口或者类中声明的变量。字段(field)包括类级变量以及实例级变量,但不包括在方法内部声明的局部变量。字段表中字段的各种描述信息(作用域比如public,private,是否被final,static修饰,是否可序列化等)均使用标志位表示,名称则引用常量池中的常量来描述。

2.7 方法表集合

在方法表中,方法的描述和字段的描述基本一致,依次包括访问标志(access_flags)、名称索引(name_index)、描述符索引(descriptor_index)、属性表集合(attributes)几项。

方法中的代码经过编译器编译成字节码指令后存放在方法属性表集合中一个名为“Code”的属性里面。

如果父类方法在子类中没有被重写,方法表集合中就不会出现来自父类的方法信息。

2.8 属性表集合

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

为了能正确解析Class文件,在Java SE
7中预定义了21项属性,虚拟机在运行时会忽略他不认识的属性。

  3、运算指令

  运算或算术指令用于对两个操作数栈上的值进行某种特定运算,并把结果重新存入到操作栈顶。

  2、常量池

  紧接着主版本号之后是常量池入口,常量池可以理解为Class文件之中的资源仓库,它是Class文件结构中与其他项目关联最多的数据类型,也是占用Class文件空间最大的数据项目之一,同时还是在Class文件中第一个出现的表类型数据项目。由于常量池中常量的数量不是固定的,所以在常量池的入口需要放置一项u2类型的数据u,代表常量池容量计数值,这个容量技术值是从1开始而不是从0开始的。这样做的目的在于满足后面某些指向常量池的索引值的数据在特定情况下需要表达“不引用任何一个常量池项目”的含义,这种情况就可以把索引值置为0来表示。上图中常量池容量为0x0016,即十进制中的22,这就表示常量池中有21项常量。

  常量池中主要存放两大类常量:字面量和符号引用。

  字面量近似于Java的常量,如文本字符串、声明为final的常量值,而符号引用则属于编译原理方面的概念,包括:类和接口的全限定名、字段的名称和描述符、方法的名称和描述符。

  Java代码在编译时没有连接的步骤,而是在虚拟机加载Class文件的时候进行动态连接。当虚拟机运行时,需要从常量池获得对应的符号引用,再在类创建时或运行时解析、翻译到具体的内存地址之中。

  常量池的每一项常量都是一个表,在JDK
1.7共有14种结构各不相同的表结构数据。这些表有一个共同的特点,就是表开始的第一位是一个u1类型的标志位。

  回头看上面的表,常量池的第一项常量,它的标志位是0x07,属于CONSTANT_Class_info类型,此类型的常量代表一个类或者接口的符号引用。

  在JDK的bin目录中,有一个专门用于分析Class文件字节码的工具:javap,上述代码生成的Class文件使用javap工具的-verbose参数输出如下:

  澳门新浦京娱乐游戏 2

  可见,计算机已经帮我们把整个常量池的21项常量都计算出来了。

  1、字节码与数据类型

  对于大部分与数据类型相关的字节码指令,它们的操作码助记符中都有特殊的字符来表示专门为哪种数据类型服务:i代表队int类型的数据操作,l代表long,s代表short,b代表byte,c代表char,f代表float,d代表double,a代表reference。也有一些指令助记符没有明确的指明操作类型的字母。还有一些指令与数据类型无关。Java虚拟机的指令集对于特殊的操作只提供了有限的类型相关指令去支持它,指令集被设计成非完全独立的,有一些单独的指令可以在必要的时候用来将一些不支持的类型转换为可被支持的类型。

  2、加载和存储指令

  加载和存储指令用于将数据在栈帧中的局部变量表和操作数栈之间来回传输。

  4、类索引、父类索引和接口索引

  类索引和父类索引都是一个u2类型的数据,而接口索引集合是一组u2类型的数据的集合,Class文件中的这个三个数据爱确定类的继承关系。类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名。由于Java语言不允许多重继续,所以父类索引只有一个。接口索引集合就用来描述这个类实现了哪些接口,这些被实现的接口将按照Implements语句后的接口顺序从左到右排列在索引集合中。

一、概述

  实现语言无关性的基础是虚拟机和字节码存储格式。Java虚拟机不和包括Java在内的任何语言绑定,只与”Class文件”这种特定的二进制文件所关联,Class文件中包含了Java虚拟机指令集合符号表以及若干其它辅助信息。Java虚拟机作为一个通用的、机器无关的执行平台,任何其他语言都可以将其作为语言的产品交付媒介。

  澳门新浦京娱乐游戏 3

  7、属性表集合

  在Class文件、字段表、方法表都可以携带自己的属性表集合,用于描述某些场景专有的信息。

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

  Exceptions属性:其作用是列举出可能抛出的受查异常,也就是方法描述时在throws关键字后面列举的异常。

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

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

  3、访问标志

  在常量池结束之后,有两个字节代表访问标志,这个标志用于识别一些类和接口层次的访问信息,包括这个Class是类还是接口,是否为public类型,是否定义为abstract类型,如果是类的话,是否声明为final类型等。

  4、类型转换指令

  类型转换指令可以将两种不同的数值类型进行相互转换,这些转换操作一般用于实现用户代码中的显示类型转换操作,或者用来处理上面提到的字节码指令集中数据类型相关指令无法与数据类型一一对应的问题。

  Java虚拟机直接支持以下数值类型的宽化类型转换(小范围类型向大范围类型的安全转换):

  • int类型到long、float或者double类型
  • long类型到float、double类型
  • float类型到double类型

  处理窄化类型转换时,则必须显式地使用转换指令来完成。

  5、异常处理命令

  在Java程序中显示抛出异常的操作都由athrow指令来实现,除了用throw语句显示抛出异常之外,Java虚拟机规范还规定了许多运行时异常会在其他Java虚拟机指令检测到异常状况时自动抛出。

  在Java虚拟机中,处理异常(catch语句)不是由字节码指令来实现的,才是采用异常表来完成的。

二、Class类文件的结构

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

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

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

  :由多个无符号数和其他表作为数据项构成的复合类型的数据类型,所有的表都习惯地以”_info”结尾,表用于描述有层次关系的复合结构的数据,整个Class文件本质上就是一张表。,它由下表所示的数据项构成:

  澳门新浦京娱乐游戏 4

  下面看看表中各个数据项的具体含义:

  6、方法表集合

  方法表用于描述方法,包括的信息有:访问标志、名称索引、描述符索引、属性表集合等。方法里的Java代码,经过编译器编译成字节码指令后,存放在方法属性表集合中一个名为Code的属性里,属性表是Class文件格式中最具扩展性的一中数据项目。

  在Java语言中,要重载(Overload)一个方法,除了要与原方法具有相同的简单名称之外,还要求必须拥有一个与原方法不同的特征签名(特征签名就是一个方法中各个参数在常量池中的字段符号引用的集合),Java代码的方法特征签名只包括了方法名称、参数顺序以及参数类型,也就是因为返回值不包含在特征签名中,因此Java语言无法仅仅依靠返回值的不同来对一个已有方法进行重载。

You can leave a response, or trackback from your own site.

Leave a Reply

网站地图xml地图