#<center>深刻理解Class中--方法、属性表集合</center>html
以前有关class文件已经写了两篇博客:java
一、【JVM虚拟机】(5)---深刻理解JVM-Class中常量池jvm
二、【JVM虚拟机】(6)---深刻理解Class中访问标志、类索引、父类索引、接口索引函数
三、【JVM虚拟机】(7)---深刻理解Class中-属性集合学习
那么这篇博客主要讲有关 方法表集合 相关的理解和代码示例。ui
方法表集合
: 告知该方法是什么修饰符修饰?是否有方法值?返回类型是什么?方法名称,方法参数,还有就是方法内的一些信息。spa
方法表集合
:方法表集合和属性表集合其实很类似,都是由一个计数器(方法)
和若干个方法表
构成,只不过方法表的结构相对复杂不少。3d
方法表的结构体
:访问标志(access_flags)、名称索引(name_index)、描述索引(descriptor_index)、属性表(attribute_info)集合组成。code
method_info { u2 access_flags; u2 name_index; u2 descriptor_index; u2 attributes_count; attribute_info attributes[attributes_count]; }
1)、访问标志
htm
很少说了,和属性中的其实差很少,只是有些修饰符不同。
<img src="https://img2018.cnblogs.com/blog/1090617/201904/1090617-20190417235540325-1150347478.jpg" style="border: 1px dashed rgb(204, 200, 200);" width="600" height="326">
2)、名称索引
就是指这个方法的名称。如:'public void getXX()'中,getXX就是名称索引。名称索引占两个字节,这个方法的名称以UTF-8格式的字符串存储在这个常量池项中。
3)、描述索引
指这个方法的返回值,方法内参数信息。一个方法的描述包含若干个参数的数据类型和返回值的数据类型。
4)、属性表(attribute_info)集合
下面讲
##<font color=#FFD700> 2、属性表集合 </font>
在Class文件、字段表、方法表均可以携带本身的属性表集合,用于描述某些场景专有的信息。 在方法表中, 属性表集合记录了某个方法的一些属性信息,这些信息包括:
方法的可执行的机器指令
抛出的异常信息
被@deprecated注解表示
编译器自动生成的
**属性表(attribute_info)**结构体的通常结构以下所示:
<img src="https://img2018.cnblogs.com/blog/1090617/201904/1090617-20190417235716144-1459133840.jpg" style="border: 1px dashed rgb(204, 200, 200);" width="600" height="550">
属性表占着很是大的一部分且定义了众多属性,上面只列举了4个,查看完成的:JDK1.7版本中21项属性表集合简要介绍
下面介绍两个重要的属性
code属性比较复杂,它是通过编译器编译成字节码指令以后的数据。就是说java程序中的方法体通过javac编译器处理后,最终变成字节码存储在Code属性内
。
并不是全部方法表都有这个属性,接口和抽象类就没有【没有方法体】。 Code属性是Class文件中最重要的一个属性,在Class文件中,Code属性用于描述代码,全部的其它数据项目都用来描述元数据,了解code属性对了解字 节码执行引擎来讲是必要基础。
<img src="https://img2018.cnblogs.com/blog/1090617/201904/1090617-20190417235758419-1368030226.png" style="border: 1px dashed rgb(204, 200, 200);" width="600" height="365">
Code属性表的组成部分:
机器指令——code
目前的JVM使用一个字节表示机器操做码,即对JVM底层而言,它能表示的机器操做码很少于2的 8 次方,即 256个。class文件中的机器指令部分是class文件中最重要的部分,而且很是复杂。
异常处理跳转信息
若是代码中出现了**try{}catch{}
块,那么try{}块内的机器指令的地址范围记录下来,而且记录对应的catch{}块中的起始机器指令地址,当运行时在try块中有异常抛出的话,JVM会将catch{}块对应懂得其实机器指令地址传递给PC寄存器**,从而实现指令跳转;
Java源码行号和机器指令的对应关系
---LineNumberTable属性表
编译器在将java源码编译成class文件时,会将源码中的语句行号跟编译好的机器指令关联起来,这样的class文件加载到内存中并运行时,若是抛出异常,JVM能够根据这个对应关系,抛出异常信息,告诉咱们咱们的源码的多少行有问题,方便咱们定位问题。
Code属性表结构体:
一、attribute_name_index
: 属性名称索引,占有2个字节,其内的值指向了常量池中的某一项,该项表示字符. 串“Code”;
二、attribute_length
: 属性长度,占有 4个字节,其内的值表示后面有多少个字节是属于此Code属性表的;
三、max_stack
: 操做数栈深度的最大值,占有 2 个字节,在方法执行的任意时刻,操做数栈都不该该超过这个值,虚拟机的运行的时候,会根据这个值来设置该方法对应的栈帧(Stack Frame)中的操做数栈的深度;
四、max_locals
最大局部变量数目,占有 2个字节,其内的值表示局部变量表所须要的存储空间大小;
五、code_length
: 机器指令长度,占有 4 个字节,表示跟在其后的多少个字节表示的是机器指令;
六、code
机器指令区域,该区域占有的字节数目由 code_length中的值决定。JVM最底层的要执行的机器指令就存储在这里;
七、exception_table_length
: 显式异常表长度,占有2个字节,若是在方法代码中出现了try{} catch()形式的结构,该值不会为空,紧跟其后会跟着若干个exception_table结构体,以表示异常捕获状况;
八、exception_table
: 显式异常表,占有8 个字节,start_pc,end_pc,handler_pc中的值都表示的是PC计数器中的指令地址。exception_table表示的意思是:若是字节码从第start_pc行到第end_pc行之间出现了catch_type所描述的异常类型,那么将跳转到handler_pc行继续处理。
九、attribute_count
: 属性计数器,占有 2 个字节,表示Code属性表的其余属性的数目
十、attribute_info
: 表示Code属性表具备的属性表,它主要分为两个类型的属性表:“LineNumberTable”类型和“LocalVariableTable”类型。“LineNumberTable”类型的属性表记录着Java源码和机器指令之间的对应关系“LocalVariableTable”类型的属性表记录着局部变量描述
之因此学习这个,是由于后面类加载机制有联系到这个属性
这个属性的做用是通知虚拟机为静态变量赋值,只要被static修饰的变量才有这个属性,【有该属性的字段必须有ACC_STATIC访问标志,反过来不必定】。
对于 "int x = 123" 和 "static int x =123"这类代码在平常编写中很常见,但虚拟机对这两种变量赋值的时刻却不一样。 对于非static变量[实例变量],是在实例构造器<init>进行 对于类变量,有两种方式选择 ①在类构造器<clinit>方法中赋值 ②使用ConstantValue属性初始化 目前Sun javac编译器是这么作的【具体咋作不知道 = =】,若是同时使用final和static修饰一个变量[这种修饰就至关于个常量],而且是String或基本类型,就使用②, 若是没有被final修饰或不是基本类型和String,就选择①在<clinit>方法中初始化 //有关这点我在上篇博客举过例子,最后几句话也对这个解释的很清楚。
<br>
##<font color=#FFD700> 3、示例 </font>
有关方法的代码示例,我就不亲自测了,由于有位博主写的已经很清晰啦,我本身写也没那么清晰。
public static synchronized final void greeting(){ }
**greeting()**方法的修饰符有:public、static、synchronized、final 这几个修饰符修饰,那么相对地,
greeting()方法的访问标志中的ACC_PUBLIC
、ACC_STATIC
、ACC_SYNCHRONIZED
、ACC_FINAL
标志位都应该是1
从上面第一张图能够得出,该访问标志的值应该是十六进制0x0039
。
紧接着访问标志(access_flags)后面的两个字节,叫作名称索引(name_index),这两个字节中的值是指向了常量池中某个常量池项的索引,该常量池项表示这这个方法名称的字符串。
方法描述符索引(descrptor_index)是紧跟在名称索引后面的两个字节,这两个字节中的值跟名称索引中的值性质同样,都是指向了常量池中的某个常量池项。这两个字节中的指向的常量池项,是表示了方法描述符的字符串。
<img src="https://img2018.cnblogs.com/blog/1090617/201904/1090617-20190417235837914-1194106857.jpg" style="border: 1px dashed rgb(204, 200, 200);" width="600" height="234">
package com.louis.jvm; public class Simple { public static synchronized final void greeting(){ int a = 10; } }
1)、 Simple.class文件以下所示
<img src="https://img2018.cnblogs.com/blog/1090617/201904/1090617-20190417235905646-2145707810.png" style="border: 1px dashed rgb(204, 200, 200);" width="600" height="300">
注意
:方法表集合的头两个字节,即方法表计数器(method_count)的值是0x0002,它表示该类中有2 个方法。注意到,咱们的Simple.java中就定义了一个greeting()方法,为何class文件中会显示有两个方法呢?
缘由
:若是咱们在类中没有定义实例化构造方法,JVM编译器在将源码编译成class文件时,会自动地为这个类添加一个不带参数的实例化构造方法,这种添加是字节码级别的,JVM对全部的类实例化构造方法名采用了相同的名称:“<init>”。若是咱们显式地以下定义Simple()构造函数,这个类编译出来的class文件和上面的不带Simple构造方法的Simple类生成的class文件是彻底相同的。
2)、Simple.class 中的<init>() 方法
<img src="https://img2018.cnblogs.com/blog/1090617/201904/1090617-20190417235935464-1875058370.png" style="border: 1px dashed rgb(204, 200, 200);" width="700" height="800">
解释:
一、方法访问标志(access_flags)
: 占有 2个字节,值为0x0001,即标志位的第 16 位为 1,因此该**<init>()**方法的修饰符是:ACC_PUBLIC;
二、 名称索引(name_index)
: 占有 2 个字节,值为 0x0004,指向常量池的第 4项,该项表示字符串'<init>',即该方法的名称是'<init>';
三、描述符索引(descriptor_index)
: 占有 2 个字节,值为0x0005,指向常量池的第 5 项,该项表示字符串“()V”,即表示该方法不带参数,而且无返回值(构造函数确实也没有返回值);
四、属性计数器(attribute_count)
: 占有 2 个字节,值为0x0001,表示该方法表中含有一个属性表,后面会紧跟着一个属性表;
五、属性表的名称索引(attribute_name_index)
:占有 2 个字节,值为0x0006,指向常量池中的第6 项,该项表示字符串“Code”,表示这个属性表是Code类型的属性表;
六、 属性长度(attribute_length)
:占有4个字节,值为0x0000 0011,即十进制的 17,代表后续的 17 个字节能够表示这个Code属性表的属性信息;
七、 操做数栈的最大深度(max_stack)
:占有2个字节,值为0x0001,表示栈帧中操做数栈的最大深度是1;
八、局部变量表的最大容量(max_variable)
:占有2个字节,值为0x0001, JVM在调用该方法时,根据这个值设置栈帧中的局部变量表的大小;
九、 机器指令数目(code_length)
:占有4个字节,值为0x0000 0005,表示后续的5 个字节 0x2A 、0xB七、 0x00、0x0一、0xB1表示机器指令;
十、机器指令集(code[code_length])
:这里共有 5个字节,值为0x2A 、0xB七、 0x00、0x0一、0xB1;
十一、显式异常表集合(exception_table_count)
: 占有2 个字节,值为0x0000,表示方法中没有须要处理的异常信息;
十二、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() 方法
解释:
一、方法访问标志(access_flags)
: 占有 2个字节,值为 0x0039 ,即二进制的00000000 00111001,即标志位的第十一、十二、1三、16位为1,根据上面讲的方法标志位的表示,能够获得该**greeting()**方法的修饰符有:ACC_SYNCHRONIZED、ACC_FINAL、ACC_STATIC、ACC_PUBLIC;
二、 名称索引(name_index)
: 占有 2 个字节,值为 0x0007,指向常量池的第 7 项,该项表示字符串“greeting”,即该方法的名称是“greeting”;
三、描述符索引(descriptor_index)
: 占有 2 个字节,值为0x0005,指向常量池的第 5 项,该项表示字符串“()V”,即表示该方法不带参数,而且无返回值;
四、属性计数器(attribute_count)
: 占有 2 个字节,值为0x0001,表示该方法表中含有一个属性表,后面会紧跟着一个属性表;
五、属性表的名称索引(attribute_name_index)
:占有 2 个字节,值为0x0006,指向常量池中的第6 项,该项表示字符串“Code”,表示这个属性表是Code类型的属性表;
六、属性长度(attribute_length)
:占有4个字节,值为0x0000 0010,即十进制的16,代表后续的16个字节能够表示这个Code属性表的属性信息;
七、操做数栈的最大深度(max_stack)
:占有2个字节,值为0x0001,表示栈帧中操做数栈的最大深度是1;
八、 局部变量表的最大容量(max_variable)
:占有2个字节,值为0x0001, JVM在调用该方法时,根据这个值设置栈帧中的局部变量表的大小;
九、器指令数目(code_length)
:占有4 个字节,值为0x0000 0004,表示后续的4个字节0x十、 0x0A、 0x3B、0xB1的是表示机器指令;
十、机器指令集(code[code_length])
:这里共有4 个字节,值为0x十、 0x0A、 0x3B、0xB1 ;
十一、显式异常表集合(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,查看反编译信息是怎样显示这一信息的:
<br>
###<font color=#FFD700> 参考 </font>
这篇文章基本上是参考,很是感谢做者分享,写的很清楚:《Java虚拟机原理图解》 <br> <br>
只要本身变优秀了,其余的事情才会跟着好起来(少将6)
原文出处:https://www.cnblogs.com/qdhxhz/p/10727117.html