之所以将属性表拿出来单独进行讲解,就是因为在上一篇博客中我已经说过,在Class类文件中,最重要的当属常量池与属性表部分,而属性表中的属性目前已高达21项,当然我们不用全部记住它们,只需要熟悉其中的几个关键属性。
属性表基本结构
我们今天只谈其中的5种属性,如果你有兴趣,请下去自行了解~
每个属性的名称都引用自常量池中一个CONSTANT_Utf8_info类型的常量来表示,属性值的结构则完全是自定义,只需要一个u2长度属性去说明属性值所占用的位数即可。
属性表结构如图:
Code属性
Java方法体中的代码经过Javac编译之后,最终变为字节码存储在Code属性中,Code属性出现在方法表的属性集合之中。接口或抽象类中的方法并不存在Code属性,因为他们并没有对方法进行具体的实现。
Code属性表的结构如下图:
在这里我尝试挑几个概念上比较重要来进行记录。
max-stack,要解释清楚它,需要了解操作数栈,栈帧等知识,所以这个属性字段先不谈。
max_locals,这个挺有意思。它代表了局部变量所需的存储空间。单位是Slot,虚拟机为局部变量分配内存使用的最小单位。对于byte、char… …这种长度不超过4个字节的数据类型,每个局部变量占用一个Slot,而double、long这两种64位的数据类型则需要两个Slot来存放。方法参数(this)、显式异常处理器参数(try-catch)、方法体中的局部变量都需要使用局部变量表来存放。
计算max_locals的值也不是方法中用到了多少个局部变量,就将每个局部变量所占用的Slot算出来最后进行简单求和。事实上局部变量表中的Slot可以重用,当代码执行超出一个局部变量的作用域时,这个局部变量所占用的Slot可以被其他局部变量所使用。
Javac编译器会根据变量的作用域来分配Slot给每个变量使用,然后计算出max_locals的大小。
code_length不用说代表的是字节码长度。它是一个u4类型的长度值,理论上最大值可以达到2的32次方-1,但Java虚拟机规范限制一个方法不允许超过65535条字节码指令,所以它实际只使用了u2的长度,一旦超过这个长度,编译器会拒绝编译。
一般来说我们不会在一个方法里面写这么长的代码,在正常的情况下。但是在编译一个很复杂的JSP文件时,还是有可能出问题,因为编译器会将JSP内容和页面输出的信息都归并于一个方法之中,此时就有可能出错~~
Code属性是Class文件中最重要的一个属性,Java程序可以分为代码和元数据,也就是方法体中的代码与字段、类、方法定义等其他信息。因此在Class文件中,只有Code属性用于描述Java方法,其他都用于描述元数据。
使用GHex16进制编辑器对Class文件我也就不进行分析了,都是上一篇博客中的套路,我们直接使用javap命令计算字节码指令。 (还是上一篇中的TestClass.java文件)
我已将常量池部分省略:
javap -verbose TestClass
{
public TestClass();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 8: 0
public int inc();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: getfield #2 // Field m:I
4: iconst_1
5: iadd
6: ireturn
LineNumberTable:
line 12: 0
}
SourceFile: "TestClass.java"
我们来分析一下这个输出结果:
因为原Java文件中实际上有两个方法,一个实例构造器,一个inc方法。因此第一个public TestClass();
代表的是实例构造器。我们可以看到args_size=1
,说明这个方法有一个参数,虽然源代码里面两个方法都没有显式参数,但是我们都知道这个参数肯定是this。
当然,这个this只对实例对象有效,也就是说,如果inc方法被声明为static,那么它的args_size
就会等于0。
往下看是三条字节码指令,关于字节码指令的内容目前也不进行讨论,所以暂且跳过。
异常表
按道理来说,字节码指令之下就是异常表(显式异常处理表),但是此代码并没有生成异常表。所以这个部分对Code来说并不是必须的。
来看一下异常表的结构:
异常表包含4个字段,这些字段的含义为:如果字节码从第start_pc到end_pc行之间(不包含第end_pc)行出现了类型为catch_type或其子类的异常(catch_type为指向一个CONSTANT_Class_info型常量的索引),则转到第handler_pc行继续处理。当catch_type的值为0时,代表任何的异常情况都需要转向到handler_pc行进行处理。
异常表实际上是Java代码的一部分,编译器使用异常表而不是简单的跳转命令来实现Java异常及finally处理机制。注:字节码的“行”是一种形象的描述,指的是字节码相对于方法体开始的偏移量,而不是Java源代码的行号。
我们使用一个详细的例子来分析一下在字节码层面中try-catch-finally
是怎么运作的。先来看一个Java源码:
public int inc() {
int x;
try {
x = 1;
return x;
} catch (Exception e) {
x = 2;
return x;
} finally {
x = 3;
}
}
大家认为这段代码在产生异常和不产生异常的情况下分别应该返回多少?
来看一下对这段代码编译后产生的字节码吧:
public int inc();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=1, locals=5, args_size=1
0: iconst_1 //try块中的x=1
1: istore_1
2: iload_1 //保存x到returnValue中
3: istore_2
4: iconst_3 //finally中的x=3
5: istore_1
6: iload_2 //将returnValue中的值放在栈顶,准备给ireturn返回
7: ireturn
8: astore_2 //将catch中定义的Exception e赋值,存储在Slot 2中
9: iconst_2 //catch中的x = 2
10: istore_1
11: iload_1 //保存x到returnValue中,此时x=2
12: istore_3
13: iconst_3 //finally块中的x=3
14: istore_1
15: iload_3 //将returnValue中的值放在栈顶,准备给ireturn返回
16: ireturn
17: astore 4 //如果出现不属于java.lang.Exception及子类的异常才走这里
19: iconst_3 //finally中的x=3
20: istore_1
21: aload 4 //将异常放置到栈顶,并抛出
23: athrow
Exception table:
from to target type
0 4 8 Class java/lang/Exception
0 4 17 any
8 13 17 any
17 19 17 any
LineNumberTable:
line 14: 0
line 15: 2
line 20: 4
line 15: 6
line 16: 8
line 17: 9
line 18: 11
line 20: 13
line 18: 15
line 20: 17
StackMapTable: number_of_entries = 2
frame_type = 72 /* same_locals_1_stack_item */
stack = [ class java/lang/Exception ]
frame_type = 72 /* same_locals_1_stack_item */
stack = [ class java/lang/Throwable ]
}
SourceFile: "TestClass.java"
如果对这段代码感到模糊,也不要紧,我将会在后面的博客中对虚拟机执行字节码的过程进行详细的解读。那么这段代码的返回结果到底是什么?
出现Exception异常返回2,没出现异常返回1,出现了Exception之外的异常,没有返回值。
Exception属性
属性表结构如下:
这里的Exceptions属性是在方法表中与Code属性平级的一项属性,而不是Code属性表中的异常属性表。Exceptions属性表的作用是列举出方法中可能抛出的受查异常(Checked Exception),也就是在方法描述时在throws关键字后面列举的异常。
此属性表中的number_of_exceptions项表示方法可能抛出number_of_exceptions种受检查异常,每一种受检查异常使用一个exception_index_table项表示,指向常量池中CONSTANT_Class_info型常量表的索引,代表了该受检查异常的类型。
总结
还有好多属性我在这里就不进行一一分析了,意义并不是很大,有兴趣的同学可以阅读《深入理解Java虚拟机》一书中的第6章内容,作者已经给出了很详细的解读。
参考阅读
《深入理解Java虚拟机》—周志明