标签:编译失败 turn 处理机 file try 现在 tar 两种 存储空间
属性表(attribute_info)在前面的讲解之中已经出现过数次,在Class文件、字段表、方法表都可以携带自己的属性表集合,以用于描述某些场景专有的信息。
例如在39,class文件结构----- 方法表集合 我们讲到的exception属性就属于方法表属性。
38,class文件结构----字段表集合讲到的ConstantValue类型的属性表就属于字段表属性。
在这里我们可以不管是class文件,还是字段表集合还是方法表集合,只要需要用到属性的地方,都会有属性表集合的。下面我们来看张表。
上面的表我们可以看到所有的属性表的集合,并且它属于哪个使用位置的。由于属性表的东西太多,所以这里我们重点来讲一下方法集合中的code属性,这个最难,也是最有代表性的。
属性表(attribute_info)在Class文件、字段表、方法表中都可以携带自己的属性表集合,以用于描述某些场景专有信息。对于每个属性,它的名称需要从常量池中引用一个 CONSTANT_Utf_info 类型
由于网上没有找到合适的教程,我们就以深入理解java虚拟机额这本书来管中窥豹一下。
Java程序方法体中的代码经过Javac编译器处理后,最终变为字节码指令存储在Code属性内。Code属性出现在方法表的属性集合之中,但并非所有的方法表都必须存在这个属性,譬如接口或者抽象类中的方法就不存在Code属性,如果方法表有Code属性存在,那么它的结构
将如表6-15所示。
1,attribute_name_index是一项指向CONSTANT_Utf8_info型常量的索引,常量值固定为"Code",它代表了该属性的属性名称,attribute_length指示了属性值的长度,由于属性名称索引与属性长度一共为6字节,所以属性值的长度固定为整个属性表长度减去6个字节。
2,max_stack代表了操作数栈(Operand Stacks)深度的最大值。在方法执行的任意时刻,操作数栈都不会超过这个深度。虚拟机运行的时候需要根据这个值来分配栈帧
(StackFrame)中的操作栈深度。
3,max_locals代表了局部变量表所需的存储空间。在这里,max_locals的单位是Slot,Slot 是虚拟机为局部变量分配内存所使用的最小单位。对于byte、char、float、int、short、 boolean和returnAddress等长度不超过32位的数据类型,每个局部变量占用1个Slot,而 double和long这两种64位的数据类型则需要两个Slot来存放。方法参数(包括实例方法中的隐藏参数"this")、显式异常处理器的参数(Exception Handler Parameter,就是
try-catch语句中catch块所定义的异常)、方法体中定义的局部变量都需要使用局部变量表来存放。另外,并不是在方法中用到了多少个局部变量,就把这些局部变量所占Slot之和作为max_locals的值,原因是局部变量表中的Slot可以重用,当代码执行超出一个局部变量的作用域时,这个局部变量所占的Slot可以被其他局部变量所使用,Javac编译器会根据变量的作用域来分配Slot给各个变量使用,然后计算出max_locals的大小。
4,code_length和code用来存储Java源程序编译后生成的字节码指令。code_length代表字节码长度,code是用于存储字节码指令的一系列字节流。既然叫字节码指令,那么每个指令就是一个u1类型的单字节,当虚拟机读取到code中的一个字节码时,就可以对应找出这个字节码代表的是什么指令,并且可以知道这条指令后面是否需要跟随参数,以及参数应当如何理解。我们知道一个u1数据类型的取值范围为0x00~0xFF,对应十进制的0~255,也就是一共可以表达256条指令,目前,Java虚拟机规范已经定义了其中约200条编码值对应的指令含义,编码与指令之间的对应关系可查阅本书的附录B"虚拟机字节码指令表"。
关于code_length,有一件值得注意的事情,虽然它是一个u4类型的长度值,理论上最大值可以达到232-1,但是虚拟机规范中明确限制了一个方法不允许超过65535条字节码指令,即它实际只使用了u2的长度,如果超过这个限制,Javac编译器也会拒绝编译。一般来讲,编写Java代码时只要不是刻意去编写一个超长的方法来为难编译器,是不太可能超过这个最大值的限制。但是,某些特殊情况,例如在编译一个很复杂的JSP文件时,某些JSP编译器会把JSP内容和页面输出的信息归并于一个方法之中,就可能因为方法生成字节码超长的原因而导致编译失败。
5,Code属性是Class文件中最重要的一个属性,如果把一个Java程序中的信息分为代码
(Code,方法体里面的Java代码)和元数据(Metadata,包括类、字段、方法定义及其他信息)两部分,那么在整个Class文件中,Code属性用于描述代码,所有的其他数据项目都用于描述元数据。了解Code属性是学习后面关于字节码执行引擎内容的必要基础,能直接阅读字节码也是工作中分析Java代码语义问题的必要工具和基本技能,因此笔者准备了一个比较详细的实例来讲解虚拟机是如何使用这个属性的。
package org.fenixsoft.clazz; public class TestClass{ private int m;
public int inc(){
return m+1;
}}
继续以代码清单6-1的TestClass.class文件为例,如图6-10所示,这是上一节分析过的实例构造器"<init>"方法的Code属性。它的操作数栈的最大深度和本地变量表的容量都为
0x0001,字节码区域所占空间的长度为0x0005。虚拟机读取到字节码区域的长度后,按照顺序依次读入紧随的5个字节,并根据字节码指令表翻译出所对应的字节码指令。翻译"2A
B700 0A B1"的过程为
法,它指向常量池中的一个CONSTANT_Methodref_info类型常量,即此方法的方法符号引用。
这条指令执行后,当前方法结束。
这段字节码虽然很短,但是至少可以看出它的执行过程中的数据交换、方法调用等操作都是基于栈(操作栈)的。我们可以初步猜测:Java虚拟机执行字节码是基于栈的体系结构。但是与一般基于堆栈的零字节指令又不太一样,某些指令(如invokespecial)后面还会带有参数,关于虚拟机字节码执行的讲解是后面两章的重点,我们不妨把这里的疑问放到第8章去解决。
我们再次使用javap命令把此Class文件中的另外一个方法的字节码指令也计算出来,结果如代码清单6-4所示。代码清单6-4 用javap命令计算字节码指令
//原始Java代码
public class TestClass{ private int m; public int inc(){
return m+1;
}}C
:\>javap-verbose TestClass
//常量表部分的输出见代码清单6-1,因版面原因这里省略掉
{public org.fenixsoft.clazz.TestClass();
Code:
Stack=1,Locals=1,Args_size=1
0:aload_0
1:invokespecial#10;//Method java/lang/Object."<init>":()V
4:return
LineNumberTable:
line 3:0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lorg/fenixsoft/clazz/TestClass;
public int inc();
Code:
Stack=2,Locals=1,Args_size=1
0:aload_0
1:getfield#18;//Field m:I
4:iconst_1
5:iadd
6:ireturn
LineNumberTable:
line 8:0
LocalVariableTable:
Start Length Slot Name Signature
0 7 0 this Lorg/fenixsoft/clazz/TestClass;
如果大家注意到javap中输出的"Args_size"的值,可能会有疑问:这个类有两个方法
——实例构造器<init>()和inc(),这两个方法很明显都是没有参数的,为什么
Args_size会为1?而且无论是在参数列表里还是方法体内,都没有定义任何局部变量,那
Locals又为什么会等于1?如果有这样的疑问,大家可能是忽略了一点:在任何实例方法里面,都可以通过"this"关键字访问到此方法所属的对象。这个访问机制对Java程序的编写很重要,而它的实
现却非常简单,仅仅是通过Javac编译器编译的时候把对this关键字的访问转变为对一个普通方法参数的访问,然后在虚拟机调用实例方法时自动传入此参数而已。因此在实例方法的局部变量表中至少会存在一个指向当前对象实例的局部变量,局部变量表中也会预留出第一个Slot位来存放对象实例的引用,方法参数值从1开始计算。这个处理只对实例方法有效,如果代码清单6-1中的inc()方法声明为static,那Args_size就不会等于1而是等于0了。
就是相当于可以调用this的关键字,在任何非静态的方法中。
在字节码指令之后的是这个方法的显式异常处理表(下文简称异常表)集合,异常表对于 Code属性来说并不是必须存在的,如代码清单6-4中就没有异常表生成。异常表的格式如表6-16所示,它包含4个字段,这些字段的含义为:如果当字节码在第start_pc行[1]到第 end_pc行之间(不含第end_pc行)出现了类型为catch_type或者其子类的异常
(catch_type为指向一个CONSTANT_Class_info型常量的索引),则转到第handler_pc行继续处理。当catch_type的值为0时,代表任意异常情况都需要转向到handler_pc处进行处理。
异常表实际上是Java代码的一部分,编译器使用异常表而不是简单的跳转命令来实现Java异常及finally处理机制[2]。代码清单6-5是一段演示异常表如何运作的例子,这段代码主要演示了在字节码层面中try-catch-finally是如何实现的。在阅读字节码之前,大家不妨先看看下面的Java源码,想一下这段代码的返回值在出现异常和不出现异常的情况下分别应该是多少?代码清单6-5 异常表运作演示
//Java源码
public int inc(){ int x; try{
x=1;
return x;
}catch(Exception e){ x=2;
return x;
}finally{ x=3;
}}/
/编译后的ByteCode字节码及异常表
public int inc();
Code:
Stack=1,Locals=5,Args_size=1
0:iconst_1//try块中的x=1
1:istore_1
2:iload_1//保存x到returnValue中,此时x=1
3:istore 4
5:iconst_3//finaly块中的x=3
6:istore_1
7:iload 4//将returnValue中的值放到栈顶,准备给ireturn返回
9:ireturn
10:astore_2//给catch中定义的Exception e赋值,存储在Slot 2中
11:iconst_2//catch块中的x=2
12:istore_1
13:iload_1//保存x到returnValue中,此时x=2
14:istore 4
16:iconst_3//finaly块中的x=3
17:istore_1
18:iload 4//将returnValue中的值放到栈顶,准备给ireturn返回
20:ireturn
21:astore_3//如果出现了不属于java.lang.Exception及其子类的异常才会走到这里
22:iconst_3//finaly块中的x=3
23:istore_1
24:aload_3//将异常放置到栈顶,并抛出
25:athrow
Exception table: from to target type
0 5 10 Class java/lang/Exception
0 5 21 any
10 16 21 any
编译器为这段Java源码生成了3条异常表记录,对应3条可能出现的代码执行路径。从Java 代码的语义上讲,这3条执行路径分别为:
如果try语句块中出现属于Exception或其子类的异常,则转到catch语句块处理。
如果try语句块中出现不属于Exception或其子类的异常,则转到finally语句块处理。
如果catch语句块中出现任何异常,则转到finally语句块处理。
返回到我们上面提出的问题,这段代码的返回值应该是多少?对Java语言熟悉的读者应该很容易说出答案:如果没有出现异常,返回值是1;如果出现了Exception异常,返回值是2;如果出现了Exception以外的异常,方法非正常退出,没有返回值。我们一起来分析一下字节码的执行过程,从字节码的层面上看看为何会有这样的返回结果。
字节码中第0~4行所做的操作就是将整数1赋值给变量x,并且将此时x的值复制一份副本到最后一个本地变量表的Slot中(这个Slot里面的值在ireturn指令执行前将会被重新读到操作栈顶,作为方法返回值使用。为了讲解方便,笔者给这个Slot起了个名字: returnValue)。如果这时没有出现异常,则会继续走到第5~9行,将变量x赋值为3,然后将之前保存在returnValue中的整数1读入到操作栈顶,最后ireturn指令会以int形式返回操作栈顶中的值,方法结束。
如果出现了异常,PC寄存器指针转到第10行,第10~20行所做的事情是将2赋值给变量x,然后将变量x此时的值赋给returnValue,最后再将变量x的值改为3。方法返回前同样将
returnValue中保留的整数2读到了操作栈顶。从第21行开始的代码,作用是变量x的值赋为
3,并将栈顶的异常抛出,方法结束。
异常表
它表示,如果字节码从start_pc行到end_pc行之间出现类型为catch_type或其子类的异常,则转到handler_pc行继续处理。具体字节码指令不再分析。
Exceptions属性
number_of_exceptions项表示方法有可能抛出多少种异常,每一种异常由一个 exception_index_table项表示,exception_index_table是一个指向常量池中 utf8 类型的索引。
LineNumberTable属性
系。它不是必须属性,如果不生成它,那么产生异常后,堆栈中将不会显示出错的行号,并且在调试时也无法在源码中设置断点。
line_number_table是一个数量为line_number_table_length、类型为 line_number_info的集合,line_number_info表包括了start_pc和
line_number两个u2数据项,前者是字节码行号,后者 是Java源码行号。
javap中显示 line 8:0,第8行正好是return所在的行号。
LocalVariableTable属性
start_pc和length属性分别代表这个局部变量的生命周期的字节码偏移量及其作用范围覆盖的长度,两者结合起来就是这个局部变量在字节码之中的作用域范围。
name_index和descriptor_index都是指向常量池中 utf8 类型的常量,分别代表局部变量的名称及局部变量的描述符。
index是这个局部变量在栈桢局部变量表中slot的位置,如果是64位类型,则它占用的slot为index 和 index+1的两个位置。
publicintinc(int a){ return m + 1?}
查看javap内容 LocalVariableTable说明了方法内局部变量的名字,占用空间以及描述符(参数是什么类型等等)。
SourceFile属性
SourceFile属性,用于记录Class文件的源码文件名称。
属性表还有几个属性,比如 ConstantValue和 Inner Class。可以自己写代码,查看javap内容学习。值得一提的是,对于类变量也就是static变量,如果变量被static和final同时修饰并且是基本数据类型或者String类型的话,那么此变量使用 ConstantValue 属性进行初始化,如果此变量没有被final修饰,或者并非基本类型及字符串,则选择在 方法中进行初始化。
Class文件结构真心复杂,很多都是要记忆的,也很枯燥,但是要学习虚拟机原理,必须清楚Class文件的结构,幸好有javap这个工具,多看看javap信息,有助于理解Class文件内容。
Jvm(40),class文件结构----- 属性表集合(这里内容比较多,等jvm写完之后看这里还有什么补充的吗)
标签:编译失败 turn 处理机 file try 现在 tar 两种 存储空间
原文地址:https://www.cnblogs.com/qingruihappy/p/9691434.html