Java虚拟机学习笔记(5)——类文件结构

          上一篇介绍了JVM对象的内存分配和回收策略。这篇接着介绍Java的类文件结构,这篇的内容可能会比较多,我尽量循序渐进的讲。要学习class的文件结构,先要大体对class文件结构有哪些内容有一个整体把握。现在,看下面一张表。


          上面的表格列出了class文件的所有内容项,一定要认真将表格看一遍,对class文件有大致的结构印象,下面将按照表格总从上到下的顺序,一一介绍其中的内容。

          关于上面的表格含义,这里做一下简单的解释,u1、u2、u4、u8代表占用了1、2、4、8个字节大小,以info后缀结尾的代表一个结构表。

        在下面对文件结构的介绍中,为了让大家对文件结构有更清晰的了解,每一项结构,我都会标明这一项是属于哪一项的哪个表中的哪个属性。这样,当内容多起来时,才不容易混乱。


魔数-magic(属于class文件的第一项)

          Java 中用魔数标记这是一个Java class文件。

         参照上面的class文件表,魔数占用u4大小。这里可以明确给出,其实class文件的魔数是:CA FE BA BE(你可以记成“咖啡宝贝”)。下面我们用16进制编辑器查看一下class文件的头4字节。

 

          可以看到,class文件的头四个字节就是CA FE BA BE,它标志了这个文件是一个class文件。


次版本号minor_version与主版本号major_version(属于class文件的第二三项)

          这一项没有什么好多说的,就是代表class文件的主次版本号。但需要注意的是,高版本的JDK中的虚拟机是拒绝执行低版本的class文件的。


          上图中,偏移量0x00000004,u2字节,0x0000代表次版本号,偏移量0x00000006,u2字节,0x0033代表主版本号(换算成10进制为:51)。


常量池入口constant_pool_count与常量池constant_pool(属于class文件的第四五项)

          constant_pool_count,如字面意思,就是常量池计数,大小u2,标记了当前class文件的常量个数(其实看下去你会发现,所有用来计数的大小都是u2)。

          constant_pool就是常量池了,常量池主要存放两大类常量:字面量(Literal)和符号引用(Symbolic Reference)。字面量比较接近Java语言层面的常量概念,如:文本字符串、声明为final的常量值等。

          而符号引用属于编译原理方面的概念,包括下面三类常量:1.类和接口的全限定名。2.字段的名称和描述符。3.方法的名称和描述符。

          常量池有14种常量(JDK 7 之前有11种,JDK 7又加入了3种),每一个常量都是一张表。下面用一张表详细展示常量池的内容。


          上面图如果太小看不清,可以右键查看原图。通过上面的表格,可以很清楚的知道,常量池中的常量类型有哪些,分别表示什么。举个例子:CONSTANT_Utf8_info常量有三个字段tag、length、bytes,其中tag标记常量类型,大小为u1,所有常量都有这个标记;length指定这个utf-8编码的字符串长度,bytes就是存储utf-8编码的字符串。其实,字符串类型的常量,字段/方法简单名、描述符字符串都是存储在这个常量里。

          现在我们来看一个具体的例子:

import com.hexDemo;

public class Demo {
    private String str;
    public int execute() {
        return 0;
    }
}

          上图是Demo类,经过编译后产生的class文件,使用十六进制编辑器打开后的字节码片段。首先,红色框框就是表示的常量池入口,占u2大小,这里的值为0x0011,转换成10进制是17,也就说该类的常量池中有16个常量,注意:为什么是16个常量不是17个?因为常量是从索引1开始的,索引0代表未指向任何常量。

          接着,我们看绿色框、紫色框(和棕色框),它们表示的就是一个具体的常量,上面用框框圈出了前四个常量。其中,绿色框表示的就是常量的tag标记,第一个0x0A代表tag为10的常量,通过查上面的常量表可以知道,tag为10的常量是CONSTANT_Methodref_info,紧接着后面的0x0003和0x000E分别表示指向声明类描述符CONSTANT_Class_info的索引项和指向名称及类型描述符CONSTANT_NameAndTyped的索引项。同样,0x07000F和0x070010分别表示的是第二和第三个常量池常量。现在看看第四个,0x01代表着这个常量是CONSTANT_Utf8_info,0x0003代表接下去的3个字节都是utf8编码的字符,0x737472就是这三个字符,通过转换成10进制对应ascii码,可以知道这三个字符为str,正好对应了源码中的属性名。

        上面只圈出了四个常量,并且分析了其中两个常量。如果要查看类中所有的常量信息,可以借助JDK提供的javap命令,使用方法就是javap -verbose 包名.类名。具体看下图。



访问标志access_flag(属于class文件的第六项)

          常量池结束之后,紧接着的两个字节代表访问标志(access flag)。access_flag中一共有16个标志位可用,当前只定义了其中8个,没有使用到的标志位要求一律为0。

          下标列出了类/接口的所有访问标志

          接着上面的实例:

          偏移量为0x000000AC,图中红色框框圈出的2个字节,就代表着该类的访问标记,即:0x0021,查看上表可知,当前类的访问标记为ACC_PUBLIC和ACC_SUPER。


类索引this_class、父类索引super_class、接口计数interfaces_count和接口集合interfaces(属于class文件的七到十项)

          在类访问标志后面,紧接着u2的类索引和u2的父类索引再加上u2的接口计数(为0的话,后面的接口集合占位0),接口计数后面接着接口索引集合(每个接口索引也是u2)其中,类索引和父类索引用两个u2类型的索引值表示,他们各自指向一个类型为CONSTANT_Class_info的类描述符常量,通过CONTSTANT_Class_info类型的常量中的索引值可以找到定义再CONSTANT_Utf8_info类型的常量中的全限定名字字符串。

          同样,我们接着看看实例:

          地址偏移0x0000AE,连续3个u2大小的项,0x0002、0x0003、0x0000代表的分别为类索引、父类索引和接口计数(因为当前类没有实现任何接口,所以计数为0),其中0x0002为索引,值为2,指向常量池中第二个常量,可以发现这个常量是CONSTANT_Class_info类型,通过这个常量,指向了一个CONSTANT_Utf8_info的字符串,这个字符串就是当前类的全限定类名:com/hexDemo/Demo;同样的,可以得出该类的直接父类为:java/lang/Object。


字段表计数field_count和字段表集合fields(属于class文件的十一和十二项)

          字段表(field_info)用于描述接口或者类中声明的变量。字段(field)包括类级变量以及实例级变量,但不包括方法内部声明的局部变量。字段的描述有哪些信息?字段的作用域(public、private、protected修饰符)、是实例变量还是类变量(static修饰符)、可变性(final)、并发可见性(volatile修饰符,是否强制从主内存读写)、可否被序列化(transient修饰符)、字段数据类型(基本类型、对象、数组)、字段名称。上述的这些信息中,各个修饰符都是布尔值,要么有某个修饰符,要么没有,很适合使用标志位来表示。而字段叫什么名字、字段被定义为什么数据类型这些都是无法固定的,只能引用常量池中的常量来描述。

          下图展示了字段表的结构

          字段表包含了u2的访问标志、u2的名称索引(指向常量池中的一个常量,对应该字段的简单名称),u2的描述索引(也是指向常量池中的一个常量,对应该字段的描述符),u2的属性计数和属性计数大小的属性(attribute是附加的一些属性信息,在后面会详细讲)。

          在进行实例讲解时,我们先看看字段有哪些访问标志,下面同样用一张表列出字段的所有访问标志。需要注意:ACC_FINAL和ACC_VOLATILE不能同时为真,接口中ACC_PUBLIC、ACC_STATIC、ACC_FINAL必须同时为真。


          接着再解释一下,什么叫简单名称是指没有类型和参数修饰的方法或字段名,如上面给的源码类中,字段名str和方法名execute就是简单名称。相对于简单名称,方法和字段的描述符会稍微复杂点。

          这里也不多说其它的,举两个例子就懂了。例1,用描述符描述一个 String[][] 字段和 int[] 字段 ,结果为:[[Ljava/lang/String和[I;

例2:用描述符描述一个 void execute(String[][] str, int i, int j, int[][] nums),结果为:([[Ljava/lang/StringII[[I)V,这里需要注意,是参数类型在前,返回值类型再后。

          ps: void在虚拟机规范中单独列出为“voidDescriptor”,在《深入理解Java虚拟机》一书中,作者为了结构统一,将void也列入表中。

          不能少的步骤,我们同样进行一下实例分析:

          偏移量0x000000B4开始u2字节,0x0001代表字段表计数(这里明显为1,代表有一个字段表),接着0x0002代表字段表的第一项,即:access_flags,值为2,说明这个字段是私有的,再接着0x0004代表一个字段简单名的索引,值为4,指向常量池中第4个的常量,值为str,就是该字段的简单名。0x0005,指向的是一个常量池索引,代表的是一个字段的描述符,值为5,常量池中第五个常量为: Ljava/lang/String(抱歉,图上忘记框起来了,偷点懒,就不重新画了);地址偏移:0x000000BC,u2大小,0x0000代表属性计数为0,所以到此字段表集合结束。


方法表计数methods_count和方法表集合methods(属于class文件的第十三和第十四项)

          方法表的结构和字段表结构是一模一样的(这里不在浪费版面,就不贴一样的图了),如果忘记了刚才介绍的字段表结构,就稍微往前再看一遍(人的记忆有时候还不如金鱼?)。

          方法的标志值和字段的标志值还是有所区别,所以这里也给出方法的所有标志值。

          我想,看到这里,应该所有人都懂的看class文件的十六进制编码了,为了篇幅完整,我在这里同样给出实例。


          话不多说,0x0002方法计数,值为2,代表有两个方法,怎么会有两个方法?别忘记除了实例方法execute外,还有实例构造器init()。第一个方法,访问标志0x0001,标明方法为public,0x0006简单名索引,值为:<init>,0x0007方法描述符索引,值为:()V。


          到了这里,我想大家一定有个疑问,方法体哪里去了?不要忘记前面说的attributes表,方法体就是在attributes表的code项里。


属性表计数attributes_count和属性表集合attributes(很多地方都有属性表,比如:Class文件、Class文件的字段表、方法表等)

        下面表中列出了属性表的属性类型(原谅我又偷懒了,没有自己手动再画一张表,下图截自于《深入理解Java虚拟机》)


          上表中列出了attributes表所可能出现的属性,但需要注意,其中每个属性都有一个表结构,表结构可能各不相同,但是每个属性表的前面两项都是一样的。其中一个是u2大小的attribute_name_index,它指向常量池的一个CONSTANT_Utf8_info类型,代表一个属性名;另外一个是u4大小的attribute_length,这一项用于说明该属性的属性表大小。

          下面将介绍属性表中的几个比较重要的属性。


Code属性(class文件中的方法表中的attributes属性表中的Code属性)

          到现在,内容开始有点多了,不要搞混各个项所在的位置。Code属性其实就是存储类中方法的字节码,具体怎么存储?下面同样给出Code属性表的结构进行详解。


          attribute_name_indexattribute_length就不多说了,就是一个常量池字符串的引用和标记属性长度的项而已。
          max_stack:代表操作数栈(Operand Stacks)的深度的最大值。虚拟机运行时需要根据这个值来分配栈帧(Stack Frame)中的操作栈深度。
          max_locals:代表局部变量表所需要的存储空间,在这里,max_locals的单位是 Slot,Slot是虚拟机为局部变量分配内存所使用的最小单位。对于byte、char、float、int、short、boolean和returnAddress等长度不超过32位的数据类型,每个局部变量占用一个Slot,而double和long这两种64位的数据类型则需要两个Slot来存放。方法参数(包括实例方法中隐藏参数“this”)、显式异常处理器的参数(Exception Handler Parameter,就是try-catch语句中catch块所定义的异常)、方法体定义的局部变量,这些都需要使用局部变量表来存放。需要注意的是,不能把这些局部变量所占Slot之和作为max_locals的值,因为局部变量表中的Slot可以重用,当代码执行超出一个局部变量的作用域时,这个局部变量所占的Slot可以被其它局部变量所使用,Javac编译器会根据局部变量的作用域来分配Slot给各个变量使用,然后计算出max_locals的大小
           code_length:代表字节码长度,这是一个u4类型的长度值,理论上最大值可以达到2^23-1,但是虚拟机规范  中限制了一个方法不允许超过65535条字节码指令。所以,实际上,它只使用了u2长度。
          code:用于存储字节码指令的一系列字节流,每个字节指令是一个u1类型的单字节,所以一共可以表示256(0~255)条指令。目前,Java虚拟机规范定义了其中约200条编码值对应的指令含义。
          exception_table_length:u2类型,表示异常长度。
          exception_table:异常表集合。下表显示了异常表的结构。这几个类型的含义是,如果当字节码在statrt_pc行到第end_pc行之间(不含第end_pc行)出现了类型位ctach_type或其子类的异常(catch_type为指向一个CONSTANT_Class_info型常量的索引),则转到第handler_pc行继续处理。当catch_type的值为0时,代表任意异常情况都需要转向到handler_pc处继续处理。注意:此处字节码的“行”是一种形象的描述,指的是字节码相对于方法体开始的偏移量,而不是Java源码的行号。
          ps: 编译器使用异常表而不是简单的跳转命令来实现Java异常和Finally处理机制。


Eception属性(Class文件的方法表中的attributes表的Eception属性)
        
  这里的Exceptions属性是在方法表中的与Code属性平级的一项属性(注意与异常表区分),Excptions属性的作用是列举出方法中可能抛出的受查异常(CheckedExceptions)。

          attribute_name_indexattribute_length不再赘述(忘记的再往前看看)。
          number_of_exception:表示方法可能抛出number_of_exceptions种受查异常,每一种受检查遗产够用exception_index_table表示。
          exception_index_table:是指向常量池中的CONSTANT_Class_info型常量的索引,代表了该受查异常的类型。

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

          attribute_name_indexattribute_length不再赘述(忘记的再往前看看)。
          line_number_table:是一个数量为line_number_table_length、类型为Line_number_info的集合,line_number_info表包括了start_pc和line_number两个u2类型的数据项,前者是字节码行号后者是Java源码行号

LocalVaribaleTable属性(Class文件的attributes表中的LocalVariableTable属性)
       
   LocalVariableTable属性用于描述栈帧中局部变量表中的变量与java源码中定义的变量之间的关系,它也不是运行时必须的属性,但默认会生成到Class文件中,可以在Javac中分别使用-g:none或-g:vars选项来取消或要求生成这项信息。如果没有生成这项属性,最大的影响就是当其他人引用这个方法时,所有的参数名称都将会丢失,IDE将会使用诸如arg0、arg1之类的占位符代替原有的参数名。

          attribute_name_indexattribute_length不再赘述(忘记的再往前看看)。
           local_varaibale_info:这个项目代表了一个栈帧与源码中局部变量的关联。下面再列出local_variable_info项目结构。

                    name_indexdescriptor:都是指向常量池中CONSTANT_Utf8_info型常量的索引,分别代表了局部变量的名称和这个局部变量的描述符。
                    start_pclength属性:分别代表了这个局部变量的声明周期开始的字节码偏移量及其操作的范围覆盖的长度,两者结合起来就是这个局部变量再字节码之中的作用域访问。

SourceFile属性(Class文件的attributes项的SourceFile属性)
         
SourceFile属性用于记录生成这个Class文件的源码文件名称。这个属性同样是可选的,可以分别使用Javac的-g:none或-g:source选项来关闭或要求关闭这项信息。在Java中,对于大多数的类来说,类名和文件名是一致的,但是有一些特殊情况(如内部类)例外。如果不生成这项属性,当抛出异常时,堆栈中将不会显示出错代码所属的文件名,这个属性是一个定长的属性,其结构见下表。

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

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

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

InnerClasses属性(Class文件的attributes表的InnerClass属性)
          InnerClasses属性用于记录内部类与宿主类之间的关联,如果一个类中定义了内部类,那编译器将会为它以及它所包含的内部类生成InnerClasses属性。该属性的结构见下表。

          数据项number_of_classes代表需要记录多个内部类信息,每个内部类的信息都由一个inner_classes_info表进行描述。inner_classes_info表的结构见下表。

          inner_class_infoouter_class_info_index:都是指向常量池中CONSTANT_Class_info常量的索引,分别代表了内部类和宿主类的符号引用。
          inner_name_index:是指向常量池中CONSTANT_Utf8_info型常量的索引,代表了内部类的名称,如果是匿名内部类,那么该项的值为0。
          inner_class_access_flags:是内部类的访问标志,类似于类的access_flags,它的取值范围见下表。


Deprecated及Synthetic属性(这个属性在类/字段表/方法表中的attribute中都可以有)
         
Deprecated和Synthetic这两个属性都属于标志类型的布尔属性,只有存在有和没有的区别,没有属性值的概念。
          Deprecated属性用于表示某个类、字段或者方法,已经被程序作者定位不再推荐使用,他可以在再代码中通过使用@deorecared注解进行设置。
          Synthetic属性代表此字段或者方法并不是由Java源码直接产生,而是由编译器自行添加的。再JDK 1.5之后,标识一个类、字段或者方法是是由编译器自动产生的,也可以设置它们访问标志中的ACC_SYNTHETIC标志位,其中最典型的例子就是Bridge_Method。所有由非用户代码产生的类、方法及字段都应当至少设置SYnthetic属性和ACC_SYSNTHETIC标志位中的一项,唯一的例外是实例构造器“<init>”方法和类构造器“<cinit>”方法
         
Deprecated和Synthetic属性结构非常简单,这里就不再给出表,这两个属性的结构只包含了属性必有的u2的attribute_name_index和u4的attribute_length,其中attribute_length为0x00000000,因为没有任何属性值需要设置。

StatckMapTable属性(Class文件的方法表中的attribute表中的Code表中的attribute表中的StackMapTable属性)
         
StackMapTable属性再JDK 1.6发布后增加到了Class文件规范中,他是一个复杂的变长属性,位于Code属性的属性表中。这个属性会在虚拟机类加载的字节码验证阶段被新类型检查验证其(Type Checker)使用,目的在于代替以前比较消耗性能的基于数据流分析的类型推倒验证器。
          StackMapTable属性包含零至多个栈映射帧(Stack Map Frames),每个栈映射帧都显式或隐式地代表了一个字节码偏移量,用于表示执行到该字节码时局部变量表和操作数栈地验证类型。类型检查器会通过检查目标方法地局部变量和操作数栈所需要地来确定一段代码指令是否复合逻辑约束。StackMapTable属性地结构见下表。

          在版本号大于或等于50.0的Class文件中,如果方法地Code属性中没有附带StackMapTable属性,那就意味者它带有一个隐式地StackMap属性。这个StackMap属性地作用等同于number_of_entries的值为0的StackMapTable属性。一个方法的Code属性最多只能有一个StackMapTable属性,否则将抛出ClassFormatError异常。

Signature属性(Class文件/Class文件的字段表/方法表中的attributes表中的Singature属性)
          Signature属性在JDK 1.5发布后增加到了Class文件规范之中,它时一个可选的定长属性,可以出现于类、字段表和方法表结构的属性表中。在JDK 1.5中大幅增强了Java语言的语法,在此之后,任何类、接口、初始化方法或成员的泛型签名,如果包含了类型变量(Type Variables)或参数化类型(Parameterized Types),则Signature属性会为它记录泛型签名信息(Java语言泛的型采用的是擦除法实现的伪泛型)。Singature属性的结构见下表。


          signature_index:该项的值必须是一个对常量池的有效索引(常量池在该索引处的项必须是CONSTANT_Utf8_info结构),表示类签名、方法类型签名或字段类型签名。如果当前Signature属性是类文件的属性,则这个结构表示类型签名,如果当前Singature属性是字段表的属性,则这个结构表示字段类型签名。

BootstrapMethods属性(Class文件的attributes表的BootstrapMethods属性)
         
BootstrapMethods属性在JDK .17发布后增加到了Class文件规范之中,它是以恶搞复杂的变长属性,位于类文件的属性表中。这个属性用于保存invokedynamic指令应用的引导方法限定符。
         
BootstrapMethods属性的结构表如下

          其中引用到的bootstrap_method结构表如下

          bootstrap_method_ref:bootstrap_method_ref项的值必须是一个对常量池的有效索引。常量池在该索引处的值必须是一个CONSTANT_MethodHandle_info结构。
          num_bootstrap_arguments[]:bootstrap_arguments[]数组的每个成员必须是一个对常量池的有效索引。常量池在该索引处必须是下列结构一:CONSTANT_String_info、CONSTANT_Class_info、CONSTANT_Integer_info、CONSTANT_Long_info、CONSTANT_Float_info、CONSTANT_Double_info、CONSTANT_MethodHandle_info或CONSTANT_MethodType_info。


          至此,Class文件的主要内容都已经介绍完毕了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值