细说JVM(类文件结构(二))

作者: Jivanmoon | 来源:发表于2018-08-05 17:05 被阅读0次

    一、前言

    上一篇博客的地址:细说JVM(类文件结构(一))

    二、类文件分析

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

    在访问标志access_flags后接下来就是类索引(this_class)和父类索引(super_class),这两个数据都是u2类型的,而接下来的接口索引集合是一个u2类型的集合,class文件由这三个数据项来确定类的继承关系。由于Java中是单继承,所以父类索引只有一个;但Java类可以实现多个接口,所以接口索引是一个集合。

    类索引用来确定这个类的全限定名,这个全限定名就是说一个类的类名包含所有的包名,然后使用"/"代替"."。比如Object的全限定名是java.lang.Object。父类索引确定这个类的父类的全限定名,除了Object之外,所有的类都有父类,所以除了Object之外所有类的父类索引都不为0.接口索引集合存储了implements语句后面按照从左到右的顺序的接口。

    类索引和父类索引都是一个索引,这个索引指向常量池中的CONSTANT_Class_info类型的常量。然后再CONSTANT_Class_info常量中的索引就可以找到常量池中类型为CONSTANT_Utf8_info的常量,而这个常量保存着类的全限定名。

    在本例子中:


    TIM截图20180805152848.png

    this_class的值是0x0005,即十进制的5,指向的CONSTANT_Class_info中的索引是26,常量池中索引是26的CONSTANT_Utf8_info的常量值是temp/HelloWorld。这样就解析到了这个类的全限定名,类的父类的全限定名也可以这样解析。

    由于这个类没有实现接口,所以接口索引集合的容量计数是0。如果容量计数是0,就不需要存储接口的信息。

    6、字段表集合

    字段表集合,顾名思义就是Java类中的字段,字段又分为类字段(静态属性)和实例字段(对象属性),那么,在Class文件中是如何保存这些字段的呢?我们可以想一想保存一个字段需要保存它的哪些信息呢?

    答案是:字段的作用域(public、private和protected修饰符)、是实例变量还是类变量(static修饰符)、可变性(final修饰符)、并发可见性(volatile修饰符)、是否可被序列化(transient修饰符)、字段的数据类型(基本类型、对象、数组)以及字段名称。

    这些信息中,各个修饰符可以用布尔值表示。而字段叫什么名字、字段被定义为什么类型数据都是无法固定的,只能用常量池中的常量来表示。下面是字段表的格式:


    class_file_fields.png

    其中的字段修饰符access_flags,和类中的access_flags类似,对于字段来说可以设置的标志位及含义如下:

    TIM截图20180805154316.png

    access_flags给出了字段中所有可以用布尔值表示的修饰符,剩下的信息就是字段的名字、变量类型等信息。access_flags后面的是name_indexdescriptor_index,前者是字段名的常量池索引,后者是字段描述符的常量池索引。name_index可以描述字段的名字,descriptor_index可以描述字段的数据类型。不过,对于方法的描述符来说就要复杂一些,因为一个方法除了返回值类型,还有参数类型,而且参数的个数还不确定。根据描述符规则,这些类型都使用一个大写字母来表示,如下表:

    ![TIM截图20180805155632.png](https://img.haomeiwen.com/i4476195/84de565c7a52a5ef.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

    对于数组类型,每一个维度将使用一个前置的“[”字符来描述。比如定义一个java.lang.String[][]类型的二维数组,将记录为[[Ljava/lang/String,一个double数组double[]将标记为[D

    当描述符用来描述方法时,按照先参数列表,后返回值的顺序描述,参数列表按照参数的严格顺序放在一组小括号()内。比如方法void inc()的描述符是:()V。方法java.lang.String toString()的描述符是:()Ljava/lang/String。方法int indexOf(char[] source,int sourceOffset,int sourceCount,char[] target,int targetOffset,int targetCount,int fromIndex)的描述符是:([CII[CIII)I

    descriptor_info后面是属性信息,这会在后面属性表集合中介绍。

    在本例子中:


    TIM截图20180805155632.png

    额,因为在程序中根本没有定义什么字段,所以字段的数量是0x0000,所以就是0,也就没有什么字段表,不过这都不是什么问题,下面我们看一下方法表。

    7、方法表集合

    在字段表集合中介绍了字段的描述符和方法的描述符,对于理解方法表有很大帮助。class文件存储格式中对方法的描述和对字段的描述几乎相同,方法表的结构也和字段表相同,这里就不再列出。不过,方法表的访问标志和字段的不同,列出如下:


    TIM截图20180805155912.png

    本例子中:


    TIM截图20180805161217.png

    我们可以看出,前两个字节是方法表集合中的院元素个数,这里是0x0002,所以有两个方法,按照字段的解析方法,可以得到每个方法的定义。分别是:

    • public <init> ()V
    • public static main ([Ljava/lang/String;)V

    但是我们发现我们本来只定义了一个main方法,为啥会有两个方法呢?
    其实,Java类都要有一个构造方法,如果没有的话编译器会自动构造一个无参的构造方法,就是上面的第一个名叫<init>的方法;同时,如果一个类中含有静态代码块或者静态变量,那么就需要首先执行类的构造方法,来执行静态代码块和初始化静态变量,但是上面的代码中并没有静态变量,也没有静态代码块,所以也就没有虚拟机默认添加的<clinit>的方法。

    不过,方法比字段还多了方法体呢,那方法体中的代码哪去了?
    在每一个方法表中descriptor_index后描述属性的时候,0x0001表明属性的个数为1,再后面的0x0009是指向常量池中的CONSTANT_Utf8_info常量,内容是Code,说明后面属性中存放的就是方法体里面编译后的字节码指令。

    8、属性表集合

    属性表在前面出现了多次,在Class文件、字段表和方法表都可以携带自己的属性表集合,来描述某些场景专有的信息。
    与Class文件中其他的数据项目要求严格的顺序、长度和内容不同,属性表集合的限制比较少,不要求严格的顺序,只要不与已有的属性名重复,任何人实现的编译器都可以向属性表中写入自定义的属性信息,Java虚拟机会在运行时忽略掉那些不认识的信息。为了能正确解析class文件,《Java虚拟机规范(第二版)》中预定义了9项虚拟机应当识别的属性。现在,属性已经达到了21项。具体信息如下表,这里仅对常见的属性做介绍:


    TIM截图20180805162541.png TIM截图20180805162554.png

    从上表可以看出,属性表集合存在的位置也是不确定的,不仅可以存储在Class文件结尾处,还可以作为数据项存在于类、方法表集合和字段表集合、Code属性中。对于存在于Class类文件中的属性表集合很好理解,毕竟在开头的Class文件结构图中的最后一部分就是属性表集合,这时属性表集合作为构成Class文件结构的一个大部分。剩下的存在于类中、方法表集合与字段表集合和Code属性中的属性表集合,其实是作为它们的一个数据项存在的。

    存在于类中的属性表集合,存储了关于这个类的一些信息。比如这个类是否是过时的(Deprecated)、在泛型中保存类的类型参数(由于生成Class文件后会进行类型擦除,Java中的泛型是一种伪泛型)和动态注解等信息;存放在方法表集合中的属性表集合存储了关于方法的信息,最主要的就是Code属性,存储了字节码指令;存放于字段表集合中的属性表集合存储了关于字段的信息,我们这里的例子没有涉及到字段的属性,不过当在类中定义了静态常量(static final)并且这个常量有初始值时会将这个值作为属性存储在字段表中的属性表集合中。

    由于属性表集合的限制较小,每个属性都会有自己的格式,因此class文件对于属性的格式要求也比较宽松,只需要满足一些特定的条件即可。下表是属性的结构:


    ![![TIM截图20180805163221.png](https://img.haomeiwen.com/i4476195/681774ecfb3fd3b9.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) ](https://img.haomeiwen.com/i4476195/b121dbb3c91c6e1d.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

    从上表可以看出,Class文件规定的属性格式只有前6个字节:两个字节的属性名称的索引和4个字节的属性长度,接下来就要按照这个长度存储属性值了。这样的宽松格式使得属性表的结构可以多样变化,甚至可以在属性的内容中再加入一个属性,比较常用的就是方法表集合中的Code属性,在Code属性中还有LineNumberTable属性和LocalVariableTable属性等。

    接下来就简单介绍一下常用的属性。

    (1)Code属性

    最常用的属性恐怕就是Code属性了,因为大多数的方法都会有编译后的字节码指令,这些指令就存储在方法表中的Code属性中。如果一个Java程序的信息可以分为代码(方法体中的代码)和元数据(包括类、字段、方法定义以及其它信息),那么Code属性存储的就是代码,其它所有的结构存储的都是元数据。不过并非所有的方法表都有这个Code属性,比如接口或抽象类中的方法表就不存在Code属性,Code属性的结构如下::

    其中attribute_name_indexattribute_length前面已经介绍过了。

    max_stack代表了操作数栈的最大深度。在方法执行的任意时刻,操作数栈都不会超过这个深度。虚拟机执行时需要根据这个值来分配栈帧中的操作栈深度。

    max_locals代表了局部变量表所需要的存储空间。在这里,max_locals的单位是slot。方法参数(包括隐式参数this)、显式异常处理器的参数(try-catch块中catch块中定义的异常)以及方法体中定义的局部变量都需要局部变量表来存放。需要注意的是,由于局部变量表中的slot可以重用,所以并不是所有的局部变量的总slot就是max_locals。编译器会根据变量的作用域来分配slot给各个变量使用,然后计算max_locals的大小。

    code_lengthcode用来存储字节码指令。Java的字节码指令的长度都是一个字节,即最多可以有256个指令,实际上一共有大约200条指令。对于字节码指令这里不过多介绍。

    exception_table_lengthexception_table分别是指异常表长度,和异常表集合。

    attributes_countattributes是Code属性中的属性表集合。

    (2)SourceFile属性

    本例子中:


    TIM截图20180805164325.png

    SourceFile属性记录生成这个Class文件的源码文件名称。在上面的数据中,0x0001表示属性表集合中有一个属性,0x0012(即十进制18)是属性名的索引值,查找常量池可以知道是SourceFile,0x00000002是这个属性的长度,即两个字节,最后的两个字节就是这个属性的内容,是一个常量池索引,0x0013,十进制19,结果是HelloWorld.java。

    到此为止,我们就分析完了一个Class文件的文件结构,不过因为例子过于简单的原因,很多属性表集合中的属性都没有展示,有兴趣的可以自己写一个比较复杂的例子,自己分析一下类文件结构,有助于提高对于JVM的理解。

    相关文章

      网友评论

        本文标题:细说JVM(类文件结构(二))

        本文链接:https://www.haomeiwen.com/subject/lcwbvftx.html