doclist 阅读(19) 评论(0)

#【JVM虚拟机】(8)--深入理解Class中--方法、属性表集合

之前有关class文件已经写了两篇博客:

1、【JVM虚拟机】(5)---深入理解JVM-Class中常量池

2、【JVM虚拟机】(6)---深入理解Class中访问标志、类索引、父类索引、接口索引

3、【JVM虚拟机】(7)---深入理解Class中-属性集合

那么这篇博客主要讲有关 方法表集合 相关的理解和代码示例。

方法表集合: 告知该方法是什么修饰符修饰?是否有方法值?返回类型是什么?方法名称,方法参数,还有就是方法内的一些信息。

一、方法集合概念

1、概念

方法表集合:方法表集合和属性表集合其实很相似,都是由一个计数器(方法)若干个方法表构成,只不过方法表的结构相对复杂很多。

方法表的结构体访问标志(access_flags)、名称索引(name_index)、描述索引(descriptor_index)、属性表(attribute_info)集合组成。

method_info {
                    u2 access_flags;
                    u2 name_index;
                    u2 descriptor_index;
                    u2 attributes_count;
                    attribute_info attributes[attributes_count];
             }

1)、访问标志

不多说了,和属性中的其实差不多,只是有些修饰符不一样。

2)、名称索引

就是指这个方法的名称。如:'public void getXX()'中,getXX就是名称索引。名称索引两个字节,这个方法的名称以UTF-8格式的字符串存储在这个常量池项中。

3)、描述索引

指这个方法的返回值,方法内参数信息。一个方法的描述包含若干个参数的数据类型和返回值的数据类型。

4)、属性表(attribute_info)集合

下面讲

二、属性表集合

1、概述

在Class文件、字段表、方法表都可以携带自己的属性表集合,用于描述某些场景专有的信息。
在方法表中, 属性表集合记录了某个方法的一些属性信息,这些信息包括:

  • 这个方法的代码实现,即方法的可执行的机器指令
  • 这个方法声明的要抛出的异常信息
  • 这个方法是否被@deprecated注解表示
  • 这个方法是否是编译器自动生成的

属性表(attribute_info)结构体的一般结构如下所示:

属性表占着非常大的一部分且定义了众多属性,上面只列举了4个,查看完成的:JDK1.7版本中21项属性表集合简要介绍

下面介绍两个重要的属性

1、Code属性

code属性比较复杂,它是经过编译器编译成字节码指令之后的数据。就是说java程序中的方法体经过javac编译器处理后,最终变成字节码存储在Code属性内

并非所有方法表都有这个属性,接口和抽象类就没有【没有方法体】。
Code属性是Class文件中最重要的一个属性,在Class文件中,Code属性用于描述代码,所有的其它数据项目都用来描述元数据,了解code属性对了解字
节码执行引擎来说是必要基础。

Code属性表的组成部分:

机器指令——code

目前的JVM使用一个字节表示机器操作码,即对JVM底层而言,它能表示的机器操作码不多于28 次方,即 256个。class文件中的机器指令部分是class文件中最重要的部分,并且非常复杂。

异常处理跳转信息

如果代码中出现了try{}catch{}块,那么try{}块内的机器指令的地址范围记录下来,并且记录对应的catch{}块中的起始机器指令地址,当运行时在try块中有异常抛出的话,JVM会将catch{}块对应懂得其实机器指令地址传递给PC寄存器,从而实现指令跳转;

Java源码行号和机器指令的对应关系---LineNumberTable属性表

编译器在将java源码编译成class文件时,会将源码中的语句行号跟编译好的机器指令关联起来,这样的class文件加载到内存中并运行时,如果抛出异常,JVM可以根据这个对应关系,抛出异常信息,告诉我们我们的源码的多少行有问题,方便我们定位问题。

Code属性表结构体:

1、attribute_name_index : 属性名称索引,占有2个字节,其内的值指向了常量池中的某一项,该项表示字符. 串“Code”;

2、attribute_length : 属性长度,占有 4个字节,其内的值表示后面有多少个字节是属于此Code属性表的;

3、max_stack : 操作数栈深度的最大值,占有 2 个字节,在方法执行的任意时刻,操作数栈都不应该超过这个值,虚拟机的运行的时候,会根据这个值来设置该方法对应的栈帧(Stack Frame)中的操作数栈的深度;

4、max_locals 最大局部变量数目,占有 2个字节,其内的值表示局部变量表所需要的存储空间大小;

5、code_length : 机器指令长度,占有 4 个字节,表示跟在其后的多少个字节表示的是机器指令;

6、code 机器指令区域,该区域占有的字节数目由 code_length中的值决定。JVM最底层的要执行的机器指令就存储在这里;

7、exception_table_length : 显式异常表长度,占有2个字节,如果在方法代码中出现了try{} catch()形式的结构,该值不会为空,紧跟其后会跟着若干个exception_table结构体,以表示异常捕获情况;

8、exception_table : 显式异常表,占有8 个字节,start_pc,end_pc,handler_pc中的值都表示的是PC计数器中的指令地址。exception_table表示的意思是:如果字节码从第start_pc行到第end_pc行之间出现了catch_type所描述的异常类型,那么将跳转到handler_pc行继续处理。

9、attribute_count : 属性计数器,占有 2 个字节,表示Code属性表的其他属性的数目

10、attribute_info : 表示Code属性表具有的属性表,它主要分为两个类型的属性表:“LineNumberTable”类型和“LocalVariableTable”类型。“LineNumberTable”类型的属性表记录着Java源码和机器指令之间的对应关系“LocalVariableTable”类型的属性表记录着局部变量描述

2、ConstantValue属性

之所以学习这个,是因为后面类加载机制有联系到这个属性

这个属性的作用是通知虚拟机为静态变量赋值,只要被static修饰的变量才有这个属性,【有该属性的字段必须有ACC_STATIC访问标志,反过来不一定】。
对于 "int x = 123" 和 "static int x =123"这类代码在日常编写中很常见,但虚拟机对这两种变量赋值的时刻却不同。
对于非static变量[实例变量],是在实例构造器<init>进行
对于类变量,有两种方式选择
①在类构造器<clinit>方法中赋值
②使用ConstantValue属性初始化
目前Sun javac编译器是这么做的【具体咋做不知道 = =】,如果同时使用final和static修饰一个变量[这种修饰就相当于个常量],并且是String或基本类型,就使用②,
如果没有被final修饰或不是基本类型和String,就选择①在<clinit>方法中初始化
//有关这点我在上篇博客举过例子,最后几句话也对这个解释的很清楚。


三、示例

有关方法的代码示例,我就不亲自测了,因为有位博主写的已经很清晰啦,我自己写也没那么清晰。

1、访问标志

public static synchronized final void greeting(){  
}  

greeting()方法的修饰符有:public、static、synchronized、final 这几个修饰符修饰,那么相对地,

greeting()方法的访问标志中的ACC_PUBLICACC_STATICACC_SYNCHRONIZEDACC_FINAL标志位都应该是1

从上面第一张图可以得出,该访问标志的值应该是十六进制0x0039

2、名称索引和描述符索引

紧接着访问标志(access_flags)后面的两个字节,叫做名称索引(name_index),这两个字节中的值是指向了常量池中某个常量池项的索引,该常量池项表示这这个方法名称的字符串。

方法描述符索引(descrptor_index)是紧跟在名称索引后面的两个字节,这两个字节中的值跟名称索引中的值性质一样,都是指向了常量池中的某个常量池项。这两个字节中的指向的常量池项,是表示了方法描述符的字符串

3、代码示例

package com.louis.jvm;  
public class Simple {  
    public static synchronized final void greeting(){  
        int a = 10;  
    }  
} 

1)、 Simple.class文件如下所示

注意 :方法表集合的头两个字节,即方法表计数器(method_count)的值是0x0002,它表示该类中有2 个方法。注意到,我们的Simple.java中就定义了一个greeting()方法,为什么class文件中会显示有两个方法呢?

原因:如果我们在类中没有定义实例化构造方法,JVM编译器在将源码编译成class文件时,会自动地为这个类添加一个不带参数的实例化构造方法,这种添加是字节码级别的,JVM对所有的类实例化构造方法名采用了相同的名称:“”。如果我们显式地如下定义Simple()构造函数,这个类编译出来的class文件和上面的不带Simple构造方法的Simple类生成的class文件是完全相同的。

2)、Simple.class 中的<init>() 方法

解释:

1、方法访问标志(access_flags) : 占有 2个字节,值为0x0001,即标志位的第 16 位为 1,所以该<init>()方法的修饰符是:ACC_PUBLIC;

2、 名称索引(name_index): 占有 2 个字节,值为 0x0004,指向常量池的第 4项,该项表示字符串'<init>',即该方法的名称是'<init>';

3、描述符索引(descriptor_index): 占有 2 个字节,值为0x0005,指向常量池的第 5 项,该项表示字符串“()V”,即表示该方法不带参数,并且无返回值(构造函数确实也没有返回值);

4、属性计数器(attribute_count) : 占有 2 个字节,值为0x0001,表示该方法表中含有一个属性表,后面会紧跟着一个属性表;

5、属性表的名称索引(attribute_name_index) :占有 2 个字节,值为0x0006,指向常量池中的第6 项,该项表示字符串“Code”,表示这个属性表是Code类型的属性表;

6、 属性长度(attribute_length):占有4个字节,值为0x0000 0011,即十进制的 17,表明后续的 17 个字节可以表示这个Code属性表的属性信息;

7、 操作数栈的最大深度(max_stack):占有2个字节,值为0x0001,表示栈帧中操作数栈的最大深度是1

8、局部变量表的最大容量(max_variable):占有2个字节,值为0x0001, JVM在调用该方法时,根据这个值设置栈帧中的局部变量表的大小;

9、 机器指令数目(code_length) :占有4个字节,值为0x0000 0005,表示后续的5 个字节 0x2A 、0xB7、 0x00、0x01、0xB1表示机器指令;

10、机器指令集(code[code_length]):这里共有 5个字节,值为0x2A 、0xB7、 0x00、0x01、0xB1

11、显式异常表集合(exception_table_count): 占有2 个字节,值为0x0000,表示方法中没有需要处理的异常信息;

12、Code属性表的属性表集合(attribute_count): 占有2 个字节,值为0x0000,表示它没有其他的属性表集合,因为我们使用了-g:none 禁止编译器生成Code属性表LineNumberTable 和LocalVariableTable;

解释下机器指令集:

第一个字节 **0x2A:查询Java 虚拟机规范中关于操作码的解释,0x2A 对应的操作是"aload_0",作用是将第一个引用类型局部变量推送至栈顶;

第二个字节 0xB7 :0xB7 对应的操作是:"invokespecial",作用是调用超类构造方法、实例初始化方法或私有方法;它带有2个字节的参数,即后面的 0x00、0x01 是它的参数,这个参数是某个常量池中的索引,指向了常量池的第一项,该项表示一个方法引用项CONSTANT_Methodref_info结构体,表示java.lang.Object 类中的<init>()方法,即 java/lang/Object."<init>":()V。这条指令的意思就是调用父类Object的构造方法<init>()

第5个字符是0xB1 : 对应操作是:“Ireturn”,作用是表示无返回值的方法返回,结束方法调用,这条语句放在方法的机器码最后,表示方法结束调用,返回。

我们可以使用javap -v Simple > Simple.txt,查看反编译信息是怎样显示这一信息的:

3)Simple.class 中的greeting() 方法

解释:

1、方法访问标志(access_flags): 占有 2个字节,值为 0x0039 ,即二进制的00000000 00111001,即标志位的第11、12、13、16位为1,根据上面讲的方法标志位的表示,可以得到该greeting()方法的修饰符有:ACC_SYNCHRONIZED、ACC_FINAL、ACC_STATIC、ACC_PUBLIC;

2、 名称索引(name_index): 占有 2 个字节,值为 0x0007,指向常量池的第 7 项,该项表示字符串“greeting”,即该方法的名称是“greeting”;

3、描述符索引(descriptor_index): 占有 2 个字节,值为0x0005,指向常量池的第 5 项,该项表示字符串“()V”,即表示该方法不带参数,并且无返回值;

4、属性计数器(attribute_count): 占有 2 个字节,值为0x0001,表示该方法表中含有一个属性表,后面会紧跟着一个属性表;

5、属性表的名称索引(attribute_name_index) :占有 2 个字节,值为0x0006,指向常量池中的第6 项,该项表示字符串“Code”,表示这个属性表是Code类型的属性表;

6、属性长度(attribute_length):占有4个字节,值为0x0000 0010,即十进制的16,表明后续的16个字节可以表示这个Code属性表的属性信息;

7、操作数栈的最大深度(max_stack) :占有2个字节,值为0x0001,表示栈帧中操作数栈的最大深度是1

8、 局部变量表的最大容量(max_variable):占有2个字节,值为0x0001, JVM在调用该方法时,根据这个值设置栈帧中的局部变量表的大小;

9、器指令数目(code_length):占有4 个字节,值为0x0000 0004,表示后续的4个字节0x10、 0x0A、 0x3B、0xB1的是表示机器指令;

10、机器指令集(code[code_length]):这里共有4 个字节,值为0x10、 0x0A、 0x3B、0xB1

11、显式异常表集合(exception_table_count): 占有2 个字节,值为0x0000,表示方法中没有需要处理的异常信息;

12 Code属性表的属性表集合(attribute_count): 占有2 个字节,值为0x0000,表示它没有其他的属性表集合,因为我们使用了-g:none 禁止编译器生成Code属性表LineNumberTable 和LocalVariableTable;

指令集解释

第一个字节 0x10 : 查询Java虚拟机规范中关于操作码的解释,0x10 对应的操作是"bipush"," 作用是将单字节的常量值(-128~127) 推送至栈顶,它要求一个参数,后面的 0x0A 即是需要推送到栈顶的单字节,注意这里的 0x0A 是16进制,就是我们在代码里写的"a=10"中的10。

第三个字节"3B" : “3B”对应的操作是:"istore_0",作用是将栈顶int 型数值存入第一个局部变量。我们在greeting() 方法中就声明了一个局部变量a,JVM的运行的时候,将这个局部变量a解析,并放置到局部变量表中的第一个位置;上述的0x10 0x0A 指令已经将0x0A 推送到了栈顶了,然后 0x3B指令便将栈顶的0x0A 取出,赋值给局部变量表中的第一个参数,即局部变量a,这样就完成了对局部变量a的赋值;

第4个字符是0xB1 : 对应操作是:“Ireturn”,作用是表示无返回值的方法返回,结束方法调用,这条语句放在方法的机器码最后,表示方法结束调用,返回。

我们可以使用javap -v Simple > Simple.txt,查看反编译信息是怎样显示这一信息的:


参考

这篇文章基本上是参考,非常感谢作者分享,写的很清楚:《Java虚拟机原理图解》



只要自己变优秀了,其他的事情才会跟着好起来(少将6)