虚拟机执行子系统 类文件结构之二 属性表集合 学习笔记(《深入理解java虚拟机》之八 类文件结构)

Class文件结构

属性表集合(attributes_count,attributes)

最后两项就是属性表集合了

属性表(attribute_info)在前面的讲解之中已经出现过数次,在Class文件、字段表、方法表都可以携带自己的属性表集合,以用于描述某些场景专有的信息

与Class文件中其他的数据项目要求严格的顺序、长度和内容不同,属性表集合限制稍微宽松了一些

(宽松了哪些)

1.不再要求各个属性表具有严格顺序,

2.并且只要不与已有属性名重复,任何人实现的编译器都可以向属性表中写入自己定义的属性信息,Java虚拟机运行时会忽略掉它不认识的属性。

 

为了能正确解析Class文件,《Java虚拟机规范(第2版)》中预定义了9项虚拟机实现应当能识别的属性,而在最新的《Java虚拟机规范(Java SE 7)》版中,预定义属性已经增加到21项,具体内容见表6-13。下文中将对其中一些属性中的关键常用的部分进行讲解。(作者讲了11个)

表6-13 预定义的

对于每个属性,它的名称需要从常量池中引用一个CONSTANT_Utf8_info类型的常量来表示而属性值的结构则是完全自定义的,只需要通过一个u4的长度属性去说明属性值所占用的位数即可。

一个符合规则的属性表应该满足表6-14中所定义的结构。

偏移量0x000010D开始,0X0000002F 的u4就是attribute_length ,十进制的47,然后0x00000111偏移量开始,就是code属性的info

 

1.Code属性

Java程序方法体中的代码经过Javac编译器处理后,最终变为字节码指令存储在Code属性内

Code属性出现方法表属性集合之中,但并非所有的方法表都必须存在这个属性,譬如接口或者抽象类中的方法就不存在Code属性

如果方法表有Code属性存在,那么它的结构将如表6-15所示。

表6-15

(一项一项解释)

attribute_name_index:

是一项指向CONSTANT_Utf8_info型常量的索引,常量值固定为“Code”,它代表了该属性的属性名称,

 

attribute_length:

指示了属性值的长度,由于属性名称索引与属性长度一共为6字节,所以属性值的长度固定为整个属性表长度减去6个字节。

【u2 + u4一共6个字节】

 


max_stack:

代表了操作数栈(Operand Stacks)深度的最大值

方法执行任意时刻操作数栈都不会超过这个深度

虚拟机运行的时候需要根据这个值分配栈帧(Stack Frame)中操作栈深度

 


max_locals:

代表了局部变量表所需的存储空间

在这里,max_locals的单位是Slot,Slot是虚拟机为局部变量分配内存所使用的最小单位

对于byte、char、float、int、short、boolean和returnAddress等长度不超过32位的数据类型每个局部变量占用1个Slot

double和long两种64位的数据类型则需要两个Slot来存放。

 

(还有哪些东西使用局部变量表放)

1.方法参数(包括实例方法中的隐藏参数“this”)、

2.显式异常处理器的参数(Exception Handler Parameter,就是trycatch语句中catch块所定义的异常)、

3.方法体中定义的局部变量

都需要使用局部变量表来存放。

 

注意!

另外并不是在方法中用到了多少个局部变量就把这些局部变量所占Slot之和作为max_locals的值

原因是:局部变量表中的Slot可以重用

代码执行超出一个局部变量作用域时,这个局部变量所占的Slot可以被其他局部变量所使用,Javac编译器会根据变量的作用域分配Slot各个变量使用,然后计算出max_locals的大小。

疑问:不理解如何重用,为什么要重用;这个疑问应该在后续的栈帧内容讲局部变量表可以解决,到时候链接过来】

【链接位预留】

 


code_lengthcode:

用来存储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内容和页面输出的信息归并于一个方法之中,就可能因为方法生成字节码超长的原因而导致编译失败。

 

 

(下面通过实例来看这个属性如何解析)

Code属性是Class文件中最重要的一个属性,如果把一个Java程序中的信息分为代码(Code,方法体里面的Java代码)元数据(Metadata,包括类、字段、方法定义及其他信息)两部分,那么在整个Class文件中,Code属性用于描述代码所有的其他数据项目都用于描述元数据

了解Code属性是学习后面关于字节码执行引擎内容的必要基础,能直接阅读字节码也是工作中分析Java代码语义问题的必要工具和基本技能,因此笔者准备了一个比较详细的实例来讲解虚拟机是如何使用这个属性的。

 

(代码清单6-1)

package org.fenixsoft.clazz;
public class TestClass{
	private int m;
	public int inc(){
		return m+1;
	}
}

继续以代码清单6-1的TestClass.class文件为例,如图6-10所示,这是上一节分析过的实例构造器“<init>”方法的Code属性。

图6-10

它的操作数栈的最大深度(max_stack)和本地变量表的容量(max_locals)都为0x0001,

字节码区域所占空间的长度(code_length)为0x0005。

虚拟机读取到字节码区域的长度后,按照顺序依次读入紧随的5个字节,并根据字节码指令表翻译出所对应的字节码指令

翻译“2A B7 00 0A B1”的过程为:

1)读入2A,查表得0x2A对应的指令为aload_0,这个指令的含义是:将第0个Slot中为reference类型的本地变量推送到操作数栈顶

2)读入B7,查表得0xB7对应的指令为invokespecial,这条指令的作用是:栈顶的reference类型的数据所指向的对象作为方法接收者调用此对象的实例构造器方法、private方法或者它的父类的方法

这个方法有一个u2类型的参数说明具体调用哪一个方法(是个index索引),它指向常量池中的一个CONSTANT_Methodref_info类型常量,即此方法的方法符号引用

【invokespecial的结构在这里:https://docs.oracle.com/javase/specs/jvms/se11/html/jvms-6.html#jvms-6.5.invokespecial

(贴一下常量池中的CONSTANT_Methodref_info 和CONSTANT_NameAndType类型)

 

(CONSTANT_Methodref_info和CONSTANT_NameAndType结构)

 

3)读入00 0A,这是invokespecial的参数,查常量池得0x000A对应的常量为实例构造器“<init>”方法的
符号引用。

000A对应的const#10 = Method#3.#11 

#3是CONSTANT_Methodref_info第一个index,指向声明方法的类描述符 CONSTANT_Class_info的索引项

#11 是CONSTANT_Methodref_info第二个index,指向名称及类型描述符CONSTANT_NameAndType的索引

而CONSTANT_NameAndType结构的两个index分别是#7(简单名称),#8(描述符)

(贴一下这个class的000A常量池表)

 

4)读入B1,查表得0xB1对应的指令为return,含义是返回此方法,并且返回值为void。这条指令执行后,当
前方法结束。

(我来贴一下这几个字节码指令的表里内容)

 

这段字节码虽然很短,但是至少可以看出它的执行过程中的数据交换、方法调用等操作都是基于栈(操作栈)的。

我们可以初步猜测:Java虚拟机执行字节码是基于栈的体系结构。但是与一般基于堆栈的零字节指令又不太一样,某些指令(invokespecial)后面还会带有参数,关于虚拟机字节码执行的讲解是后面两章的重点,我们不妨把这里的疑问放到第8章(虚拟机字节码引擎)去解决。

我们再次使用javap命令把此Class文件中的另外一个方法的字节码指令也计算出来,结果如代码清单6-4所示。

代码清单6-4

//常量表部分的输出见代码清单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;
}

【(不看答案我自己解读一次Code!!)

1)max_stack = 2,max_locals = 1,Args_size是啥!?【后面书中作者内容有说明】

2)aload_0 :将第0个Slot中为reference类型的本地变量推送到操作数栈顶。(this)

3)getfield :字节码指令为0xb4,获取常量池index为0x0012 (这十六进制,也就是const18)的实例域,并将int m的值压入栈顶

结构:https://docs.oracle.com/javase/specs/jvms/se11/html/jvms-6.html#jvms-6.5.getfield

https://docs.oracle.com/javase/specs/jvms/se11/html/jvms-5.html#jvms-5.1

 

4)iconst_1: 字节码指令 0x4,将常量int 型 数值为1 推入栈顶

https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5.iconst_i

5)iadd:字节码指令0x60,栈顶两int型数值相加并压入栈顶 :1+m 压入栈顶

https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5.iadd

6)ireturn: 字节码指令0xac,从当前方法返回int

https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5.ireturn

 

(继续书中作者的内容)

如果注意到javap中输出的“Args_size”的值,可能会有疑问:(对!我有!)

疑问一:这个类有两个方法——实例构造器<init>() 和 inc(),这两个方法很明显都是没有参数的,为什么Args_size会为1?

疑问二:而且无论是在参数列表里还是方法体内,都没有定义任何局部变量,那Locals又为什么会等于1?

注意!!!敲黑板!!

如果有这样的疑问,大家可能是忽略了一点(疑问解答:):

在任何实例方法里面,都可以通过“this”关键字访问到此方法所属的对象。

这个访问机制对Java程序的编写很重要,而它的实现却非常简单:

1.通过Javac编译器编译的时候把对this关键字的访问转变为对一个普通方法参数的访问

2.然后在虚拟机调用实例方法自动传入此参数而已。

因此在实例方法的局部变量表至少会存在一个指向当前对象实例的局部变量局部变量表中也会预留出第一个Slot位来存放对象实例的引用,方法参数值从1开始计算

(再看 代码清单6-4 上面转换出来的内容,下面都有局部变量表信息都是指的this)

疑问:为啥一个length 是5,一个length是7,这个length指的是什么?

 

再注意!!!

这个处理只对实例方法有效,如果代码清单6-1中的inc()方法声明为static,那Args_size就不会等于1而是等于0了

(变成static等于0而不是1,也就是我们java里的,static代码块,方法中,不能使用this,具体原因在类加载的内容,后续看到补上链接)

【链接占位】

 

 

 

异常处理表


在字节码指令之后的是这个方法的显式异常处理表(下文简称异常表)集合,异常表对于Code属性来说并不是必须存在的,如代码清单6-4中就没有异常表生成。


异常表的格式如表6-16所示,它包含4个字段

异常表结构6-16

 

这些字段的含义为:

如果当字节码在第start_pc行[1]到第end_pc行之间(不含第end_pc行)

出现了类型为catch_type或者其子类异常catch_type为指向一个CONSTANT_Class_info型常量的索引),

转到第handler_pc行继续处理

 

catch_type的值为0时,代表任意异常情况都需要转向到handler_pc处进行处理

[1]此处字节码的“行”是一种形象的描述,指的是字节码相对于方法体开始的偏移量而不是Java源码的行号,下
同。

 



【不太理解,找到了官方原文的解释:https://docs.oracle.com/javase/specs/jvms/se6/html/ClassFile.doc.html#1567

The value of start_pc must be a valid index into the codearray of the opcode of an instruction.The value of end_pc either must be a valid index into the code array of the opcode of an instruction or must be equal to code_length, the length of the code array. The value of start_pc must be less than the value of end_pc.

The start_pc is inclusive and end_pc is exclusive;

按官方的说法是,start_pc和end_pc是code字节码指令的索引,end_pc的取值最大为code_length,而start_pc必须小于end_pc的值,包含start_pc值而不包含end_pc值,[start_pc, end_pc),也就是索引从0开始到5

以<init>() 的字节码指令为例

code_length是5,code为:2A B7 00 0A B1

假如!!start_pc为1,end_pc为5,那么start_pc到end_pc的范围就是指令B7 00 0A B1

(下面还有作者的异常表运作的事例,接着往下看)

exception_table[]

Each entry in the exception_table array describes one exception handler in the code array. The order of the handlers in the exception_table array is significant. See Section 3.10 for more details.

Each exception_table entry contains the following four items:

start_pcend_pc

The values of the two items start_pc and end_pc indicate the ranges in the code array at which the exception handler is active. The value of start_pc must be a valid index into the codearray of the opcode of an instruction. The value of end_pc either must be a valid index into the code array of the opcode of an instruction or must be equal to code_length, the length of the code array. The value of start_pc must be less than the value of end_pc.

The start_pc is inclusive and end_pc is exclusive; that is, the exception handler must be active while the program counter is within the interval [start_pc, end_pc).4

handler_pc

The value of the handler_pc item indicates the start of the exception handler. The value of the item must be a valid index into the code array and must be the index of the opcode of an instruction.

catch_type

If the value of the catch_type item is nonzero, it must be a valid index into the constant_pool table. The constant_pool entry at that index must be a CONSTANT_Class_info (§4.4.1) structure representing a class of exceptions that this exception handler is designated to catch. This class must be the class Throwable or one of its subclasses. The exception handler will be called only if the thrown exception is an instance of the given class or one of its subclasses.

If the value of the catch_type item is zero, this exception handler is called for all exceptions. This is used to implement finally (see Section 7.13, "Compiling finally").

 

异常表实际上是Java代码的一部分,编译器使用异常表而不是简单的跳转命令来实现Java异常及finally处理机制[2]。

(选看)

[2]在JDK1.4.2之前的Javac编译器采用了jsr和ret指令实现finally语句,但1.4.2之后已经改为编译器自动在每段可能的分支路径之后都将finally语句块的内容冗余生成一遍来实现finally语义。在JDK 1.7中,已经完全禁止Class文件中出现jsr和ret指令,如果遇到这两条指令,虚拟机会在类加载的字节码校验阶段抛出异常。

 


代码清单6-5是一段演示异常表如何运作的例子,这段代码主要演示了在字节码层面中try-catch-finally是如
何实现的。在阅读字节码之前,大家不妨先看看下面的Java源码,想一下这段代码的返回值在出现异常和不出现异
常的情况下分别应该是多少?

源码:

public int inc(){
    int x;
    try{
        x=1;
        return x;
    }catch(Exception e){
        x=2;
        return x;
    }finally{
        x=3;
    }
}

代码清单6-5

//编译后的ByteCode字节码及异常表
public int inc();
    Code:
     Stack=1,Locals=5,Args_size=1
     0:iconst_1//try块中的x=1,1推到栈顶
     1:istore_1//给x赋值栈顶1
     2:iload_1//保存x到returnValue中,此时x=1,将x的值推到栈顶
     3:istore 4//栈顶的值,此时为1,放到slot 4
     5:iconst_3//finaly块中的x=3,3 推到栈顶
     6:istore_1//给x赋值栈顶3
     7:iload 4//将returnValue中的值放到栈顶,准备给ireturn返回,将slot4的值推到栈顶
     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条执行路径分别为:

1.如果try语句块中(0-5)出现(type)属于Exception或其子类的异常,则转到catch语句块(10)处理。

2.如果try语句块中(0-5)出现(type)不属于Exception或其子类的异常,则转到finally语句块(21)处理。

3.如果catch语句块(10-16)中出现任何异常,则转到finally语句块(21)处理。

 

返回到我们上面提出的问题,这段代码的返回值应该是多少?对Java语言熟悉的读者应该很容易说出答案:

如果没有出现异常,返回值是1;

如果出现了Exception异常,返回值是2;

如果出现了Exception以外的异常,方法非正常退出,没有返回值,抛出异常。

 

 

我们一起来分析一下字节码的执行过程,从字节码的层面上看看为何会有这样的返回结果。


(我自己在0-7后面补充了指令意思,后面指令都也是那几个,看着就稍微明白一点)
字节码中第0~4行所做的操作就是将整数1赋值给变量x,并且将此时x的值复制一份副本最后一个本地变量表的Slot

(这个Slot里面的值在ireturn指令执行前将会被重新读到操作栈顶作为方法返回值使用。为了讲解方便,笔者给这个Slot起了个名字:returnValue)。

【看指令,istore_4,后面ireturn之前有一个iload_4】

 

如果这时没有出现异常,则会继续走到第5~9行,将变量x赋值为3,

然后将之前保存在returnValue中的整数1读入到操作栈顶,

最后ireturn指令会以int形式返回操作栈顶中的值,方法结束。

 


如果出现了异常PC寄存器指针转到第10行,

第10~20行所做的事情是将2赋值给变量x,然后将变量x此时的值赋给returnValue,最后再将变量x的值改为3。

方法返回前同样将returnValue中保留的整数2读到了操作栈顶。


 

如果出现了不属于Exception或其子类的异常,从第21行开始的代码,

作用是变量x的值赋为3,并将栈顶的异常抛出,方法结束。



尽管大家都知道这段代码出现异常的概率非常小,但并不影响它为我们演示异常表的作用。如果大家到这里仍然对字节码的运作过程比较模糊,其实也不要紧,关于虚拟机执行字节码的过程,本书第8章(虚拟机字节码引擎章节)中将会有更详细的讲解。

 

 

2.Exceptions属性

(贴一下他回忆一下)

 

 

【TODO !!!后续继续更新!!】

 

 

 

 

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值