JVM之Class结构属性表
- Code
- Exceptions
- LineNumberTable
- LocalVariableTable,LocalVariableTypeTable
- ConstantValue
- Deprecated及Synthetic属性
- StackMapTable
- MethodParameters
概述
上篇文章提到过在Class结构表中,属性表存在于Class表,字段表和方法表中,是为了描述额外的信息。
属性表在《JAVA虚拟机规范》中并没有像其他数据一样做严格的限制,我们甚至可以自己实现一个编译器往Class结构的属性表中注入额外的属性信息,虚拟机运行时会忽略掉它识别不了的属性。
属性表属性总览
这张图中按Class结构,字段表,方法表这三个维度进行了区分标注,将三者共有的属性提取到最顶部的黄色椭圆中,蓝色代表各自属性表额外用到的属性,红色代表Code属性中引用的其他属性的集合。
引自《深入理解JAVA虚拟机》,读者也可以看这下面的两张图:
属性结构
首先表结构中的前两个字节说明该属性是叫什么名字,也就是什么类型的属性,最终指向常量池中的CONSTANT_Utf8_info类型的常量。
eg:Code,ConstantValue,SourceFile。。。等
接着用四个字节描述属性值的长度,也就是说明属性值所占用的字节数;
除了第一个属性名称信息和该属性描述的属性长度,其他的就是属性值了,因此该值(属性值长度)固定为 整个属性表长度-6个字节
最后列出属性信息,有多少属性信息呢?前四个字节已经列出了这个属性包含多少个属性,因此最后描述各个属性的信息。
前两个属性是所有属性都共有的,之后就不进行讲解了。
常见属性
Code
只有方法内有方法体的方法表才会有这项属性,像抽象方法,非defult接口方法是没有这个属性的(没有方法体)
该属性用于存放
操作数栈最大深度,
本地变量表最大占用存储空间,
方法中Catch块定义的异常类型和数量,
编译后方法体的字节码指令和指令长度,
还有其他属性
首先先看下Code属性表的结构:
1.max_stack操作数栈最大深度
先看下之前的这篇文章,Java程序运行是基于栈的操作,就是说的该项属性。
通过指令从本地变量表中拿数据放到栈顶;将数据从栈顶保存到本地变量中;对栈顶两个值进行运算后将结果压入栈顶;将栈顶的值作为返回结果return…等等这些都是通过操作数据结构栈来完成的。
这项属性描述了方法中操作数栈的最大深度,虚拟机运行时会根据这项属性来分配栈帧中操作数栈的最大深度。
2.max_locals局部变量表所需空间
局部变量表最大占用的存储空间,存储的单位用的是Slot(变量槽),一个
Slot占用空间大小为32位。
存储范围:
比如方法的参数,Catch块中定义的异常类型,方法体中定义的局部变量。
对于方法参数来说,根据实例方法(通过对象才能访问)和静态方法(通过类名就可以直接访问)又有不同;实例方法的参数中第一个是this,该变量会在编译时加入,而静态方法则没有这个变量。
存储空间:
对于小于32位的数据类型用一个变量槽,大于32位的用多个变量槽存储(比如小于64位的数据类型double和long用两个变量槽存储)。
优化:
对变量限定作用域,如果变量超出了作用域范围,那么存储该变量的Slot进行存储其他的变量,也就是重用变量槽。
根据同时生存的最大局部变量数量和类型计算出max_locals的大小。
3.code_length,code属性用于表述方法体编译后的字节码指令长度和字节码指令流
字节码指令流,一个字节代表一条指令,即每条指令就是一个u1类型的单字节;u1数据类型取值范围是0X00~0XFF,对应十进制为0到255。
即u1可以表达256条指令,目前使用的大约只有200条
方法体中不允许超过65535条字节码指令,如果超出了编译器拒绝编译;但是code_length用的是u4长度值,可以达到2的32次幂,相当于指令长度只用了一半(u2长度)。
4.exception_table_length,exception_table用于描述方法块中Catch块定义的异常数量和类型
如果方法体内部没有catch块则length为0,后面的字节不描述exception_table
Exception table:
from to target type
0 5 10 Class java/lang/Exception
0 5 21 any
10 16 21 any
Exceptions
该属性和上面提到的exception_table描述的信息不一样,exception_table是描述方法体中Catch块中定义的异常数量和类型;而该项属性是描述方法throws的异常数量和类型,通俗来说就是调用该方法需要catch的异常,也叫作受查异常。
因此上面的excepitons_table描述方法体内部Catch的异常,Exceptions描述方法抛出的异常
结构:
LineNumberTable
用于描述Java代码行号和字节码行号的对应关系,该项属性可选择是否输出
用处:当执行某段字节码指令抛出异常时可以根据这个对应关系,提示开发者Java代码对应的行号。
结构
LocalVariableTable,LocalVariableTypeTable
1.LocalVariableTable
用于描述局部变量表中的变量与JAVA代码中定义变量之间的关系,同样也可以选择不生成该属性
用处:当编写代码引用到这个方法时,参数可以直接显示变量名和类型,如果没有该属性,就用arg0,arg1代替;调试信息的时候可以根据参数变量名明确语义。
2.LocalVariableTypeTable
引入泛型后加入的属性,结构和LocalVariableTable相似,将原先用于描述字段描述符的descriptor_index替换成了描述字段的特征签名(Signature)。
对于非泛型变量,特征签名和描述符是一致的,但是对于泛型来说,由于泛型参数类型的擦除,描述符不能够描述泛型类型,因此定义了该属性来完成泛型的描述。
结构:
ConstantValue
变量初始化,赋值时机:
位于该属性结构中的常量将会在类加载的准备阶段就会初始化并且赋值;
其他的静态变量在这个阶段只是会被初始化然后赋默认值,如果静态变量设置了final关键字,那么就是第一种情况会对变量进行赋值;
对于实例变量(非静态变量)的赋值是在实例构造器《init》中。
该结构中存放的字段是:
《Java虚拟机规范》中规定该属性结构中存放的必须是静态的字段,而对于javac编译器来说还需要满足final关键字的修饰,因此经过javac编译器编译后的该属性中存放的字段必须是static并且是final的。
通知虚拟机自动为静态变量赋值(上面那句话)。该属性中只能存放基本类型和String,因为该属性的属性值
结构:
Deprecated及Synthetic属性
这两项属性有点特殊,不携带任何属性值,出现这两个属性的目的只是为了标识,这两个属性只有存在或不存在。
1.Deprecated属性
该属性用于表示某个类,字段或方法已经不再推荐使用,通过“@deprecated”注解设置这个属性
2.Synthetic属性
该属性用于表示字段或者方法是编译器自己添加的,不是代码中的。
也可以通过设置访问标志ACC_SYNTHETIC标志位生成该项属性。
结构:
两者都一样,不携带任何属性值。只是用于标识
StackMapTable
该属性位于Code属性的属性表中
用处:在类加载阶段的验证阶段使用该属性,代替以前耗性能的基于数据流分析的类型推导验证器(有了该属性之后就不用类型推导了,可以直接判断类型是不是符合要求,之后单独写类加载阶段进行分析)
之前验证阶段是基于数据流来进行分析推导出操作数栈和本地变量表操作的类型是否一致等(比如istore需要将操作数栈的数据保存到本地变量表中,但是取出的数据类型不是int就会发生问题),现在基于该项属性可以不用推导
结构:
一个Code属性最多只能有一个StackMapTable属性
MethodParameters
用于记录方法的各个形参名称和信息
方法参数属性,位于class中的属性表中。
之前说过这部分是存储在局部变量表中的,因为方法中有方法体code属性,而code中需要有局部变量表属性代表这个方法中的变量存储。
但是为什么还要单独抽出一个属性放在class中呢?
大家想想没有code就没有局部变量表,没有局部变量表是不是就不能存储方法参数了;你看接口中他有方法吧但是呢他其实没有方法提code所以它的方法参数往哪放呢?往他借口的属性表集合中放,也就是与code同级。这样的话我接口里可以直接获得方法参数通过这个属性;而对于正常的方法也就是有方法体的代码可以从code中的局部变量表中拿。
数据结构:
1.首先说明他是什么,我是一个方法参数类型
2.我说明我存储的时候数据有多长(多少字节),为了切割按照这个就可以正确读取对应的数据;但是如果这个属性中还用到了其他的数据结构(属性),那么就是这个属性的个数了
3.对于没有再次用到其他属性来描述的属性直接使用定长数据即可;但是对于有用到其他属性来描述这个属性的话,则后面是对应的属性一个一个排开,然后每个属性如果是定长的话则不需要通过长度来说明所占字节,然后这个属性中存储的第一个永远是他是什么也就是名字,然后再是对应的值
不断使用这种结构来描述一个完整的class结构
结构: