《Java虚拟机原理图解》1.5、 class文件中的方法表集合--method方法在class文件中是怎样组织的
读完本文,你将会学到:1、类中定义的method方法是如何在class文件中组织的2、method方法的表示-方法表集合在class文件的什么位置3、类中的method方法的实现代码---即机器码指令存放到哪了,并初步了解机器指令4. 为什么没有在类中定义自己的构造函数,却可以使用new ClassName()构造函数创建对象5. IDE代码提示功能的基本原理
0. 前言
了解JVM虚拟机原理是每一个Java程序员修炼的必经之路。但是由于JVM虚拟机中有很多的东西讲述的比较宽泛,在当前接触到的关于JVM虚拟机原理的教程或者博客中,绝大部分都是充斥的文字性的描述,很难给人以形象化的认知,看完之后感觉还是稀里糊涂的。
感于以上的种种,我打算把我在学习JVM虚拟机的过程中学到的东西,结合自己的理解,总结成《Java虚拟机原理图解》 这个系列,以图解的形式,将抽象的JVM虚拟机的知识具体化,希望能够对想了解Java虚拟机原理的的Java程序员 提供点帮助。
1.概述
方法表集合是指由若干个方法表(method_info)组成的集合。对于在类中定义的若干个,经过JVM编译成class文件后,会将相应的method方法信息组织到一个叫做方法表集合的结构中,字段表集合是一个类数组结构,如下图所示:
2. method方法的描述-方法表集合在class文件中的位置
method方法的描述-方法表集合紧跟在字段表集合的后面(想了解字段表集合的读者可以点击我查看),如下图所示:
接下来让我们看看Method_info 结构体是怎么组织method方法信息的:
3. 一个类中的method方法应该包含哪些信息?----method_info结构体的定义
对于一个方法的表示,我们根据我们可以概括的信息如下所示:
实际上JVM还会对method方法的描述添加其他信息,我们将在后面详细讨论。如上图中的method_info结构体的定义,该结构体的定义跟描述field字段 的field_info结构体的结构几乎完全一致,如下图所示。
方法表的结构体由:访问标志(access_flags)、名称索引(name_index)、描述索引(descriptor_index)、属性表(attribute_info)集合组成。
访问标志(access_flags):
method_info结构体最前面的两个字节表示的访问标志(access_flags),记录这这个方法的作用域、静态or非静态、可变性、是否可同步、是否本地方法、是否抽象等信息,实际上不止这些信息,我们后面会详细介绍访问标志这两个字节的每一位具体表示什么意思。
名称索引(name_index):
紧跟在访问标志(access_flags)后面的两个字节称为名称索引,这两个字节中的值指向了常量池中的某一个常量池项,这个方法的名称以UTF-8格式的字符串存储在这个常量池项中。如public void methodName(),很显然,“methodName”则表示着这个方法的名称,那么在常量池中会有一个CONSTANT_Utf8_info格式的常量池项,里面存储着“methodName”字符串,而mehodName()方法的方法表中的名称索引则指向了这个常量池项。
描述索引(descriptor_index):
描述索引表示的是这个方法的特征或者说是签名,一个方法会有若干个参数和返回值,而若干个参数的数据类型和返回值的数据类型构成了这个方法的描述,其基本格式为: (参数数据类型描述列表)返回值数据类型 。我们将在后面继续讨论。
属性表(attribute_info)集合:
这个属性表集合非常重要,方法的实现被JVM编译成JVM的机器码指令,机器码指令就存放在一个Code类型的属性表中;如果方法声明要抛出异常,那么异常信息会在一个Exceptions类型的属性表中予以展现。Code类型的属性表可以说是非常复杂的内容,也是本文最难的地方。
接下来,我们将一一击破它们,看看它们到底是怎么表示的。
4. 访问标志(access_flags)---记录着method方法的访问信息
访问标志(access_flags)共占有2 个字节,分为 16 位,这 16位 表示的含义如下所示:
举例:某个类中定义了如下方法:
greeting()方法的修饰符有:public、static、synchronized、final 这几个修饰符修饰,那么相对应地,greeting()方法的访问标志中的ACC_PUBLIC、ACC_STATIC、ACC_SYNCHRONIZED、ACC_FINAL标志位都应该是1,即:public static synchronized final void greeting(){ }
从上图中可以看出访问标志的值应该是二进制00000000 00111001,即十六进制0x0039。我们将在文章的最后一个例子中证实这里点。
5. 名称索引和描述符索引----一个方法的签名
紧接着访问标志(access_flags)后面的两个字节,叫做名称索引(name_index),这两个字节中的值是指向了常量池中某个常量池项的索引,该常量池项表示这这个方法名称的字符串。
方法描述符索引(descrptor_index)是紧跟在名称索引后面的两个字节,这两个字节中的值跟名称索引中的值性质一样,都是指向了常量池中的某个常量池项。这两个字节中的指向的常量池项,是表示了方法描述符的字符串。
所谓的方法描述符,实质上就是指用一个什么样的字符串来描述一个方法,方法描述符的组成如下图所示:
关于不同的数据类型的描述符是怎样的,我已经在《Java虚拟机原理图解》1.4 class文件中的字段表集合--field字段在class文件中是怎样组织的 第五部分字段的数据类型表示和字段名称表示 进行过详细的阐释,感兴趣的读者可以前去查看。
举例:对于如下定义的的greeting()方法,我们来看一下对应的method_info结构体中的名称索引和描述符索引信息是怎样组织的。
public static synchronized final void greeting(){
}
如下图所示,method_info结构体的名称索引中存储了一个索引值x,指向了常量池中的第x项,第 x项表示的是字符串"greeting",即表示该方法名称是"greeting";描述符索引中的y 值指向了常量池的第y项,该项表示字符串"()V",即表示该方法没有参数,返回值是void类型。
6.属性表集合--记录方法的机器指令和抛出异常等信息
属性表集合记录了某个方法的一些属性信息,这些信息包括:
- 这个方法的代码实现,即方法的可执行的机器指令
- 这个方法声明的要抛出的异常信息
- 这个方法是否被@deprecated注解表示
- 这个方法是否是编译器自动生成的
属性表(attribute_info)结构体的一般结构如下所示:
6.1 Code类型的属性表--method方法中的机器指令的信息
Code类型的属性表(attribute_info)可以说是class文件中最为重要的部分,因为它包含的是JVM可以运行的机器码指令,JVM能够运行这个类,就是从这个属性中取出机器码的。除了要执行的机器码,它还包含了一些其他信息,如下所示:
Code属性表的组成部分:
机器指令----code:
目前的JVM使用一个字节表示机器操作码,即对JVM底层而言,它能表示的机器操作码不多于2的 8 次方,即 256个。class文件中的机器指令部分是class文件中最重要的部分,并且非常复杂,本文的重点不止介绍它,我将专门在一片博文中讨论它,敬请期待。
异常处理跳转信息---exception_table:
如果代码中出现了try{}catch{}块,那么try{}块内的机器指令的地址范围记录下来,并且记录对应的catch{}块中的起始机器指令地址,当运行时在try块中有异常抛出的话,JVM会将catch{}块对应懂得其实机器指令地址传递给PC寄存器,从而实现指令跳转;
Java源码行号和机器指令的对应关系---LineNumberTable属性表:
编译器在将java源码编译成class文件时,会将源码中的语句行号跟编译好的机器指令关联起来,这样的class文件加载到内存中并运行时,如果抛出异常,JVM可以根据这个对应关系,抛出异常信息,告诉我们我们的源码的多少行有问题,方便我们定位问题。这个信息不是运行时必不可少的信息,但是默认情况下,编译器会生成这一项信息,如果你项取消这一信息,你可以使用-g:none 或-g:lines来取消或者要求设置这一项信息。如果使用了-g:none来生成class文件,class文件中将不会有LineNumberTable属性表,造成的影响就是 将来如果代码报错,将无法定位错误信息报错的行,并且如果项调试代码,将不能在此类中打断点(因为没有指定行号。)
局部变量表描述信息----LocalVariableTable属性表:
局部变量表信息会记录栈帧局部变量表中的变量和java源码中定义的变量之间的关系,这个信息不是运行时必须的属性,默认情况下不会生成到class文件中。你可以根据javac指令的-g:none或者-g:vars选项来取消或者设置这一项信息。
它有什么作用呢? 当我们使用IDE进行开发时,最喜欢的莫过于它们的代码提示功能了。如果在项目中引用到了第三方的jar包,而第三方的包中的class文件中有无LocalVariableTable属性表的区别如下所示:
举例:
如下定义Simple类,使用javac -g:none Simple.java 编译出Simple.class 文件,并使用javap -v Simple > Simple.txt 查看反编译的信息,然后看Simple.class文件中的方法表集合是怎样组织的:
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文件中会显示有两个方法呢??
在Simple.classz中出现了两个方法表,分别代表构造方法<init>()和 greeting()方法,现在让我们分别来讨论这两个方法:
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;
B. 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;
6.2 Exceptions类型的属性表----method方法声明的要抛出的异常信息
有些方法在定义的时候,会声明该方法会抛出什么类型的异常,如下定义一个Interface接口,它声明了sayHello()方法,抛出Exception异常:
package com.louis.jvm; public interface Interface { public void sayHello() throws Exception; }
现在让我们看一下Exceptions类型的属性表(attribute_info)结构体是怎样组织的:
如上图所示,Exceptions类型的属性表(attribute_info)结构体由一下元素组成:
属性名称索引(attribute_name_index):占有 2个字节,其中的值指向了常量池中的表示"Exceptions"字符串的常量池项;
属性长度(attribute_length):它比较特殊,占有4个字节,它的值表示跟在其后面多少个字节表示异常信息;
异常数量(number_of_exceptions):占有2 个字节,它的值表示方法声明抛出了多少个异常,即表示跟在其后有多少个异常名称索引;
异常名称索引(exceptions_index_table):占有2个字节,它的值指向了常量池中的某一项,该项是一个CONSTANT_Class_info类型的项,表示这个异常的完全限定名称;
举例:
将上面定义的Interface接口类编译成class文件,然后我们查看Interface.class文件,找出方法表集合所在位置和相应的数据,并辅助javap -v Inerface 查看常量池信息,如下图所示:
由于sayHello()方法是在的Interface接口类中声明的,它没有被实现,所以它对应的方法表(method_info)结构体中的属性表集合中没有Code类型的属性表。
注:
1. 方法计数器(methods_count)中的值为0x0001,表明其后的方法表(method_info)就一个,即我们就定义了一个方法,其后会紧跟着一个方法表(method_info)结构体;
2. 方法的访问标志(access_flags)的值是0x0401,二进制是00000100 00000001,第6位和第16位是1,对应上面的标志位信息,可以得出它的访问标志符有:ACC_ABSTRACT、ACC_PUBLIC。细心的读者可能会发现,在上面声明的sayHello()方法中并没有声明为abstract类型啊。确实如此,这是因为编译器对于接口内声明的方法自动加上ACC_ABSTRACT标志。
3. 名称索引(name_index)中的值为0x0005,0x0005指向了常量池的第5项,第五项表示的字符串为“sayHello”,即表示的方法名称是sayHello
4. 描述符索引(descriptor_index)中的值为0x0006,0x0006指向了常量池中的第6项,第6项表示的字符串为“()V” 表示这个方法的无入参,返回值为void类型
5. 属性表计数器(attribute_count)中的值为0x0001,表示后面的属性表的个数就1个,后面紧跟着一个attribute_info结构体;
6. 属性表(attribute_info)中的属性名称索引(attribute_name_index)中的值为0x0007,0x0007指向了常量池中的第7 项,第 7项指向字符串“Exceptions”,即表示该属性表表示的异常信息;
7. 属性长度(attribute_length)中的值为:0x00000004,即后续的4个字节将会被解析成属性值;
8. 异常数量(number_of_exceptions)中的值为0x0001,表示这个方法声明抛出的异常个数是1个;
9.异常名称索引(exception_index_table)中的值为0x0008,指向了常量池中的第8项,第8项表示的是CONSTANT_Class_info类型的常量池项,表示“java/lang/Exception”,即表示此方法抛出了java.lang.Exception异常。
7. IDE代码提示功能实现的基本原理
现在对于企业级的开发,开发者们越来越依赖IDE如Intellij IDEA、Eclipse、MyEclipse、NetBeans等,利用他们提供的高级功能,可以极大地提高编码的速度和效率。
每个IDE都提供了代码提示功能,它们实现的基本原理其实就是IDE针对它们项目下的包中所有的class文件进行建模,解析出它们的方法信息,当我们一定的条件时,IDE会自动地将合适条件的方法列表展示给开发者,供开发者使用。
在上面将Code属性表的时候也讲了,如果编译的第三方包,没有LocalVariableTable属性表信息,IDE的提示信息会稍有不同:
8. 写在后面
以上就是Class文件的方法表集合的全部内容。
读者可能觉得本文关于方法表的Code属性表讨论的不够深入,在讨论Code属性表的时候,我简单介绍了它的两个属性表LineNumberTable 和LocalVariableTable这两个在有什么实际作用,但是没有详细第介绍它们,并且在列举的例子中,刻意地使用了 -g:none 选项 ,以使生成的class文件没有这两项信息,这么做是因为Code 属性太过复杂,而本文主要是想让读者了解的是 方法表集合,所以就生成了最精简的Code属性表,以减少读者的负担。
接下来的一篇文章,我打算专门来讨论Code属性表,揭开Code属性表的所有秘密,敬请关注~~
本文还引出了一个需要讨论的话题:就是Code属性表中的机器指令,机器指令的运行要依赖于JVM体系结构的设计机制,理解机器指令的运行机制,这将是根非常非常难啃的骨头.......
-----------------------------------------------------------------------------------------------------------------------------------------
更多推荐
所有评论(0)