您的位置:首页 > 其它

笔记-类文件结构

2017-08-28 03:49 190 查看
1 Class类文件结构

(1)class文件是一组以8位字节为基础单位当二进制流,没有任何的分隔符

(2)u1、u2等来描述一个字节、两个字节的无符号数;无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成当字符串值

(3)表是由多个无符号数或者其他表作为数据项构成的复合数据类型,命名习惯性以“_Info”结尾,整个class文件本质上就是一个表,如6-1所示

1.1  魔数和Class文件版本

    magic,4个字节表示,唯一作用是确定这个文件是否为一个可被虚拟机结合搜当Class的文件。Class文件当魔数为0xCAFEBABE。

    minor_version,次版本号;major_version,主版本号。

1.2 常量池

    Class文件中的资源仓库,是Class文件中的第一个表类型数据项目。

   constant_pool_count,常量池的容量计数值,该值从1开始而不是从0开始,将第0项空置出来是为了满足后面某些指向常量池的索引值的数据在特定情况下需要表达“不引用用任何一个常量池项目”的含义;constant_pool,常量池内容。

    常量池主要存放两大类常量字面量(Liternal)符号引用(Symbilic References);

    字面量:较接近于java语言层面的常量概念,如文本字符串、声明为final当常量值等。

    符号引用:属于编译原理方面的概念:

    (1)类和接口的全限定名(Fully Qualified Name)

    (2)字段当名称和描述符(Descriptor)

    (3)方法的名称和描述符 

    Java代码在进行Javac编译的时候,并不像C和C++那样有“连接”这一步骤,而是在虚拟

机加载Class文件的时候进行动态连接。也就是说,在Class文件中不会保存各个方法、字段

的最终内存布局信息,因此这些字段、方法的符号引用不经过运行期转换的话无法得到真正

的内存入口地址,也就无法直接被虚拟机使用。当虚拟机运行时,需要从常量池获得对应的

符号引用,再在类创建时或运行时解析、翻译到具体的内存地址之中。

    常量池中的每一项常量都是一个表

    每个表的表结构



    1.2.1 CONSTANT_Class_info型常量的结构



    tag是标志位,如6-3所示,用于区分常量类型。name_index是一个索引值,如0x0002则表示常量池中的第二项常量。

    

    1.2.2 CONSTANT——Utf8_info型常量结构

    


    Class文件的中的方法、字段等都需要引用CONSTANT_Utf8_info型常量来描述名称,所以该表内容的长度也就是java中对应方法、字段等的名称最大长度,所以如果超过u2可表示的最大长度,将会无法编译。

    1.2.3 常量池中当十四种常量项当结构总表





1.3 访问标志

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

     


1.4 类索引、父类索引与接口索引集合

    类索引(this_class)和父类索引(super_class)都是一个u2类型的数据,而接口索引集合(interfaces)是一组u2类型的数据的集合,Class文件中由这三项数据来确定这个类的继承关系。类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名。    

    除了java.lang.Object之外,所有的java类的父类索引都不为0

    类索引、父类索引与接口索引都按顺序排列在访问标志之后,类索引和父类索引用两个u2类型的索引值表示,他们各自指向一个类型为CONSTANT_Class_info的类描述符常量,通过CONSTANT_Class_info类型的常量中的索引值可以找到定义在CONSTANT_Utf8_info类型当常量中的全限定名字符串。

1.5 字段表集合

    字段表(field_info)用于描述接口或者类中声明的变量。字段包括类级变量以及实例级变量,但不包括方法内部声明的局部变量。

    字段表结构:



字段修饰符放在access_flags项目中,其设置的标志位和含义见表6-9



    

     name_index和descroptor_index都是常量池的引用,分别代表着字段的简单名称以及字段和方法的描述符。

    字段和方法的描述符的作用是用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值。

    用描述符来描述方法时,按照先参数列表,后返回值的顺序描述,参数列表按照参数的严格顺序放在一组小括号“()”之内。如方法voidinc()的描述符为“()V”,方法java.lang.String

toString()的描述符为“()Ljava/lang/String;”,方法intindexOf(char[]source,intsourceOffset,intsourceCount,char[]target,inttargetOffset,inttargetCount,int fromIndex)的描述符为“([CII[CIII)I”。

    描述符识别字符的含义:



    字段表都包含的固定数据项目到descriptor_index为止就结束了,不过在descriptor_index之后跟随着一个属性表集合用于存储一些额外的信息,字段都可以在属性表中描述零多项的额外信息。

    字段表集合中不会列出从超类或者父接口中继承而来的字段,但有可能列出原本Java代码之中不存在的字段,譬如在内部类中为了保持对外部类的访问性,会自动添加指向外部类实例的字段。另外,在Java语言中字段是无法重载的,两个字段的数据类型、修饰符不管是否相同,都必须使用不一样的名称,但是对于字节码来讲,如果两个字段的描述符不一致,那字段重名就是合法的。

1.6 方法表集合

    方法表结构

    




    方法里的Java代码,经过编译器编译成字节码指令后,存放在方法属性表集合中一个名为“Code”的属性里面,属性表作为Class文件格式中最具扩展性的一种数据项目

    与字段表相似,如果父类方法在子类中没有被重写,方法表集合中就不会出现来自父类的方法信息,但同样有可能出现由编译器自动添加的方法,比如“<clinit>”方法和“<init>”方法。

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

1.7 属性表集合

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

    虚拟机规范预定义的属性





    对于每个属性,它的名称都要从常量池中引用一个CONSTANT_Utf8_info类型的常量来表示,而属性的结构则是完全自定义的,只需要通过一个u4长度属性区说明属性值所占的位数即可。结构如图



1.7.1 Code属性

    Java程序方法体中的代码经过javac编译器处理后,最终变成字节码指令存储在Code属性内。Code属性必须存在于方法表的属性集合中,但方法表的并非必须存在这个属性。






    attribute_name_index:指向CONSTANT_Utf8_info型常量的索引,固定值为“Code”

    attribute_length:属性值长度,即整个属性表长度减去(2+4)

    max_stack:操作数栈深度的最大值,虚拟机运行时根据这个值来分配栈帧

    max_locals:代表局部变量表所需要的存储空间,Solt是最小单位,对于byte、char、float、int、short、boolean和returnAddress等长度不超过32位的数据类型,每个局部变量占用1个Slot,而double和long这两种64位的数据类型则需要两个Slot来存放。方法参数(包括实例方法中的隐藏参数“this”)、显式异常处理器的参数(Exception Handler Parameter,就是try-catch语句中catch块所定义的异常)、方法体中定义的局部变量都需要使用局部变量表来存放

    code_length和code:用来存储Java源程序编译后生成的字节码指令

    Code,方法体里面的Java代码;Metadata(元数据),包括类、字段、方法定义及其他信息

异常表并非Code中必须存在的,它的结构如下图所示:



        

1.7.2 Exceptions属性

    Exceptions属性是在方法表中与Code属性平级的一个属性。Exception属性的作用的列举出方法中可能抛出的受检异常,也就是方法描述时在throws关键字后面列举的异常。表结构如图所示:



    number_of_exceptions:可能抛出多少种受检异常

    exception_index_table:一个指向常量池中CONSTANT_Class_info型常量的索引,代表该种受检异常的类型

1.7.3 LIneNumberTable属性

    LineNumberTable属性用于描述Java源码行号与字节码行号(字节码的偏移量)之间的对应关系。它并不是运行时必需的属性,但默认会生成到Class文件之中,可以在Javac中分别使用-g:none或-g:lines选项来取消或要求生成这项信息。如果选择不生成LineNumberTable属性,对程序运行产生的最主要的影响就是当抛出异常时,堆栈中将不会显示出错的行号,并且在调试程序的时候,也无法按照源码行来设置断点。



    line_number_table中的属性:

    start_pc:字节码行号

    line_number:java源代码行号

1.7.4 LocalVariavleTable

    用于描述帧栈中局部变量表中的变量与Java源码中定义的变量之间的关系,非必须属性,但会默认生成。但失去这个信息时,最大的影响就是当其他人引用这个方法时,所有的参数名称都将会丢失,但不影响程序的运行。

   




    start_pc和length属性分别代表这个局部变量的生命周期开始的字节码偏移量及其作用范围覆盖的长度,两者结合起来就是这个局部变量的作用域范围

    name_index和descriptor_index都是指向常量池中CONSTANT_Utf8_info型常量的索引,分别代表了局部变量的名称以及这个局部变量的描述符。index是这个局部变量在栈帧局部变量表中Slot的位置。当这个变量数据类型是64位类型时(double和long),它占用的Slot为index和index+1两个。

1.7.5 SourceFile属性

    用于记录生成这个Class文件的源码文件的名称。该属性可选。若不选,当抛出异常时,堆栈中将不会显示出错误代码所属文件名



    sourcefile_index数据项是指向常量池中CONSTANT_Utf8_info型常量的索引,常量值是源码文件的文件名。

1.7.6 ConstantValue属性

    用于通知虚拟机自动为静态变量赋值,只有被static修饰的变量才可以使用这项属性。对于非static类型的变量(也就是实例变量)的赋值是在实例构造器<init>方法中进行的;而对于类变量,则有两种方式可以选择:在类构造器<clinit>方法中或者使用ConstantValue属性。目前Sun Javac编译器的选择是:如果同时使用final和static来修饰一个变量(按照习惯,这里称“常量”更贴切),并且这个变量的数据类型是基本类型或者java.lang.String的话,就生成ConstantValue属性来进行初始化,如果这个变量没有被final修饰,或者并非基本类型及字符串,则将会选择在<clinit>方法中进行初始化。



    


ConstantValue属性是一个定长属性,它的attribute_length数据项值必须固定为2。constantvalue_index数据项代表了常量池中一个字面量常量的引用,根据字段类型的不同,字面量可以是CONSTANT_Long_info、CONSTANT_Float_info、CONSTANT_Double_info、CONSTANT_Integer_info、CONSTANT_String_info常量中的一种。

1.7.7 InnerClass属性

    用于记录内部类和宿主类之间的关联





1.7.8 Deprecated及Syntheic属性

    都属于标志类型的布尔属性,只存在有或者没有的区别,没有属性值的概念

    Deprecated用于表示某个类、某个字段或者方法,已经被程序作者定为不再推荐使用,可以在代码中使用注解@eprecated声明。

    Syntheic代表此字段或者方法并不是由java源码直接产生的,而是由自动编译器自行添加的。标识一个类、字段或者方法是编译器自动产生的,也可以设置它们访问标志中的ACC_SYNTHETIC标志位,其中最典型的例子就是Bridge Method。所有由非用户代码产生的类、方法及字段都应当至少设置Synthetic属性和ACC_SYNTHETIC标志位中的一项,唯一的例外是实例构造器“<init>”方法和类构造器“<clinit>”方法。



1.7.9 StackMapTable属性

    它是一个复杂的变长属性,位于Code属性的属性表中。这个属性会在虚拟机类加载的字节码验证阶段被新类型检查验证器(Type Checker)使用(见7.3.2节),目的在于代替以前比较消耗性能的基于数据流分析的类型推导验证器。新的验证器在同样能保证Class文件合法性的前提下,省略了在运行期通过数据流分析去确认字节码的行为逻辑合法性的步骤,而是在编译阶段将一系列的验证类型(Verification Types)直接记录在Class文件之中,通过检查这些验证类型代替了类型推导过程,从而大幅提升了字节码验证的性能。

    StackMapTable属性中包含零至多个栈映射帧(Stack Map Frames),每个栈映射帧都显式或隐式地代表了一个字节码偏移量,用于表示该执行到该字节码时局部变量表和操作数栈的验证类型。类型检查验证器会通过检查目标方法的局部变量和操作数栈所需要的类型来确定一段字节码指令是否符合逻辑约束。

1.7.10 Signature属性

    可选的定长属性,可以出现于类、字段表和方法表结构的属性表中。任何类、接口、初始化方法或成员的泛型签名如果包含了类型变量(Type Variables)或参数化类型(Parameterized Types),则Signature属性会为它记录泛型签名信息。



    其中signature_index项的值必须是一个对常量池的有效索引。常量池在该索引处的项必须是CONSTANT_Utf8_info结构,表示类签名、方法类型签名或字段类型签名。

1.7.11 BootstrapMethods属性

    复杂的变长属性,位于类文件的属性表中,用于保存invokedynamic指令引用的引导方法限定符。在一个类文件中最多只有一个

    




2 字节码指令简介

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

成。由于Java虚拟机采用面向操作数栈而不是寄存器的架构(这两种架构的区别和影响将在第8章中探讨),所以大多数的指令都不包含操作数,只有一个操作码。

2.1 字节码与数据类型

    指令集将会故意被设计成非完全独立的(Java虚拟机规范中把这种特性称为“Not Orthogonal”,即并非每种数据类型和每一种操作都有对应的指令)。有一些单独的指令可以在必要的时候用来将一些不支持的类型转换为可被支持的类型。    

    大部分的指令都没有支持整数类型byte、char和short,甚至没有任何指令支持boolean类型。编译器会在编译期或运行期将byte和short类型的数据带符号扩展(Sign-Extend)为相应的int类型数据,将boolean和char类型数据零位扩展(Zero-Extend)为相应的int类型数据。与之类似,在处理boolean、byte、short和char类型的数组时,也会转换为使用对应的int类型的字节码指令来处理。因此,大多数对于boolean、byte、short和char类型数据的操作,实际上都是使用相应的int类型作为运算类型(ComputationalType)。

    




2.2 加载和存储指令

    用于将数据在帧栈中的局部变量表和操作数栈之间来回传输

    存储数据的操作数栈和局部变量表主要就是由加载和存储指令进行操作,除此之外,还有少量指令,如访问对象的字段或数组元素的指令也会向操作数栈传输数据。

将一个局部变量加载到操作栈:iload、iload_<n>、lload、lload_<n>、fload、fload_<n>、dload、dload_<n>、aload、aload_<n>。

将一个数值从操作数栈存储到局部变量表:istore、istore_<n>、lstore、lstore_<n>、fstore、fstore_<n>、dstore、dstore_<n>、astore、astore_<n>。

将一个常量加载到操作数栈:bipush、sipush、ldc、ldc_w、ldc2_w、aconst_null、iconst_m1、iconst_<i>、lconst_<l>、fconst_<f>、dconst_<d>。

扩充局部变量表的访问索引的指令:wide。


2.3 运算指令

    用于对两个操作数栈上的值进行某种特定运算,并把结果重新放入操作栈顶

    由于没有直接支持byte、short、char和boolean类型的算术指令,对于这类数据的运算,应使用int类型的指令代替,整数和浮点数的算数指令在溢出和被零除的时候也有各自不同的行为表现

    只有除法指令以及求余指令中当出现除数为零时会出现ArithmeticException,其余任何整形数运算场景都不应该出现运行时异常

加法指令:iadd、ladd、fadd、dadd。

减法指令:isub、lsub、fsub、dsub。

乘法指令:imul、lmul、fmul、dmul。

除法指令:idiv、ldiv、fdiv、ddiv。

求余指令:irem、lrem、frem、drem。

取反指令:ineg、lneg、fneg、dneg。

位移指令:ishl、ishr、iushr、lshl、lshr、lushr。

按位或指令:ior、lor。

按位与指令:iand、land。

按位异或指令:ixor、lxor。

局部变量自增指令:iinc。

比较指令:dcmpg、dcmpl、fcmpg、fcmpl、lcmp。


2.4 类型转换指令

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

Java虚拟机直接支持(即转换时无需显式的转换指令)以下数值类型的宽化类型转换(Widening Numeric Conversions,即小范围类型向大范围类型的安全转换):

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

long类型到float、double类型。

float类型到double类型。


相对的,处理窄化类型转换(Narrowing Numeric Conversions)时,必须显式地使用转换指令来完成,这些转换指令包括:i2b、i2c、i2s、l2i、f2i、f2l、d2i、d2l和d2f。窄化类型转换可能会导致转换结果产生不同的正负号、不同的数量级的情况,转换过程很可能会导致数值的精度丢失。

2.5 对象创建与访问指令

    虽然类实例和数组都是对象,但Java虚拟机对这两者的创建和操作使用了不同的字节码指令

创建类实例的指令:new。

创建数组的指令:newarray、anewarray、multianewarray。

访问类字段(static字段,或者称为类变量)和实例字段(非static字段,或者称为实例变

量)的指令:getfield、putfield、getstatic、putstatic。

把一个数组元素加载到操作数栈的指令:baload、caload、saload、iaload、laload、

faload、daload、aaload。

将一个操作数栈的值存储到数组元素中的指令:bastore、castore、sastore、iastore、

fastore、dastore、aastore。

取数组长度的指令:arraylength。

检查类实例类型的指令:instanceof、checkcast。


2.6操作数栈管理指令

    如同操作一个普通数据结构中的堆栈那样,Java虚拟机提供了一些用于直接操作操作数栈的指令,包括:

将操作数栈的栈顶一个或两个元素出栈:pop、pop2。

复制栈顶一个或两个数值并将复制值或双份的复制值重新压入栈顶:dup、dup2、

dup_x1、dup2_x1、dup_x2、dup2_x2。

将栈最顶端的两个数值互换:swap。


2.7 控制指令转移

    Java虚拟机有条件或无条件地从指定的位置指令而不是控制转移指令的下一条指令继续执行程序

条件分支:ifeq、iflt、ifle、ifne、ifgt、ifge、ifnull、ifnonnull、if_icmpeq、if_icmpne、

if_icmplt、if_icmpgt、if_icmple、if_icmpge、if_acmpeq和if_acmpne。

复合条件分支:tableswitch、lookupswitch。

无条件分支:goto、goto_w、jsr、jsr_w、ret。 


2.8 方法调用和返回指令

    方法调用指令与数据类型无关,而方法返回指令是根据返回值的类型区分的,包括ireturn(当返回值是boolean、byte、char、short和int类型时使用)、lreturn、freturn、dreturn和areturn,另外还有一条return指令供声明为void的方法、实例初始化方法以及类和接口的类初始化方法使用。

invokevirtual指令用于调用对象的实例方法,根据对象的实际类型进行分派(虚方法分派),这也是Java语言中最常见的方法分派方式。

invokeinterface指令用于调用接口方法,它会在运行时搜索一个实现了这个接口方法的对象,找出适合的方法进行调用。

invokespecial指令用于调用一些需要特殊处理的实例方法,包括实例初始化方法、私有方法和父类方法。

invokestatic指令用于调用类方法(static方法)。

invokedynamic指令用于在运行时动态解析出调用点限定符所引用的方法,并执行该方法,前面4条调用指令的分派逻辑都固化在Java虚拟机内部,而invokedynamic指令的分派逻辑是由用户所设定的引导方法决定的。


2.9 异常处理指令

    Java虚拟机中,处理异常不是由字节码指令来实现的,而是采用异常表来完成的

2.10 同步指令

    Java虚拟机支持方法级的同步和方法内部一段指令序列的同步,这两种同步结构都是使用管程(Monitor)来支持的

    同步一段指令集序列通常是由Java语言中的synchronized语句块来表示的,Java虚拟机的指令集中有monitorenter和monitorexit两条指令来支持synchronized关键字的语义,正确实现synchronized关键字需要Javac编译器与Java虚拟机两者共同协作支持,譬如代码清单6-6中所示的代码。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: