class字节码结构(四)(方法集合的结构)

时间:2023-01-22 09:19:02

《Java虚拟机原理图解》1.5、 class文件中的方法表集合--method方法在class文件中是怎样组织的

一个类有多个方法,所以方法肯定是一个集合。

目标是:

1,了解方法在字节码中结构,在字节码中的位置。

2,了解方法体会怎样被编译成机器指令

3,开发环境是如何根据字节码产生代码提示功能。

1,方法位置和结构:计数器和方法信息集合

class字节码结构(四)(方法集合的结构)

2,方法信息包括那些内容:

class字节码结构(四)(方法集合的结构)

这些信息对应的方法信息结构和字段的结构类似:

class字节码结构(四)(方法集合的结构)

这些内容包括:

访问标志(access_flags):记录这个方法的作用域、静态or非静态、可变性、是否可同步、是否本地方法、是否抽象等信息,实际上不止这些信息(略)
名称索引(name_index):名字的索引对应CONSTANT_Utf8_info格式的常量池项,里面存储着“methodName”字符串。
描述索引(descriptor_index):是这个方法签名的索引。签名是由若干个参数的数据类型和返回值的数据类型共同构成的。
常量池的格式为:(参数数据类型描述列表)返回值数据类型 。
属性表(attribute_info)集合:主要存放是方法体编译后的机器指令和方法抛出的异常信息。属性集合是由不同类型的属性组合成的。

访问标志通过2个字节表示,字节的每一位具有特定的含义。

class字节码结构(四)(方法集合的结构)

侧重说下:属性表集合:

属性表集合记录了某个方法的一些属性信息,这些信息包括:
1,这个方法的代码实现,即方法的可执行的机器指令
2,这个方法声明的要抛出的异常信息
3,这个方法是否被@deprecated注解表示(有点疑问,这个不是可以放在访问标志中)
4,这个方法是否是编译器自动生成的(有点疑问,这个不是可以放在在访问标志中)

属性集合是分类型的,每个元素代表一种类型:分别对应不同的属性名称。

class字节码结构(四)(方法集合的结构)

Code类型的属性表(也就是属性值的内容主要是方法体的机器指令,还有其他信息):

class字节码结构(四)(方法集合的结构)

主要是机器指令,异常处理(捕获的异常如何处理),code属性值还有2个属性项(用来记录指令对应的行号;记录方法参数的名字)

下面是Code属性表的组成部分详细说明:

机器指令----code:
目前的JVM使用一个字节表示机器操作码,即对JVM底层而言,它能表示的机器操作码不多于2的 次方,即 256个。 异常处理跳转信息---exception_table:
记录try{}块内的机器指令的地址范围和对应的catch{}块中的起始机器指令地址,
当运行时在try块中有异常抛出的话,JVM会将catch{}块对应指令地址传递给PC寄存器,从而实现指令跳转; Java源码行号和机器指令的对应关系---LineNumberTable属性表:
编译器会将源码中的语句行号跟编译好的机器指令关联起来,如果抛出异常,知道多少行有问题。默认情况下,编译器会生成这一项信息。
可以使用-g:none 或-g:lines来取消或者要求设置这一项信息。如果使用了-g:none来生成class文件,class文件中将不会有LineNumberTable属性表,
造成的影响就是:,代码报错,将无法定位错误信息报错的行;,如果项调试代码,将不能在此类中打断点(因为没有指定行号。) 局部变量表描述信息----LocalVariableTable属性表:
记录的源码中方法参数的名称(即局部变量的名称),如果没有这个属性,字节码的变量名称可能就是a,b,c,d之类的。
默认情况下不会生成到class文件中。你可以根据javac指令的-g:none或者-g:vars选项来取消或者设置这一项信息。

有没有LocalVariableTable属性体现在哪个方面(造成可读性比较差):

1,反编译后看到的变量名字是没有具体意义的字母(比如 arg1,arg0,)

2,开发环境代码提供功能,显示的方法参数的名字是没有具体意义的字母。

class字节码结构(四)(方法集合的结构)

对上面整个code属性结构的一个说明:

. attribute_name_index,属性名称索引,占有2个字节,其内的值指向了常量池中的某一项,该项表示字符串“Code”;
. attribute_length,属性长度,占有 4个字节,其内的值表示后面有多少个字节是属于此Code属性表的;
. max_stack,操作数栈深度的最大值,占有 个字节,
在方法执行的任意时刻,操作数栈都不应该超过这个值,虚拟机的运行的时候,会根据这个值来设置该方法对应的栈帧(Stack Frame)中的操作数栈的深度;
. max_locals,最大局部变量数目,占有 2个字节,其内的值表示局部变量表所需要的存储空间大小;
. code_length,机器指令长度,占有 个字节,表示跟在其后的多少个字节表示的是机器指令;
. 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,属性计数器,占有 个字节,表示Code属性表的其他属性的数目
. attribute_info,表示Code属性表具有的属性表,它主要分为两个类型的属性表:“LineNumberTable”类型和“LocalVariableTable”类型。
“LineNumberTable”类型的属性表记录着Java源码和机器指令之间的对应关系
“LocalVariableTable”类型的属性表记录着局部变量描述

关于构造方法的生成:

如果我们在类中没有定义实例化构造方法,JVM编译器在将源码编译成class文件时,
会自动地为这个类添加一个不带参数的实例化构造方法,
JVM对所有的类实例化构造方法名采用了相同的名称:“<init>”。

补充:

如果我们在类中使用到了static修饰的代码块,那么,JVM会在class文件中生成一个“<cinit>()”构造方法。

一个方法的例子:

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

生成字节码过程中会自动生成无参构造器:

public Simple(){
super();
}

下面是构造器的字节码结构:

class字节码结构(四)(方法集合的结构)

. 方法访问标志(access_flags): 占有 2个字节,值为0x0001,即标志位的第  位为 ,所以该<init>()方法的修饰符是:ACC_PUBLIC;
. 名称索引(name_index): 占有 个字节,值为 0x0004,指向常量池的第 4项,该项表示字符串“<init>”,即该方法的名称是“<init>”;
.描述符索引(descriptor_index): 占有 个字节,值为0x0005,指向常量池的第 项,该项表示字符串“()V”,
即表示该方法不带参数,并且无返回值(构造函数确实也没有返回值);
. 属性计数器(attribute_count): 占有 个字节,值为0x0001,表示该方法表中含有一个属性表,后面会紧跟着一个属性表;
. 属性表的名称索引(attribute_name_index):占有 个字节,值为0x0006,
指向常量池中的第6 项,该项表示字符串“Code”,表示这个属性表是Code类型的属性表;
. 属性长度(attribute_length):占有4个字节,值为0x0000 ,即十进制的 ,表明后续的 个字节可以表示这个Code属性表的属性信息;
. 操作数栈的最大深度(max_stack):占有2个字节,值为0x0001,表示栈帧中操作数栈的最大深度是1;
. 局部变量表的最大容量(max_variable):占有2个字节,值为0x0001, JVM在调用该方法时,根据这个值设置栈帧中的局部变量表的大小;
. 机器指令数目(code_length):占有4个字节,值为0x0000 ,表示后续的5 个字节 0x2A 、0xB7、 0x00、0x01、0xB1表示机器指令;
.机器指令集(code[code_length]):这里共有 5个字节,值为0x2A 、0xB7、 0x00、0x01、0xB1;
. 显式异常表集合(exception_table_count): 占有2 个字节,值为0x0000,表示方法中没有需要处理的异常信息;
. Code属性表的属性表集合(attribute_count): 占有2 个字节,值为0x0000,表示它没有其他的属性表集合,
因为我们使用了-g:none 禁止编译器生成Code属性表的 LineNumberTable 和LocalVariableTable;

使用javap -v Simple > Simple.txt,查看二进制对应的字节码显示:

class字节码结构(四)(方法集合的结构)

JVM的机器指令中的操作码(Opcode)规定只用1个字节表示,所以JVM的指令最多不超过256 个。
第一个字节 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”,作用是表示无返回值的方法返回,结束方法调用,这条语句放在方法的机器码最后,表示方法结束调用,返回。

Simple.class 中的greeting() 方法的结构:

class字节码结构(四)(方法集合的结构)

. 方法访问标志(access_flags): 占有 2个字节,值为 0x0039 ,即二进制的00000000 ,即标志位的第11、、、16位为1,
根据上面讲的方法标志位的表示,可以得到该greeting()方法的修饰符有:ACC_SYNCHRONIZED、ACC_FINAL、ACC_STATIC、ACC_PUBLIC;
. 名称索引(name_index): 占有 个字节,值为 0x0007,指向常量池的第 项,该项表示字符串“greeting”,即该方法的名称是“greeting”;
. 描述符索引(descriptor_index): 占有 个字节,值为0x0005,指向常量池的第 项,该项表示字符串“()V”,即表示该方法不带参数,并且无返回值;
. 属性计数器(attribute_count): 占有 个字节,值为0x0001,表示该方法表中含有一个属性表,后面会紧跟着一个属性表;
. 属性表的名称索引(attribute_name_index):占有 个字节,值为0x0006,指向常量池中的第6 项,该项表示字符串“Code”,表示这个属性表是Code类型的属性表;
. 属性长度(attribute_length):占有4个字节,值为0x0000 ,即十进制的16,表明后续的16个字节可以表示这个Code属性表的属性信息;
. 操作数栈的最大深度(max_stack):占有2个字节,值为0x0001,表示栈帧中操作数栈的最大深度是1;
. 局部变量表的最大容量(max_variable):占有2个字节,值为0x0001, JVM在调用该方法时,根据这个值设置栈帧中的局部变量表的大小;
. 机器指令数目(code_length):占有4 个字节,值为0x0000 ,表示后续的4个字节0x10、 0x0A、 0x3B、0xB1的是表示机器指令;
.机器指令集(code[code_length]):这里共有4 个字节,值为0x10、 0x0A、 0x3B、0xB1 ;
.显式异常表集合(exception_table_count): 占有2 个字节,值为0x0000,表示方法中没有需要处理的异常信息;
.Code属性表的属性表集合(attribute_count): 占有2 个字节,值为0x0000,表示它没有其他的属性表集合,
因为我们使用了-g:none 禁止编译器生成Code属性表的 LineNumberTable 和LocalVariableTable;

重点说下:机器指令,字节码如下:

class字节码结构(四)(方法集合的结构)

第一个字节 0x10 对应的操作是"bipush"," 作用是将单字节的常量值(-128~127) 推送至栈顶,它要求一个参数,
后面的 0x0A 即是需要推送到栈顶的单字节,
注意这里的 0x0A 是16进制,就是我们在代码里写的"a="中的10。
第三个字节"3B",“3B”对应的操作是:"istore_0",作用是将栈顶int 型数值存入第一个局部变量。
我们在greeting() 方法中就声明了一个局部变量a,
JVM的运行的时候,将这个局部变量a解析,并放置到局部变量表中的第一个位置; 上述的0x10 0x0A 指令已经将0x0A 推送到了栈顶了,然后 0x3B指令便将栈顶的0x0A 取出,赋值给局部变量表中的第一个参数,即局部变量a,
这样就完成了对局部变量a的赋值;
接着第4个字符是0xB1 ,对应操作是:“Ireturn”,作用是表示无返回值的方法返回,结束方法调用,这条语句放在方法的机器码最后,表示方法结束调用,返回。

总结:

1,对于局部变量的值就放在->字节的方法集合中的->code属性值中的->机器指令中

2,对于局部变量,是根据位置来区别的,指令中没有具体的名字(如果要有需要用到LocalVariableTable属性)

Exceptions类型的属性表----method方法声明的要抛出的异常信息:

注意,异常类型的属性和code类型中异常信息是有区别的:

code类型中的异常是:方法中捕获异常及如何处理。
这里的异常属性表是:方法上主动抛出了什么异常的信息。

例子:

package com.louis.jvm;
public interface Interface {
public void sayHello() throws Exception;
}

class字节码结构(四)(方法集合的结构)

如上图所示,Exceptions类型的属性表(attribute_info)结构体由一下元素组成:
属性名称索引(attribute_name_index):占有 2个字节,其中的值指向了常量池中的表示"Exceptions"字符串的常量池项;
属性长度(attribute_length):它比较特殊,占有4个字节,它的值表示跟在其后面多少个字节表示异常信息;
异常数量(number_of_exceptions):占有2 个字节,它的值表示方法声明抛出了多少个异常,即表示跟在其后有多少个异常名称索引;
异常名称索引(exceptions_index_table):占有2个字节,它的值指向了常量池中的某一项,该项是一个CONSTANT_Class_info类型的项,
表示这个异常的完全限定名称;

属性长度问题:

如果某个方法定义中,没有声明抛出异常,那么,表示该方法的方法表(method_info)结构体中的属性表集合中不会有Exceptions类型的属性表;
换句话说,如果方法声明了要抛出的异常,方法表(method_info)结构体中的属性表集合中必然会有Exceptions类型的属性表,并且该属性表中的异常数量不小于1。
我们假设异常数量中的值为 N,那么后面的异常名称索引的数量就为N,它们总共占有的字节数为N*,而异常数量占有2个字节,
那么将有下面的这个关系式:
属性长度(attribute_length)中的值= + *异常数量(number_of_exceptions)中的值
Exceptions类型的属性表(attribute_info)的长度=++属性长度(attribute_length)中的值

二进制和字节码结构:

class字节码结构(四)(方法集合的结构)

由于sayHello()方法是在的Interface接口类中声明的,它没有被实现,所以它对应的方法表(method_info)结构体中的属性表集合中没有Code类型的属性表。
注:
. 方法计数器(methods_count)中的值为0x0001,表明其后的方法表(method_info)就一个,
即我们就定义了一个方法,其后会紧跟着一个方法表(method_info)结构体;
. 方法的访问标志(access_flags)的值是0x0401,二进制是00000100 ,第6位和第16位是1,
对应上面的标志位信息,可以得出它的访问标志符有:ACC_ABSTRACT、ACC_PUBLIC。
细心的读者可能会发现,在上面声明的sayHello()方法中并没有声明为abstract类型啊。确实如此,
这是因为编译器对于接口内声明的方法自动加上ACC_ABSTRACT标志。
. 名称索引(name_index)中的值为0x0005,0x0005指向了常量池的第5项,第五项表示的字符串为“sayHello”,即表示的方法名称是sayHello
. 描述符索引(descriptor_index)中的值为0x0006,0x0006指向了常量池中的第6项,第6项表示的字符串为“()V” 表示这个方法的无入参,返回值为void类型
. 属性表计数器(attribute_count)中的值为0x0001,表示后面的属性表的个数就1个,后面紧跟着一个attribute_info结构体;
. 属性表(attribute_info)中的属性名称索引(attribute_name_index)中的值为0x0007,0x0007指向了常量池中的第7 项,
第 7项指向字符串“Exceptions”,即表示该属性表表示的异常信息;
. 属性长度(attribute_length)中的值为:0x00000004,即后续的4个字节将会被解析成属性值;
. 异常数量(number_of_exceptions)中的值为0x0001,表示这个方法声明抛出的异常个数是1个;
.异常名称索引(exception_index_table)中的值为0x0008,指向了常量池中的第8项,
第8项表示的是CONSTANT_Class_info类型的常量池项,表示“java/lang/Exception”,即表示此方法抛出了java.lang.Exception异常。

补充:局部变量的值存放位置:(查看字节码是以机器指令的方式在code属性中)

code属性结构中还有个局部变量表记录局部变量一些信息:Java字节码浅析(—)

LocalVariableTable:
Start Length Slot Name Signature
i I