深入理解JVM虚拟机:(四)类文件结构(下)

前言

上一篇 深入理解JVM虚拟机:(三)类文件结构(上) 中我们介绍了Java的类文件结构,这一篇我们继续来看Java其他的类文件结构。

字段表集合

字段表用于描述接口或者类中声明的变量。字段包括类级变量以及实例级变量,但不包括在方法内部声明的局部变量。我们可以想一想在Java中描述一个字段可以包含什么信息?可以包括的信息有:字段的作用域(private、protect、public修饰符)、是实例变量还是类变量(static修饰符)、可变性(final)、并发可见性(volatile修饰符,是否强制主从内存读写)、可否被序列化(transient修饰符)、字段数据类型(基本类型、对象、数组)、字段名称。上述这些信息中,各个修饰符都是布尔值,要么有某个修饰符,要么没有,很合适使用标志位来表示。而字段叫什么名字、字段被定义为什么数据类型,这些都是无法固定的,只能引用常量池中的常量来描述。

image

字段修饰符放在access_flags项目中,它与类中的access_flags项目是非常相似的,都是一个u2的数据类型,其中可以设置的标志位和含义见下图

image

很明显,在实际情况中,ACC_PUBLIC、ACC_PRIVATE、ACC_PROTECTED三个标志做多只能选择其一,ACC_FINAL、ACC_VOLATILE不能同时选择。接口之中的字段必须有ACC_PUBLIC、ACC_STATIC、ACC_FINAL标志,这些都是由Java本身的语言规则所决定的。
跟随access_flags标志的是两项索引值:name_index和descriptor_index。它们都是对常量池的引用,分别代表着字段的简单名称和方法的描述符。现在需要解释一下“简单名称”、“描述符”以及前面多次出现过的“全限定名”这三种特殊字符串的概念。
全限定名和简单名称很好理解,例如:“org/test/clazz/TestClass”是这个类的全限定名,仅仅是把类全名中的“.”替换成了“/”而已,为了使连续的多个全限定名之间不产生混淆,在使用时最后一般会加入一个“;”表示全限定名结束。简单名称是指没有类型和参数修饰的方法或者字段名称,这个类中的inc()方法和m字段的简单名称分为是“inc”和“m”。
相对于全限定名和简单名称来说,方法和字段的描述符就要复杂一些。描述符的作用是用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值。根据描述符规则,基本数据类型以及代表无返回值的void类型都用一个大写字符来表示,而对象类型则用字符L加对象的全限定名来表示。

image

对于数组类型,每一维度将使用一个前置的“[”字符来描述,如一个定义为“java.lang.String[][]”类型的二维数组,将被记录为:“[[Ljava/lang/String”,一个整型数组“int[]”将被记录为“[I”。

用描述符来描述方法时,按照先参数列表,后返回值的顺序描述,参数列表按照参数的严格顺序放在一组小括号“()”之内。如方法void inc()的描述符为“()V”,方法java.lang.String toString()的描述符为“()Ljava/lang/String;”,方法int indexOf(char[] source, int sourceOffset, int sourceCount, char[] target, int targetOffset, int targetCount, int fromIndex)的描述符为“([CII[CIII)I”。

字段表都包含的固定数据项目到descriptor_index为止就结束了,不过在descriptor_index之后跟随着一个属性表集合用于存储一些额外的信息,字段都可以在属性表中描述零至多项的额外信息。

字段表集合中不会列出从超类或者父接口中继承而来的字段,但有可能列出原本Java代码之中不存在的字段。譬如在内部类中为了保持对外部类的访问性,会自动添加指向外部类实例的字段。另外,在Java语言中字段是无法重载的,两个字段的数据类型、修饰符不管是否相同,都必须使用不一样的名称,但是对于字节码来讲,如果两个字段的描述符不一致,那字段重名就是合法的。

扫描二维码关注公众号,回复: 2583259 查看本文章

方法表集合

Class文件存储格式中对方法的描述与对字段的描述几乎采用了完全一致的方式,方法表的结构如同字段表一样,依次包括了访问标志(access_flag)、名称索引(name_index)、描述符索引(descriptor_index)、属性表集合(attributes)几项。这些数据项目的含义也非常类似,仅在访问标志和属性表集合的可选项中有所区别。
image

因为volatile关键字和transient关键字不能修饰方法,所以方法表的访问标志中没有了ACC_VOLATILE标志和ACC_TRANSIENT标志。与之相对的,synchronized、native、strictfp和abstract关键字可以修饰方法,所以方法表的访问标志中增加了ACC_SYNCHRONIZED、ACC_NATIVE、ACC_STRICTFP和ACC_ABSTRACT标志。对于方法表,所有标志位及其取值见下图。

image

在这里,可能你会有疑问,方法的定义可以通过访问标志、名称索引、描述符索引表达清楚,但方法里面的代码哪去了?方法里的Java代码,经过编译器编译成字节码指令后,存放在方法属性表集合中一个名为“Code”的属性里面,属性表作为Class文件格式中最具有扩展性的一种数据项目,将在后续文章介绍。

与字段表集合相对应的,如果父类方法在子类中没有被重写,方法表集合中就不会出现来自父类的方法信息。但同样的,有可能会出现由编译器自动添加的方法,最典型的便是类构造器“”方法和实例构造器“”方法。

在Java语言中,要重载一个方法,除了要与原方法具有相同的简单名称之外,还要求必须拥有一个与原方法不同的特征签名,特征签名就是一个方法中各个参数在常量池中的字段符号引用的集合,也就是因为返回值不会包含在特征签名中,因此Java语言里面是无法仅靠返回值的不同来对一个已有方法进行重载的。但是在Class文件格式中,特征签名的范围更大一些,只要描述符不是完全一致的两个方法也可以共存。也就是说,如果两个方法有相同的名称和特征签名,但返回值不同,那么也是可以合法共存于同一个Class文件中的。

属性表集合

属性表在前面的讲解中已经出现过多次,在Class文件、字段表、方法表都可以携带自己的属性表集合,以用于描述某些场景专有的信息。
与Class文件中其他的数据项目要求严格的顺序、长度、和内容不同,属性表集合的限制稍微放宽了一些,不再要求各个属性表具有严格的顺序,并且只要不予已有属性表名重复,任何人实现的编译器都可以项属性表中写入自己定义的属性信息,Java虚拟机运行时会忽略掉它不认识的属性。为了能正确解析Class文件,《Java虚拟机规范》中预定义属性已经增加到了21项,,具体见下表。

属性名称 使用位置 含义
Code 方法表 Java代码编译成的字节码指令
ConstantValue 字段表 final关键字定义的常量值
Deprecated 类、方法表、字段表 被声明为deprecated的方法和字段
Exceptions 方法表 方法抛出的异常
EnclosingMethod 类文件 仅当一个类为局部类或者匿名类时才能拥有这个属性,这个属性用于标识这个类所在的外围方法
InnerClasses 类文件 内部类列表
LineNumberTable Code属性 Java源码的行号与字节码指令的对应关系
LocalVariableTable Code属性 方法的局部变量描述
StackMapTable Code属性 JDK1.6中增加的属性,供新的类型检查验证器检查和处理目标方法的局部变量和操作数栈所需要的类型是否匹配
Signature 类、方法表、字段表 JDK1.5中新增的属性,这个属性用于支持泛型情况下的方法签名,在Java语言中,任何类、接口、初始化方法或成员的泛型签名如果包含了类型变量或参数化类型,则Signature属性会为它记录泛型签名信息。由于Java的泛型采用擦除法实现,是为了避免类型信息被擦除后导致签名混乱,需要这个属性记录泛型中的信息
SourceFile 类文件 记录源文件名称
SourceDebugExtension 类文件 JDK1.6中新增的属性,SourceDebugExtension属性用于存储额外的调试信息。譬如在进行JSP文件调试时,无法通过Java堆栈来定位到JSP的行号,JSR-45规范为这些非Java虚拟机中的程序提供了一个进行调试的标准机制,使用SourceDebugExtension属性就可以用于存储这个标准所新加入的调试信息
Synthetic 类、方法表、字段表 标识方法或字段为编译器自动生成的
LocalVariableTypeTable 类文件 JDK1.5中新增的属性,它使用特征签名代替描述符,是为了引入泛型语法之后能描述泛型参数化类型而添加
RuntimeVisibleAnnotations 类、方法表、字段表 JDK1.5中新增的属性,为动态注解提供支持。RuntimeVisibleAnnotations属性用于指明哪些注解是运行时(实际上运行时就说进行反射调用)可见的
RuntimeInvisibleAnnotations 类、方法表、字段表 JDK1.5中新增的属性,作用与RuntimeVisibleAnnotations属性刚好相反,用于指明哪些注解是运行时不可见的
RuntimeVisibleParameterAnnotations 方法表 JDK1.5中新增的属性,作用与RuntimeVisibleAnnotations属性相似,只不过作用对象为方法参数
RuntimeInvisibleParameterAnnotations 方法表 JDK1.5中新增的属性,作用与RuntimeInvisibleAnnotations属性相似,只不过作用对象为方法参数
AnnotationDefault 方法表 JDK1.5中新增的属性,用于记录注解类元素的默认值
BootstrapMethods 类文件 JDK1.7中新增的属性,用于保存invokedynamic指令引用的引导方法限定符

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

类型 名称 数量
u2 attribute_name_index 1
u4 attribute_length 1
u1 info attribute_length

1、Code属性

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

2、Exception属性

这里的Exceptions属性是在方法表与Code属性平级的一项属性,Exceptions属性的作用是列举出方法中可能抛出的 受检查异常,也就是方法描述时在throws关键字后面列举的异常。

3、LineNumberTable

LineNumberTable属性用于描述Java源码行号与字节码行号之间的对应关系。它不是运行时必须的属性,但默认会生成到Class文件中,可以在Javac中分别使用–g:none或-g:lines选项来取消或要求生成这项信息。如果选择不生成LineNumberTable属性,对程序运行产生的最主要赢就是当抛出异常时,堆栈中将不会显示出错误的行号,并且在调试程序的时候,也无法按照源码行来设置断点。

4、LocalVariableTable属性

LocalVariableTable属性用于描述栈帧中局部变量表中的变量与Java源码中定义的变量之间的关系,它也不是运行时必须的属性,但默认会生成到Class文件中,可以在Javac中分别使用-g:none或者-g:vars选项来取消或要求生成这项信息。如果没有生成这项属性,最大的影响就是当其他人引用这个方法时,所有的参数名称都将丢失,IDE将会使用诸如arg0、arg1之类的占位符代替原有的参数名,这对程序运行没有影响,但是对代码编写带来较大的不便,而且在调试期间无法根据参数名从上下文中获取参数值。

5、SourceFile属性

SourceFile属性用于记录生成这个Class文件的源码文件名称。这个属性也是可选的,可以分别使用Javac的-g:none或-g:source选项来关闭或要求生成这项信息。在Java中对于大多数的类来说,类名和文件名是一致的,但有一些特殊情况,例如内部类例外。

6、ConstantValue属性

ConstantValue属性的作用是通知虚拟机自动为静态变量赋值。只有被static关键字修饰的变量才可以使用这项属性。类似“int x=123”和“static int x=123”这样的变量定义在Java程序中是非常常见的事情,但虚拟机对这两种变量赋值的方式和时刻都有所不同。对于非static类型的变量的赋值是在实例构造器方法中进行的;而对于类变量,则有两种方式可以选择:在类构造器方法中或者使用ConstantValue属性。目前Sun Javac编译器的选择是:如果同时使用final和static来修饰一个变量,并且这个变量的数据类型是基本类型或者String的话,就生成ConstantValue属性来进行初始化,如果这个变量没有被final修饰,或者并非基本类型及字符串,则将会选择在方法中进行初始化。

7、InnerClasses属性

InnerClasses属性用于记录内部类与宿主类之间的关联。如果一个类中定义了内部类,那编译器将会为它机器它所包含的内部类生成InnerClasses属性。

8、BootstrapMethods属性

BootstrapMethods属性在JDK1.7发布后增加到了Class文件规范中,它是一个复杂的变长属性,位于类文件的属性表中。这个属性用于保存invokedynamic指令引用的引导方法限定符。《Java虚拟机规范》规定,如果某个类文件结构的常量池中曾经出现过CONSTANT_InvokeDynamic_info类型的常量,那么这个类文件的属性表中必须存在一个明确的BootstrapMethods属性,另外,即使CONSTANT_InvokeDynamic_info类型的常量在常量池中出现过多次,类文件的属性表中最多也只能有一个BootstrapMethods属性。

更多Java干货文章请关注我的个人微信公众号:老宣与你聊Java

这里写图片描述

猜你喜欢

转载自blog.csdn.net/wtopps/article/details/80628908