6、JVM--类文件结构(上)
6.1、概述
写的程序需要经编译器翻译成由0和1构成的二进制格式才能由计算机执行
6.2、无关性基石
Java在刚刚诞生之时曾经提出过一个非常著名的宣传口号:“一次编写,到处运行(Write Once,Run Anywhere)”
充分表达了软件开发人员对冲破平台界限的渴求。
Sun公司以及其他虚拟机提供商发布了许多可以运行在各
种不同平台上的虚拟机,这些虚拟机都可以载入和执行同一种平台无关的字节码,从而实现
了程序的“一次编写,到处运行”。
各种不同平台的虚拟机与所有平台都统一使用的程序存储格式——字节码(ByteCode)是构成平台无关性的基石
实现语言无关性的基础仍然是虚拟机和字节码存储格式。Java虚拟机不和包括Java在内
的任何语言绑定,它只与“Class文件”这种特定的二进制文件格式所关联,Class文件中包含
了Java虚拟机指令集和符号表以及若干其他辅助信息。
如图:
Java语言中的各种变量、关键字和运算符号的语义最终都是由多条字节码命令组合而成
的,因此字节码命令所能提供的语义描述能力肯定会比Java语言本身更加强大。因此,有一
些Java语言本身无法有效支持的语言特性不代表字节码本身无法有效支持,这也为其他语言
实现一些有别于Java的语言特性提供了基础。
6.3、Class类文件的结构
注意:
任何一个Class文件都对应着唯一一个类或者接口的定义信息
反过来说:类或者接口并不一定都得定义再文件里
Class文件是一组以8位字节为基础的二进制流,各个数据项严格按照顺序紧凑的排列再Class文件之中
中间没有添加任何分隔符,使得整个Class文件中的存储内容几乎全部是运行的必要数据,没有空隙存在
当遇到需要占用8位字节以上的数据项时,则会按照高位在前的方式分割成若干个8位字节进行存储
Class文件格式采用一种类似于C语言结构体的伪结构体来存储数据
这种伪结构中只有两种数据类型:无符号数和表,解析都要经过这两种数据类型为基础
无符号数属于基本数据类型,以u1、u2、u4、u8来分别代表一个字节,2个字节,4个字节,8个字节的无符号数
无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值
表是由多个无符号数或者其他表作为数据项构成的符合数据类型,所有表都习惯性的以“_info"结尾
Class文件格式:
无论是无符号数还是表,当需要描述同一个类型但数量不定的多个数据,经常会使用一个前置的容量计数器
加若干个连续的数据项形式,这时称这一系列连续的某一项数据为某一个类型的集合
6.3.1、魔数于Class文件的版本
每个Class文件的头4个字节称为魔数(Magic Number),它的唯一作用就是确定这个文件是否为一个能被虚拟机接受的Class文件
很多文件存储标准中都使用魔数来进行身份识别,如图片格式、gif过着jpeg等再文件头中都存在魔数
使用魔数而不是扩展名来进行识别主要是基于安全性方面的考虑,因为文件扩展名可以随意改动
文件格式的制定者可以自由的选择魔数值,只要这个魔数值还没有被广泛采用过的同时又不会引起混淆即可
魔数值在Java还称做“Oak”语言的时候(大约是1991年前后)就已经确定下来了。
紧接着魔数的4个字节存储的是Class文件的版本号:
第5、第6个字节是次版本号
第7、第8个字节是主版本号
Java的版本号是从45开始的,JDK1.1之后的每个JDK大版本发布主版本号向上加1
高版本的JDK能向下兼容以前版本的Class文件,但不能运行以后此版本的Class文件
即使文件格式并未发生任何变化,虚拟机也必须拒绝执行超过其版本号的Class文件
新建一个Class文件:
使用WinHex打开这个Class文件:
开头4个字节:0x7061636B
代表次版本号:0x6167
主版本号:0x6520
JDK1.1到JDK1.7主流JDK版本编译输出的默认和可支持的Class文件版本号:
这种顺序称为”Big-Endian“,具体是指最高位字节再地址的最低位
最低位字节再地址最高位的顺序来存储数据
它是SPARC、PowerPC等处理器的默认多字节存储顺序。
6.3.2、常量池
接着主次版本号之后的是常量池入口,常量池可以理解位Class文件之中的资源仓库
它是Class文件结构中与其他项目关联最多的数据类型
也是占用Class问价空间最大的数据项目之一,同时还是再Class文件中第一个出现的类型数据项目
由于常量池中常量数目不是固定的,所以常量池入口需要放置一项u2类型的数据
代表常量池容量计数值
与Java中语言习惯不一样的是:这个容器计数是从1开始的
上图所示的常量池容量为0x6F72
在Class文件格式规范制定之时,设计者将第0项常量空出来是有特殊考虑的,这样做的目的在
于满足后面某些指向常量池的索引值的数据在特定情况下需要表达“不引用任何一个常量池
项目”的含义,这种情况就可以把索引值置为0来表示。
Class文件结构中只有常量池的容量计
数是从1开始,对于其他集合类型,包括接口索引集合、字段表集合、方法表集合等的容量
计数都与一般习惯相同,是从0开始的
常量池中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References)。
字面量比较接近于Java语言层面的常量概念,如文本字符串、声明为final的常量值等
符号引用则属于编译原理方面的概念,包括了下面三类常量:
1、类和接口的全限定名(Fully Qualified Name)
2、字段的名称和描述符(Descriptor)
3、方法的名称和描述符
Java代码再进行javac编译的时候,不像C、C++那样由“连接”这一个步骤
而是再虚拟机中加载class文件的时候进行动态连接
Class文件中不会保存各个方法、字段的最终内存布局信息
因此这些字段、方法的符号引用不经过运行期转换的话无法得到真正的内存入口地址
也就无法直接被虚拟机使用
当虚拟机运行时,需要从常量池获得对应的符号引用
在类创建时或者运行时解析、翻译到具体的内存地址之中
常量池中每一项常量都是一个表,在JDK 1.7之前共有11中结构各不相同的表结构数据
在JDK1.7为了更好的支持动态语言调用,又增加了三种:
(CONSTANT_MethodHandle_info、CONSTANT_MethodType_info和CONSTANT_InvokeDynamic_info)
这14种都有一个共同特点
从表的第一位是一个u1类型的标志位(tag)代表当前常量属于那种常量类型
常量池的项目类型:
6.3.3、访问标志
在常量池结束之后,紧接着两个字节代表访问标志(access_flags)
这个标志用于识别一些类或者接口层次的访问信息
包括:这个Class是类还是接口,是否定义public类;是否是abstract雷尼格
如果是类的话,是否声明为ginal等
具体的标志位以及标志的含义:
access_flags中一共有16个标志位可以使用,当前只定义了其中8个[1],没有使用到的标志
位要求一律为0。以代码清单6-1中的代码为例,TestClass是一个普通Java类,不是接口、枚
举或者注解,被public关键字修饰但没有被声明为final和abstract,并且它使用了JDK 1.2之后
的编译器进行编译,因此它的ACC_PUBLIC、ACC_SUPER标志应当为真,而ACC_FINAL、
ACC_INTERFACE、ACC_ABSTRACT、ACC_SYNTHETIC、ACC_ANNOTATION、
ACC_ENUM这6个标志应当为假,因此它的access_flags的值应为:0x0001|0x0020=0x0021。
6.3.4、类索引、父类索引与接口索引集合
类索引和父类索引都是一个u2类型数据
接口索引集合是一组u2类型的数据集合
Class文件是由这三项数据来确定这个类的继承关系
类索引:用于确定这个类的全限定名
父类索引:确定这个类的父类的全限定名,Java不支持多继承,所以父类索引只有一个
除了java.lang.Object之外,所有的java类都有父类
故:除了java.lang.Object之外所有java类的父类索引都不为零。
接口索引集合就
用来描述这个类实现了哪些接口,这些被实现的接口将按implements语句(如果这个类本身
是一个接口,则应当是extends语句)后的接口顺序从左到右排列在接口索引集合中。
类索引、父类索引和接口索引集合都按顺序排列在访问标志之后,类索引和父类索引用
两个u2类型的索引值表示,它们各自指向一个类型为CONSTANT_Class_info的类描述符常
量,通过CONSTANT_Class_info类型的常量中的索引值可以找到定义在
CONSTANT_Utf8_info类型的常量中的全限定名字符串。
对于接口索引集合,入口的第一项——u2类型的数据为接口计数器
(interfaces_count),表示索引表的容量。如果该类没有实现任何接口,则该计数器值为0,
后面接口的索引表不再占用任何字节
类索引查找全限定名的过程:
6.3.5、字段表集合
字段表(field_info)用于描述接口或者类中声明的变量。
字段(field)包括类级变量以及实例级变量,但不包括在方法内部声明的局部变量。
java中描述一个字段可以包含的信息:
字段的作用域(public、private、protected修饰符)
实例变量还是类变量(static修饰符)
可变性(final)
并发可见性(volatile修饰符,是否强制从主内存读写)
可否被序列化(transient修饰符)
字段数据类型(基本类型、对象、数组)
字段名称。
上述这些信息中,各个修饰符都是布尔值,要么有某个修饰
符,要么没有,很适合使用标志位来表示。而字段叫什么名字、字段被定义为什么数据类
型,这些都是无法固定的,只能引用常量池中的常量来描述。
字段表结构:
字段修饰符放在access_flags项目中,它与类中的access_flags项目是非常类似的,都是一
个u2的数据类型
字段访问标志:
在实际情况中,ACC_PUBLIC、ACC_PRIVATE、ACC_PROTECTED三个标志
最多只能选择其一,ACC_FINAL、ACC_VOLATILE不能同时选择。接口之中的字段必须有
ACC_PUBLIC、ACC_STATIC、ACC_FINAL标志,这些都是由Java本身的语言规则所决定的。
跟随access_flags标志的是两项索引值:name_index和descriptor_index。它们都是对常量
池的引用,分别代表着字段的简单名称以及字段和方法的描述符。现在需要解释一下“简单
名称”、“描述符”以及前面出现过多次的“全限定名”这三种特殊字符串的概念。
相对于全限定名和简单名称来说,方法和字段的描述符就要复杂一些。描述符的作用是
用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值。根据描
述符规则,基本数据类型(byte、char、double、float、int、long、short、boolean)以及代表
无返回值的void类型都用一个大写字符来表示,而对象类型则用字符L加对象的全限定名来
表示
描述符标识字符含义:
对于数组类型,每一维度将使用一个前置的“[”字符来描述,如一个定义
为“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语言中字段是无法重载的,两个字段的数据类型、修饰符不管是
否相同,都必须使用不一样的名称,但是对于字节码来讲,如果两个字段的描述符不一致,
那字段重名就是合法的。
6.3.6、方发表集合
Class文件存储格式中对方法的描述与对字段的描述几乎用了完全一致的方式
方法表的结构如同字段一样,依次包括了访问标志、名称索引、描述符索引、属性表集合
方法表结构:
因为volatile关键字和transient关键字不能修饰方法,所以方法表的访问标志中没有了
ACC_VOLATILE标志和ACC_TRANSIENT标志。与之相对的,synchronized、native、strictfp
和abstract关键字可以修饰方法,所以方法表的访问标志中增加了ACC_SYNCHRONIZED、
ACC_NATIVE、ACC_STRICTFP和ACC_ABSTRACT标志。
方法访问标志:
方法的定义可以通过访问标志、名称索引、描述符索引表达清楚,方法代码去哪里?
方法里的Java代码,经过编译器译成字节码文件指令后,存放在方法属性集合中一个名为“Code”的属性里面
属性表作为Class文件格式中最具有扩展性的一种数据项目
与字段表集合相对应的,如果父类方法在子类中没有被重写(Override),方法表集合
中就不会出现来自父类的方法信息。但同样的,有可能会出现由编译器自动添加的方法,最
典型的便是类构造器“<clinit>”方法和实例构造器“<init>”方法
在Java语言中,要重载(Overload)一个方法,除了要与原方法具有相同的简单名称之
外,还要求必须拥有一个与原方法不同的特征签名,特征签名就是一个方法中各个参数在
常量池中的字段符号引用的集合,也就是因为返回值不会包含在特征签名中,因此Java语言
里面是无法仅仅依靠返回值的不同来对一个已有方法进行重载的。但是在Class文件格式中,
特征签名的范围更大一些,只要描述符不是完全一致的两个方法也可以共存。也就是说,如
果两个方法有相同的名称和特征签名,但返回值不同,那么也是可以合法共存于同一个Class
文件中的。
6.3.7、属性表集合
在Class文件、字段表、方
法表都可以携带自己的属性表集合,以用于描述某些场景专有的信息。
与Class文件中其他的数据项目要求严格的顺序、长度和内容不同,属性表集合的限制稍
微宽松了一些,不再要求各个属性表具有严格顺序,并且只要不与已有属性名重复,任何人
实现的编译器都可以向属性表中写入自己定义的属性信息,Java虚拟机运行时会忽略掉它不
认识的属性。
虚拟机规范预定义的属性:
对于每个属性,它的名称需要从常量池中引用一个CONSTANT_Utf8_info类型的常量来表示
属性值的结构完全是自定义的
只需要通过一个u4的长度属性去说明属性值所占用的位数即可
属性表结构:
1、Code属性
Java程序方法体中的代码经过Javac编译器处理后,最终变为字节码指令存储在Code属性
内。Code属性出现在方法表的属性集合之中,但并非所有的方法表都必须存在这个属性,譬
如接口或者抽象类中的方法就不存在Code属性,如果方法表有Code属性存在,那么它的结构
将如下图所示:
Code属性表的结构
attribute_name_index是一项指向CONSTANT_Utf8_info型常量的索引,常量值固定
为“Code”,它代表了该属性的属性名称,attribute_length指示了属性值的长度,由于属性名称
索引与属性长度一共为6字节,所以属性值的长度固定为整个属性表长度减去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来存放。方法参数(包括实例方法中的隐藏参
数“this”)、显式异常处理器的参数(Exception Handler Parameter,就是try-catch语句中catch
块所定义的异常)、方法体中定义的局部变量都需要使用局部变量表来存放。另外,并不是
在方法中用到了多少个局部变量,就把这些局部变量所占Slot之和作为max_locals的值,原因
是局部变量表中的Slot可以重用,当代码执行超出一个局部变量的作用域时,这个局部变量
所占的Slot可以被其他局部变量所使用,Javac编译器会根据变量的作用域来分配Slot给各个
变量使用,然后计算出max_locals的大小。
code_length和code用来存储Java源程序编译后生成的字节码指令。code_length代表字节码
长度,code是用于存储字节码指令的一系列字节流。既然叫字节码指令,那么每个指令就是
一个u1类型的单字节,当虚拟机读取到code中的一个字节码时,就可以对应找出这个字节码
代表的是什么指令,并且可以知道这条指令后面是否需要跟随参数,以及参数应当如何理
解。我们知道一个u1数据类型的取值范围为0x00~0xFF,对应十进制的0~255,也就是一共
可以表达256条指令,目前,Java虚拟机规范已经定义了其中约200条编码值对应的指令含
义
关于code_length,有一件值得注意的事情,虽然它是一个u4类型的长度值,理论上最大
值可以达到232-1,但是虚拟机规范中明确限制了一个方法不允许超过65535条字节码指令,
即它实际只使用了u2的长度,如果超过这个限制,Javac编译器也会拒绝编译。一般来讲,编
写Java代码时只要不是刻意去编写一个超长的方法来为难编译器,是不太可能超过这个最大
值的限制。但是,某些特殊情况,例如在编译一个很复杂的JSP文件时,某些JSP编译器会把
JSP内容和页面输出的信息归并于一个方法之中,就可能因为方法生成字节码超长的原因而
导致编译失败。
Code属性是Class文件中最重要的一个属性,如果把一个Java程序中的信息分为代码
(Code,方法体里面的Java代码)和元数据(Metadata,包括类、字段、方法定义及其他信
息)两部分,那么在整个Class文件中,Code属性用于描述代码,所有的其他数据项目都用
于描述元数据。
2.Exceptions属性
Exceptions属性是在方法表中与Code属性平级的一项属性
Exceptions属性的作用是列举出方法中可能抛出的受查异常
(Checked Excepitons),也就是方法描述时在throws关键字后面列举的异常。
属性表结构:
Exceptions属性中的number_of_exceptions项表示方法可能抛出number_of_exceptions种受查
异常,每一种受查异常使用一个exception_index_table项表示,exception_index_table是一个指
向常量池中CONSTANT_Class_info型常量的索引,代表了该受查异常的类型。
3.LineNumberTable属性
LineNumberTable属性用于描述Java源码行号与字节码行号(字节码的偏移量)之间的对应关系。
它并不是运行时必需的属性,但默认会生成到Class文件之中,可以在Javac中分别
使用-g:none或-g:lines选项来取消或要求生成这项信息。
如果选择不生成LineNumberTable
属性,对程序运行产生的最主要的影响就是当抛出异常时,堆栈中将不会显示出错的行号,
并且在调试程序的时候,也无法按照源码行来设置断点。
LineNumberTable属性表结构:
line_number_table是一个数量为line_number_table_length、类型为line_number_info的集
合,line_number_info表包括了start_pc和line_number两个u2类型的数据项,前者是字节码行
号,后者是Java源码行号。
4.LocalVariableTable属性
LocalVariableTable属性用于描述栈帧中局部变量表中的变量与Java源码中定义的变量之
间的关系,它也不是运行时必需的属性,但默认会生成到Class文件之中,可以在Javac中分
别使用-g:none或-g:vars选项来取消或要求生成这项信息。
如果没有生成这项属性,最大的
影响就是当其他人引用这个方法时,所有的参数名称都将会丢失,IDE将会使用诸如arg0、
arg1之类的占位符代替原有的参数名,这对程序运行没有影响,但是会对代码编写带来较大
不便,而且在调试期间无法根据参数名称从上下文中获得参数值。
LocalVariableTable属性结构:
local_variable_info项目结构:
start_pc和length属性分别代表了这个局部变量的生命周期开始的字节码偏移量及其作用
范围覆盖的长度,两者结合起来就是这个局部变量在字节码之中的作用域范围
name_index和descriptor_index都是指向常量池中CONSTANT_Utf8_info型常量的索引,分
别代表了局部变量的名称以及这个局部变量的描述符。
index是这个局部变量在栈帧局部变量表中Slot的位置。当这个变量数据类型是64位类型
时(double和long),它占用的Slot为index和index+1两个。
在JDK 1.5引入泛型之后,LocalVariableTable属性增加了一个“姐妹属性”:
LocalVariableTypeTable,这个新增的属性结构与LocalVariableTable非常相似,仅仅是把记录
的字段描述符的descriptor_index替换成了字段的特征签名(Signature),对于非泛型类型来
说,描述符和特征签名能描述的信息是基本一致的,但是泛型引入之后,由于描述符中泛型
的参数化类型被擦除掉,描述符就不能准确地描述泛型类型了,因此出现了
LocalVariableTypeTable。
5.SourceFile属性
SourceFile属性用于记录生成这个Class文件的源码文件名称。这个属性也是可选的,可
以分别使用Javac的-g:none或-g:source选项来关闭或要求生成这项信息。在Java中,对于大
多数的类来说,类名和文件名是一致的,但是有一些特殊情况(如内部类)例外。如果不生
成这项属性,当抛出异常时,堆栈中将不会显示出错代码所属的文件名。
SourceFile属性结构:
sourcefile_index数据项是指向常量池中CONSTANT_Utf8_info型常量的索引,常量值是源
码文件的文件名。
6.ConstantValue属性
ConstantValue属性的作用是通知虚拟机自动为静态变量赋值。只有被static关键字修饰的
变量(类变量)才可以使用这项属性。
类似“int x=123”和“static int x=123”这样的变量定义在
Java程序中是非常常见的事情,但虚拟机对这两种变量赋值的方式和时刻都有所不同。
对于非static类型的变量(也就是实例变量)的赋值是在实例构造器<init>方法中进行的;而对
于类变量,则有两种方式可以选择:在类构造器<clinit>方法中或者使用ConstantValue属
性。
目前Sun Javac编译器的选择是:如果同时使用final和static来修饰一个变量(按照习惯,
这里称“常量”更贴切),并且这个变量的数据类型是基本类型或者java.lang.String的话,就生
成ConstantValue属性来进行初始化,如果这个变量没有被final修饰,或者并非基本类型及字
符串,则将会选择在<clinit>方法中进行初始化
虽然有final关键字才更符合“ConstantValue”的语义,但虚拟机规范中并没有强制要求字
段必须设置了ACC_FINAL标志,只要求了有ConstantValue属性的字段必须设置ACC_STATIC
标志而已,对final关键字的要求是Javac编译器自己加入的限制。
而对ConstantValue的属性值
只能限于基本类型和String,不过笔者不认为这是什么限制,因为此属性的属性值只是一个
常量池的索引号,由于Class文件格式的常量类型中只有与基本属性和字符串相对应的字面
量,所以就算ConstantValue属性想支持别的类型也无能为力。
ConstantValue属性结构:
从数据结构中可以看出,ConstantValue属性是一个定长属性,它的attribute_length数据项
值必须固定为2。
constantvalue_index数据项代表了常量池中一个字面量常量的引用,根据字
段类型的不同,字面量可以是CONSTANT_Long_info、CONSTANT_Float_info、
CONSTANT_Double_info、CONSTANT_Integer_info、CONSTANT_String_info常量中的一种。
7.InnerClasses属性
InnerClasses属性用于记录内部类与宿主类之间的关联。如果一个类中定义了内部类,那
编译器将会为它以及它所包含的内部类生成InnerClasses属性。
InnerClasses属性结构:
数据项number_of_classes代表需要记录多少个内部类信息,每一个内部类的信息都由一
个inner_classes_info表进行描述。
inner_classes_info表的结构:
inner_class_info_index和outer_class_info_index都是指向常量池中CONSTANT_Class_info
型常量的索引,分别代表了内部类和宿主类的符号引用
inner_name_index是指向常量池中CONSTANT_Utf8_info型常量的索引,代表这个内部类
的名称,如果是匿名内部类,那么这项值为0。
inner_class_access_flags是内部类的访问标志,类似于类的access_flags,它的取值范围见下图:
inner_class_access_flags标志:
8.Deprecated及Synthetic属性
Deprecated和Synthetic两个属性都属于标志类型的布尔属性,只存在有和没有的区别,没
有属性值的概念。
Deprecated属性用于表示某个类、字段或者方法,已经被程序作者定为不再推荐使用,
它可以通过在代码中使用@deprecated注释进行设置。
Synthetic属性代表此字段或者方法并不是由Java源码直接产生的,而是由编译器自行添
加的,在JDK 1.5之后,标识一个类、字段或者方法是编译器自动产生的,也可以设置它们
访问标志中的ACC_SYNTHETIC标志位,其中最典型的例子就是Bridge Method。所有由非用
户代码产生的类、方法及字段都应当至少设置Synthetic属性和ACC_SYNTHETIC标志位中的
一项,唯一的例外是实例构造器“<init>”方法和类构造器“<clinit>”方法。
Deprecated及Synthetic属性结构:
其中attribute_length数据项的值必须为0x00000000,因为没有任何属性值需要设置。
9.StackMapTable属性
StackMapTable属性在JDK 1.6发布后增加到了Class文件规范中,它是一个复杂的变长属
性,位于Code属性的属性表中。这个属性会在虚拟机类加载的字节码验证阶段被新类型检查
验证器(Type Checker)使用,目的在于代替以前比较消耗性能的基于数据流
分析的类型推导验证器。
这个类型检查验证器最初来源于Sheng Liang为Java ME CLDC实现的字节码验证器。
新的验证器在同样能保证Class文件合法性的前提
下,省略了在运行期通过数据流分析去确认字节码的行为逻辑合法性的步骤,而是在编译阶
段将一系列的验证类型(Verification Types)直接记录在Class文件之中,通过检查这些验证
类型代替了类型推导过程,从而大幅提升了字节码验证的性能。
这个验证器在JDK 1.6中首
次提供,并在JDK 1.7中强制代替原本基于类型推断的字节码验证器。
StackMapTable属性中包含零至多个栈映射帧(Stack Map Frames),每个栈映射帧都显
式或隐式地代表了一个字节码偏移量,用于表示该执行到该字节码时局部变量表和操作数栈
的验证类型。
类型检查验证器会通过检查目标方法的局部变量和操作数栈所需要的类型来确
定一段字节码指令是否符合逻辑约束
StackMapTable属性结构:
《Java虚拟机规范(Java SE 7版)》明确规定:在版本号大于或等于50.0的Class文件
中,如果方法的Code属性中没有附带StackMapTable属性,那就意味着它带有一个隐式的
StackMap属性。这个StackMap属性的作用等同于number_of_entries值为0的StackMapTable属
性。一个方法的Code属性最多只能有一个StackMapTable属性,否则将抛出ClassFormatError异常
10.Signature属性
Signature属性在JDK 1.5发布后增加到了Class文件规范之中,它是一个可选的定长属性,
可以出现于类、属性表和方法表结构的属性表中。
在JDK 1.5中大幅增强了Java语言的语法,
在此之后,任何类、接口、初始化方法或成员的泛型签名如果包含了类型变量(Type
Variables)或参数化类型(Parameterized Types),则Signature属性会为它记录泛型签名信息。
之所以要专门使用这样一个属性去记录泛型类型,是因为Java语言的泛型采用的是擦除
法实现的伪泛型,在字节码(Code属性)中,泛型信息编译(类型变量、参数化类型)之后
都通通被擦除掉。
使用擦除法的好处是实现简单(主要修改Javac编译器,虚拟机内部只做
了很少的改动)、非常容易实现Backport,运行期也能够节省一些类型所占的内存空间。
但坏处是运行期就无法像C#等有真泛型支持的语言那样,将泛型类型与用户定义的普通类型同
等对待,例如运行期做反射时无法获得到泛型信息。
Signature属性就是为了弥补这个缺陷而
增设的,现在Java的反射API能够获取泛型类型,最终的数据来源也就是这个属性。
Singnature属性结构:
其中signature_index项的值必须是一个对常量池的有效索引。常量池在该索引处的项必须
是CONSTANT_Utf8_info结构,表示类签名、方法类型签名或字段类型签名。
如果当前的
Signature属性是类文件的属性,则这个结构表示类签名,如果当前的Signature属性是方法表
的属性,则这个结构表示方法类型签名,如果当前Signature属性是字段表的属性,则这个结
构表示字段类型签名
11.BootstrapMethods属性
BootstrapMethods属性在JDK 1.7发布后增加到了Class文件规范之中,它是一个复杂的变
长属性,位于类文件的属性表中。
这个属性用于保存invokedynamic指令引用的引导方法限定符。
《Java虚拟机规范(Java SE 7版)》规定,如果某个类文件结构的常量池中曾经出现过
CONSTANT_InvokeDynamic_info类型的常量,那么这个类文件的属性表中必须存在一个明确
的BootstrapMethods属性,另外,即使CONSTANT_InvokeDynamic_info类型的常量在常量池中
出现过多次,类文件的属性表中最多也只能有一个BootstrapMethods属性。
BootstrapMethods
属性与JSR-292中的InvokeDynamic指令和java.lang.Invoke包关系非常密切,要介绍这个属性的
作用,必须先弄清楚InovkeDynamic指令的运作原理,在后期进行介绍
目前的Javac暂时无法生成InvokeDynamic指令和BootstrapMethods属性,必须通过一些非
常规的手段才能使用到它们,也许在不久的将来,等JSR-292更加成熟一些,这种状况就会
改变。
BootstrapMethods属性的结构:
bootstrap_method属性的结构:
BootstrapMethods属性中,num_bootstrap_methods项的值给出了bootstrap_methods[]数组中
的引导方法限定符的数量。而bootstrap_methods[]数组的每个成员包含了一个指向常量池
CONSTANT_MethodHandle结构的索引值,它代表了一个引导方法,还包含了这个引导方法
静态参数的序列(可能为空)。bootstrap_methods[]数组中的每个成员必须包含以下3项内
容。
bootstrap_method_ref:bootstrap_method_ref项的值必须是一个对常量池的有效索引。常
量池在该索引处的值必须是一个CONSTANT_MethodHandle_info结构。
num_bootstrap_arguments:num_bootstrap_arguments项的值给出了bootstrap_arguments[]数
组成员的数量。
bootstrap_arguments[]:bootstrap_arguments[]数组的每个成员必须是一个对常量池的有效
索引。常量池在该索引处必须是下列结构之一:CONSTANT_String_info、
CONSTANT_Class_info、CONSTANT_Integer_info、CONSTANT_Long_info、
CONSTANT_Float_info、CONSTANT_Double_info、CONSTANT_MethodHandle_info或
CONSTANT_MethodType_info。
6、JVM--类文件结构(上)的更多相关文章
- JVM类文件结构
作为一名Java后台开发的程序员, 深入理解JVM, 重要性不言而喻, 这篇文章主要是记录JVM类文件结构相关知识. 2. 实例 这部分比较抽象, 所以以实例的形式来学习. 这部分作为资料, 以便后面 ...
- jvm 类文件结构学习
本文以代码示例来学习 java 类文件的结构,其中对类文件结构的学习均来自周志明先生所著的 <深入理解 Java 虚拟机>一书,在此表示诚挚的感谢. 代码如下: package com.r ...
- 【搞定Jvm面试】 面试官:谈谈 JVM 类文件结构的认识
类文件结构 一 概述 在 Java 中,JVM 可以理解的代码就叫做字节码(即扩展名为 .class 的文件),它不面向任何特定的处理器,只面向虚拟机.Java 语言通过字节码的方式,在一定程度上解决 ...
- 四、JVM — 类文件结构
类文件结构 一 概述 二 Class 文件结构总结 2.1 魔数 2.2 Class 文件版本 2.3 常量池 2.4 访问标志 2.5 当前类索引,父类索引与接口索引集合 2.6 字段表集合 2.7 ...
- JVM —— 类文件结构(上)
一.概述 实现语言无关性的基础仍然是虚拟机和字节码存储格式.java虚拟机不和包括java在内的任何语言绑定,它只与“Class文件”这种特定的二进制文件格式所关联,Class文件中包含了java虚拟 ...
- JVM —— 类文件结构(下)
简介 Java虚拟机的指令由一个字节长度的.代表着某种特定操作含义的操作码(opcode)以及跟随其后的零至多个代表此操作所需参数的操作数(operand)所构成 虚拟机中许多指令并不包含操作数.只有 ...
- 深入理解JVM(六)类文件结构
6.1 关于类文件 1.class文件的一次编译,到处运行的跨平台性: 2.JVM不止有跨平台性,还有跨语言性,不管是JRuby还是Groovy写出来的程序,只要编译出符合JVM规范的class文件就 ...
- JVM学习笔记(三):类文件结构
代码编译的结果从本地机器码转变为字节码,是存储格式发展的一小步,却是编程语言发展的一大步. 实现语言无关性的基础是虚拟机和字节码存储格式.Java虚拟机不和包括Java在内的任何语言绑定,只与&quo ...
- JVM学习第三天(JVM的执行子系统)之开篇Class类文件结构
虽然这几天 很忙,但是学习是不能落下的,也不能推迟,因为如果推迟了一次,那么就会有无数次;加油,come on! Java跨平台的基础: 各种不同平台的虚拟机与所有平台都统一使用的程序存储格式——字节 ...
- JVM学习笔记-第六章-类文件结构
JVM学习笔记-第六章-类文件结构 6.3 Class类文件的结构 本章中,笔者只是通俗地将任意一个有效的类或接口锁应当满足的格式称为"Class文件格式",实际上它完全不需要以磁 ...
随机推荐
- uni-app初体验及打包成apk
首先用HBuilderX新建建一个uni-app项目 新建一个目录ucenter,该目录下新建两个vue文件ucenter.vue和setting.vue ucenter.vue <templa ...
- Idea无法运行Maven项目
导入项目到tomcat的时候要选择Arifact 如果maven项目没有这个选项, <groupId>com.bihang</groupId> <artifactId&g ...
- JQ面试问题(转载)
1 你在公司是怎么用jquery的? 答:在项目中是怎么用的是看看你有没有项目经验(根据自己的实际情况来回答) 你用过的选择器啊,动画啊,表单啊,ajax事件等 配置Jquery环境 下载jquery ...
- Thinkphp+ECharts生成柱状图
1.首先进ECharts官网下载echarts.js 点击下载,结合TP5讲解,主要代码在js里面,更多请到ECharts官网 2.引进echarts.js <!DOCTYPE html> ...
- 设计模式原则(5)--Law of Demeter(LoD)--迪米特法则
作者QQ:1095737364 QQ群:123300273 欢迎加入! 1.定义: 一个软件实体应当尽可能少地与其他实体发生相互作用.也就是说:一个类对自己依赖的类知道的越少越好.也就是 ...
- kotlin-2(IdeaIU-2018.2汉化破解)
1.下载文件包: 链接:https://pan.baidu.com/s/1AaAqkJ5E88k69dhcDiC0tA 提取码:b5uk 2.点击ideaIU-2018.2安装软件,安装完成后,不要点 ...
- Flask结合Redis消息队列实现电影弹幕
用到的弹幕播放器插件:dplayer.js(开源) 1.安装Redis 2.安装flask-redis包 pip install flask-redis3.下载dplayer 页面搭建 1.引入资源 ...
- [基础知识]在PeopleSoft中SMTP设置不生效如何查找问题
在PeopleSoft中如果配置了工作流邮件或者标准页面的通知,都是可以发送出邮件的,这些邮件都是由SMTP服务器发送.SMTP需要在APP服务器和PRCS服务器中配置. 如果无法从PeopleSof ...
- hadoop重启后,hdfs目录权限问题
今天重启了下Hadoop集群,使用Eclipse调试hdfs api的时候报错: [WARNING] java.lang.NullPointerException at org.conan.kafka ...
- Android手机上,利用bat脚本模拟用户操作
………… 那么你就可以来看看这篇帖子了. 言归正传 利用bat脚本模拟用户操作,需要用到两点: ①就是adb命令了,adb命令可以用来模拟用户在手机上的操作 ②bat语言,就是批处理语言,主要用来进行 ...