深入理解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底层而言,它能表示的机器操作码不多于2的 8 次方,即 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_PUBLIC
、ACC_STATIC
、ACC_SYNCHRONIZED
、ACC_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对所有的类实例化构造方法名采用了相同的名称:“
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)
```