概述
由于计算机只识别0、1,因此,越来越多的程序语言选择了与操作系统和机器指令集无关的、平台中立的格式来作为程序编译后的存储格式。
平台无关性的基石
各种不同平台的虚拟机与所有平台都统一使用的程序存储格式:字节码是构成平台无关性的基石。
实现语言无关性的基础仍然是虚拟机和字节码存储格式,使用JAVA编译器可以把JAVA代码编译为存储字节码的Class文件,使用JRuby等其他语言编写的编译器一样可以把程序代码编译成Class文件,虚拟机并不关心Class的来源是什么语言,只要其符合Class文件应有的结构就可以在JAVA虚拟机中运行。
JAVA语言中的各种变量、关键字、运算符号的语义最终都是由多条字节码命令组合而成的,因此字节码命令所能提供的语义描述能力肯定会比JAVA语言本身更加强大。因此,有一些JAVA语言本身无法有效支持的语言特性并不代表字节码本身无法有效支持,这也是其他语言实现一些有别于JAVA的语言特性提供了基础。
Class类文件的结构:概述
Class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在Class文件之中,中间没有添加任何分隔符,这就使得整个Class文件中存储的内容几乎全部都是程序运行的必要数据,没有空隙存在。
当遇到需要占用8位字节以上空间的数据项时,则会按照高位在前的方式分割成若干个8位字节来进行存储。
根据JAVA虚拟机规范的规定:Class文件格式采用一种类似于C语言结构体的伪结构来存储,这种伪结构中只有2种数据类型:无符号数、表。
无符号数属于基本的数据类型,以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节、8个字节的无符号数。无符号数可以用来描述数字、索引引用、数量值、按照UTF-8编码构成的字符串值。
表是由多个无符号数,或者其他表作为数据项构成的复合数据类型,所有表都习惯性地以“_info”结尾。表用于描述有层次关系的、复合结构的数据,整个Class文件本质上就是一张表,该表所示的数据项构成:
类型 | 名称 | 数量 |
u4 | magic魔数,4字节 | 1 |
u2 | minor_version次版本号,2字节 | 1 |
u2 | major_version主版本号,2字节 | 1 |
u2 | constant_pool_count常量池,2字节 | 1 |
cp_info | constant_pool,字面量、符号引用 | constant_pool_count - 1 |
u2 | access_flags | 1 |
u2 | this_class | 1 |
u2 | super_class | 1 |
u2 | interfaces_count | 1 |
u2 | interfaces | interfaces_count |
u2 | fields_count | 1 |
field_info | fields | fields_count |
u2 | methods_count | 1 |
method_info | methods | methods_count |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
由于Class文件没有任何分隔符,因此table中所列出的数据项,无论是顺序还是数量,都是被严格限定的,哪个字节代表什么含义,长度是多少,先后顺序如何,都不允许改变。
无论是无符号数,还是表,当需要描述同一类型但数量不定多的多个数据时,经常会使用一个前置的容量计数器加若干个连续的数据项形成,称这一系列连续的某一类型的数据为:某一类型的集合。
Class类文件的结构:魔数与Class文件的版本magic/minor_version/major_version
每个Class文件的头4个字节被称为:魔数。
魔数唯一作用是用于确定这个Class文件是否为一个能被虚拟机接受的Class文件。很多文件存储标准中都使用魔数来进行身份识别,比如gif、jpeg等文件头中都存有魔数。
使用魔数而不是扩展名来进行识别主要是基于安全考虑,因为文件扩展名可以随意地被改动。文件格式的制定者可以自由地选择魔数值,只要这个魔数值还没有被广泛采用过而且不会引起混淆即可。
Class文件的魔数值为:0xCAFEBABE,该魔数值在大约1991年前后就确定下来了。
紧接着魔数后面的4个字节存储的是Class文件的版本号:第5和第6个字节是次版本号(Minor Version),第7和第8个字节是主版本号(Major Version)。
JAVA的版本号是从45开始的,JDK1.1之后的每个JDK大版本发布后主版本号向上加1,例如:JDK1.0-1.1,使用了45.0-45.3的版本号,高版本的JDK能向下兼容以前版本的Class文件,但不能运行以后版本的Class文件,即使文件格式并未发生变化,例如:JDK1.1能支持的版本号45.0-45.65535的Class,无法执行版本号为46.0以上的Class,而JDK1.2则能支持版本号45.0-45.65535的Class文件。
JDK1.1 - 45
JDK1.7 - 51
由此可见,每当JDK升级后,主版本号加1,后续则是次版本号依次递增。这里可以使用UralEdit直接打开Class文件来查看4字节的魔数、2个字节的次版本号、2个字节的主版本号。
Class类文件的结构:常量池constant_pool_count/constant_pool
4字节的魔数、2个字节的次版本号、2个字节的主版本号后面,紧接着就是常量池入口。
常量池是Class文件结构中与其他项目关联最多的数据类型,也是占用Class文件空间最大的数据项目之一,同时它还是在Class文件中第一个出现的表类型数据项目。
由于常量池中常量的数量是不固定的,所以在常量池的入口需要放置一项u2类型的数据,来代表常量池容量计数值。
与JAVA语言习惯不一样的是,该容量计数是从1而不是0开始。常量池容量,0x00000008)为16进制数0x0016,换成十进制就是22,这就代表常量池中有21项常量,其索引值从1-21。
在制定Class文件格式规范时,将第0项常量空出来是为了满足后面某些指向常量池的索引值的数据在某些特定情况下需要表达“不引用任何一个常量池项目”的意思,这种情况就可以把索引值设置为0来表示。
Class文件结构中只有常量池的容量计数是从1开始的,对于其他集合类型,包括接口索引集合、字段表集合、方法表集合等容量计数都是从0开始的。
常量池中主要存放2大类常量:字面量、符号引用。
字面量比较接近于JAVA语言层面的常量概念,比如:文本字符串、final常量值等。而符号引用则属于编译原理方面的概念,包括了下面3类常量:类和接口的全限定名、字段的名称和描述符、方法的名称和描述符。
JAVA代码在进行JAVAC编译时不会进行动态连接,而是在虚拟机加载Class文件的时候进行动态连接,即在Class文件中并不会保存各个方法、字段的最终内存布局信息,因此这些字段、方法的符号引用不经过转换的话是无法直接被虚拟机使用的。当虚拟机运行时,需要从常量池获得对应的符号引用,再在类创建时或者运行时解析、翻译到具体的内存地址中。
常量池中的每一项常量都是一个表,共有11种结构各不相同的表结构数据,这11种表都有一个共同的特点:表开始的第一位是一个u1类型的标志位(tag,取值为1-12,缺少标志为2的数据类型),代表当前这个常量属于哪种常量类型,11种常量类型所代表的具体含义如下:
类型 | 标志 | 描述 |
CONSTANT_Utf8_info | 1 | UTF-8编码的字符串 |
CONSTANT_Integer_info | 3 | 整型字面量 |
CONSTANT_Float_info | 4 | 浮点型字面量 |
CONSTANT_Long_info | 5 | 长整型字面量 |
CONSTANT_Double_info | 6 | 双精度浮点型字面量 |
CONSTANT_Class_info | 7 | 类或接口的符号引用 |
CONSTANT_String_info | 8 | 字符串类型字面量 |
CONSTANT_Fieldref_info | 9 | 字段的符号引用 |
CONSTANT_Methodref_info | 10 | 类中方法的符号引用 |
CONSTANT_InterfaceMethodref_info | 11 | 接口中方法的符号引用 |
CONSTANT_NameAndType_info | 12 | 字段或方法的部分符号引用 |
之所以说常量池是最繁琐的数据,是因为这11种常量类型各自均有自己的结构。
以上面这个图举例说明:0-3是魔数,4-5是次版本号,6-7是主版本号,8-9是常量池constant_pool_count,这里的A对应的是0x07,即tag=7,说明该常量属于CONSTANT_Class_info类型,此常量代表一个类或接口的符号引用。CONSTANT_Class_info结构组成如下:
类型 | 标志 | 描述 |
u1 | tag | 1 |
u2 | name_index | 1 |
tag是标志位,上面已经讲解过了,取值为1-12,缺少标志为2的数据类型,代表当前这个常量属于哪种常量类型。
name_index是一个索引值,它指向常量池中一个CONSTANT_Utf8_info类型的常量,此常量代表了这个类/接口的全限定名,由于name_index是u2类型的,因此这里的name_index值(偏移地址:0x0000000B)为0x0002,即指向了常量池中的第二项常量。
那我们再看看第二项常量,其tag是(偏移地址:0x0000000D)0x01,tag=1代表是一个CONSTANT_Utf8_info类型的常量。CONSTANT_Utf8_info类型的结构如下所示:
类型 | 标志 | 描述 |
u1 | tag | 1 |
u2 | length | 1 |
u1 | bytes | length |
length值说明了这个UTF-8编码的字符串长度是多少字节,它后面紧跟着的长度为length字节的连续数据时一个使用UTF-8缩略编码表示的字符串。UTF-8缩略编码与普通UTF-8编码区别在于:从\u0001-\u007f之间的字符(相当于1-127的ASCII)的缩略编码用1个字节表示,从\u0080-\u007f之间的所有字符的缩略编码用2个字节表示,从\u0800-\uffff之间的所有字符的缩略编码就按照普通UTF-8编码规则使用3个字节表示。
由于Class文件中方法、字段等都需要引用CONSTANT_Utf8_info类型常量来描述名称,所以CONSTANT_Utf8_info类型常量的最大长度也就是JAVA中方法、字段名的最大长度。而这里的最大长度就是length的最大值,即u2类型能表达的最大值65535。所以JAVA程序中如果定义了超过64KB英文字符的变量、方法名,则将无法编译。
本案例中该字符串length值是(偏移地址:0x0000000E)0x001D,换成10进制就是29个字节,即往后的29个字节正好都在1-127的ASCII码范围以内。内容是:org/fenixsoft/clazz/TestClass
TestClass.class常量池中剩余的19个常量都可以通过类似的方法计算出来。
在JDK的bin目录下,javap是一个专门用于分析Class文件字节码的工具,在Class中会自动生成一部分不在JAVA代码中直接出现过的常量,这些常量被用在字段表、方法表、属性表,用来描述一些不方便使用“固定字节”来表达的内容,这些内容如下:描述方法的返回值是什么类型、有几个参数、每个参数类型是什么等等。由于JAVA中的类数量是无穷无尽的,无法通过简单的无符号字节来描述一个方法用到了什么类,因此在描述方法的这些信息时,需要引用常量表中的符号引用来进行表达。
11种数据类型的结构汇总如下:
常量 | 项目 | 类型 | 描述 |
CONSTANT_Utf8_info | tag | u1 | 值为1 |
length | u2 | UTF-8编码的字符串占用的字节数 | |
bytes | u1 | 长度为length的UTF-8编码的字符串 | |
CONSTANT_Integer_info | tag | u1 | 值为3 |
bytes | u4 | 按照高位在前存储的int值 | |
CONSTANT_Float_info | tag | u1 | 值为4 |
bytes | u4 | 按照高位在前存储的float值 | |
CONSTANT_Long_info | tag | u1 | 值为5 |
bytes | u8 | 按照高位在前存储的long值 | |
CONSTANT_Double_info | tag | u1 | 值为6 |
bytes | u8 | 按照高位在前存储的double值 | |
CONSTANT_Class_info | tag | u1 | 值为7 |
index | u2 | 指向全限定名常量项的索引 | |
CONSTANT_String_info | tag | u1 | 值为4 |
index | u2 | 指向字符串字面量的索引 | |
CONSTANT_Fieldref_info | tag | u1 | 值为9 |
index | u2 | 指向声明字段的类/接口描述符CONSTANT_Class_info的索引项 | |
index | u2 | 指向字段描述符CONSTANT_NameAndType的索引项 | |
CONSTANT_Methodref_info | tag | u1 | 值为10 |
index | u2 | 指向声明方法的类描述符CONSTANT_Class_info的索引项 | |
index | u2 | 指向名称及类型描述符CONSTANT_NameAndType的索引项 | |
CONSTANT_InterfaceMehtodref_info | tag | u1 | 值为11 |
index | u2 | 指向声明方法的接口描述符CONSTANT_Class_info的索引项 | |
index | u2 | 指向名称及类型描述符CONSTANT_NameAndType的索引项 | |
CONSTANT_NameAndType_info | tag | u1 | 值为12 |
index | u2 | 指向该字段或方法名称常量项的索引 | |
index | u2 | 指向该字段或方法描述符常量项的索引 |
Class类文件的结构:访问标志access_flags
在常量池结束之后,紧挨着的2个字节代表访问标志access_flags,这个标志用于识别一些类或接口层次的访问信息,包括:这个Class是类还是接口、是否定义为public类型、是否定义为abstract类型、如果是类的话是否被声明为final等等。
具体标志位及标志含义如下所示:
标志名称 | 标志值 | 含义 |
ACC_PUBLIC | 0x0001 | 是否为public类型 |
ACC_FINAL | 0x0010 | 是否被声明为final,只有类可设置 |
ACC_SUPER | 0x0020 | 是否允许使用invokespecial字节码指令,JDK1.2之后编译出来的类的这个标志为真 |
ACC_INTERFACE | 0x0200 | 标识这是一个接口 |
ACC_ABSTRACT | 0x0400 | 是否为abstract类型,对于接口或抽象类来说,此标志值为真,其他类值为假 |
ACC_SYNTHETIC | 0x1000 | 标识这个类并非由用户代码产生的 |
ACC_ANNOTATION | 0x2000 | 标识这是一个注解 |
ACC_ENUM | 0x4000 | 标识这是一个枚举 |
access_flags中一共有32个标志位可以使用,当前只定义了其中的8个,没有使用到的标志位要求一律为0.
Class类文件的结构:类索引this_class、父类索引super_class与接口索引集合interfaces_count/interfaces
类索引this_class和父类索引super_class都是一个u2类型的数据。
接口索引集合interfaces是一组u2类型的数据的集合。
Class文件中由这3项数据来确定这个类的继承关系。类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名。由于JAVA语言不允许多重继承,所以父类索引只有1个,除了java.lang.Object之外,所有的java类都有父类,因此除了Object之外,所有JAVA类的父类索引都不为0(这就是为什么类索引与父类索引都是1个的原因)。
接口索引集合用来描述这个类实现了哪些接口,这些被实现的接口将按照implements语句(如果是接口本身则按照extends语句)后的接口顺序从左到右排列在接口的索引集合中(这就是为什么是interfaces_count/interfaces配合的原因)。
类索引、父类索引、接口索引集合都是按照顺序在访问标志access_flags之后,类索引和父类索引用2个u2类型的索引值表示,它们各自指向一个类型为CONSTANT_Class_info的类描述符常量,通过CONSTANT_Class_info类型的常量中的索引值可以找到定义在CONSTANT_Utf8_info类型的常量中的全限定名字符串。
对于接口索引集合,入口的第一项是u2类型的数据为接口计数器interfaces_count,表示索引表的容量。如果该类没有实现任何接口,那么该计数器值为0,后面接口的索引表不再占用任何字节。
Class类文件的结构:字段表集合fields_count/fields
字段表field_info类型用于描述接口或类中声明的变量。字段field包括了类级变量、实例级变量,但是不包括在方法内部声明的变量。
字段表field_info由2部分组成:自己存储的修饰符、存储在常量池constant_pool中的字段名称和字段类型。
字段表field_info结构如下:
类型 | 名称 | 数量 |
u2 | access_flags | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | attribute_count | 1 |
attribute_info | attributes | attributes |
access_flags是一个u2的数据类型,其可以设置的标志位和含义如下所示:
标志名称 | 标志值 | 含义 |
ACC_PUBLIC | 0x0001 | 字段是否为public |
ACC_PRIVATE | 0x0002 | 字段是否为private |
ACC_PROTECTED | 0x0004 | 字段是否为protected |
ACC_STATIC | 0x0008 | 字段是否为static |
ACC_FINAL | 0x0010 | 字段是否为final |
ACC_VOLATILE | 0x0040 | 字段是否为volatile |
ACC_TRANSIENT | 0x0080 | 字段是否为transient |
ACC_SYNTHETIC | 0x1000 | 字段是否由编译期自动产生 |
ACC_ENUM | 0x4000 | 字段是否为enum |
name_index和descriptor_index都是对常量池constant_pool的引用,分别代表着字段的简单名称、字段、方法的描述符。
全限定名:org/fenixsoft/clazz/TestClass这种带有全部路径的,就是该类的全限定名。仅仅是把类全名中的“.”替换成了“/”而已,为了使连续的多个全限定名之间不产生混淆,在使用时最后一般会加入一个“;”号来表示权限名结束。
简单名称:指没有数据类型、参数修饰的方法或字段名称。
描述符:用来描述字段的数据类型、方法的参数列表(包括数量、类型、顺序)和返回值。根据描述符规则,基本数据类型(byte、char、float、int、long、short、boolean)及代表无返回值的void类型都用一个大写字符来表示,而对象类型则用字符L + 对象全限定名来表示,详情如下:
标识字符 | 含义 |
B | 基本类型byte |
C | 基本类型char |
D | 基本类型double |
F | 基本类型float |
I | 基本类型int |
J | 基本类型long |
S | 基本类型short |
Z | 基本类型boolean |
V | 特殊类型void |
L | 对象类型,例如:Ljava/lang/Object |
对于数组类型,每一维度将使用一个前置的“[”字符来描述,例如java.lang.String[][]类型的二维数组,将被记录为:[[Ljava/lang/String;,一个整型数组int[]将被记录为:[I。
用描述符来描述方法时,按照先参数列表、后返回值的顺序来描述,参数列表按照参数的严格顺序在一组小括号()之内。如方法void inc()的描述符为()V,方法java.lang.String.toString()描述符为()Ljava/lang/String;,方法int indexOf(char[] source)描述符为([C)I。
attribute_count/attributes属性表集合用于存储一些额外的信息,字段都可以在属性表中描述0至多项额外的信息。
字段表集合fields_count/fields中不会列出从超类或父接口中继承而来的字段,但有可能列出原本JAVA代码中不存在的字段,例如在内部类中为了保持对外部类的访问性,会自动添加指向外部类实例的字段。另外JAVA语言中字段是无法重载的,对于2个字段的数据类型、修饰符不管是否相同,都必须使用不一样的名称,但对于字节码来讲,如果2个字段的描述符不一致,那字段重名就是合法的。
Class类文件的结构:方法表集合methods_count/methods
Class文件存储格式中对方法的描述与对字段的描述几乎采用了完全一致的方式,方法表的结构如同字段表一样,依次包括了:访问标志access_flags、名称索引name_index、描述符索引descriptor_index、属性表索引attributes。
方法表method_info结构如下:
类型 | 名称 | 数量 |
u2 | access_flags | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | attribute_count | 1 |
attribute_info | attributes | attributes |
access_flags是一个u2的数据类型,其可以设置的标志位和含义如下所示:
标志名称 | 标志值 | 含义 |
ACC_PUBLIC | 0x0001 | 方法是否为public |
ACC_PRIVATE | 0x0002 | 方法是否为private |
ACC_PROTECTED | 0x0004 | 方法是否为protected |
ACC_STATIC | 0x0008 | 方法是否为static |
ACC_FINAL | 0x0010 | 方法是否为final |
ACC_SYNCHRONIZED | 0x0020 | 方法是否为synchronized |
ACC_BRIDGE | 0x0040 | 方法是否是由编译器产生的桥接方法 |
ACC_VARARGS | 0x0080 | 方法是否接受不定参数 |
ACC_NATIVE | 0x0100 | 方法是否为native |
ACC_ABSTRACT | 0x0400 | 方法是否为abstract |
ACC_STRICT | 0x0800 | 方法是否为strictfp |
ACC_SYNTHETIC | 0x1000 | 方法是否是由编译器自动产生的 |
方法体内的JAVA代码,经过编译器编译成字节码指令之后,存放在方法属性表集合中一个名叫“Code”的属性里面。
如果父类方法在子类中没有被重写,方法表集合中就不会出现来自父类的方法信息。但同样的,有可能会出现由编译器自动添加的方法,最典型的便是类构造器方法<clinit>和实例构造器方法<init>。
在JAVA语言中,要重载overload一个方法,除了要与原方法具有相同的简单名称之外,还要求必须拥有一个与原方法不同的特征签名。
特征签名就是一个方法中各个参数在常量池中的字段符号引用的集合,也就是因为返回值不会包含在特征签名之中,因此JAVA语言里面是无法仅仅依靠返回值的不同来对一个已有方法进行重载的。但是在Class文件格式之中,特征签名的范围更大一些,只要描述符不是完全一致的2个方法也是可以共存的。也就是说,如果2个方法有相同的名称和特征签名,但返回值不同,那么也是可以合法共存于同一个Class文件中的。
Class类文件的结构:属性表集合attributes_count/attributes
属性表attribute_info类型,在Class文件、字段表、方法表中都可以携带自己的属性表集合,用于描述某些场景专有的信息。
与Class文件中其他的数据项目要求严格的顺序、长度、内容不同,属性表集合的限制稍微宽松些,不再要求各个属性表具有严格的顺序,并且只要不与已有的属性名重复,任何人实现的编译器都可以向属性表中写入自定义的属性信息,JAVA虚拟机运行时会忽略掉它不认识的属性。
为了能正确地解析Class文件,JAVA虚拟机规范预定义了9项虚拟机实现应当能识别的属性,详情如下:
属性名称 | 使用位置 | 含义 |
Code | 方法表 | JAVA代码编译成的字节码指令 |
ConstantValue | 字段表 | final关键字定义的常量值 |
Deprecated | 类、方法表、字段表 | 被声明为deprecated的方法和字段 |
Exceptions | 方法表 | 方法抛出的异常 |
InnerClasses | 类文件 | 内部类列表 |
LineNumberTable | Code属性 | JAVA源码的行号与字节码指令的对应关系 |
LocalVariableTable | Code属性 | 方法的局部变量描述 |
SourceFile | 类文件 | 源文件名称 |
Synthetic | 类、方法表、字段表 | 标识方法或字段为编译器自动生成的 |
对于每个属性,它的名称需要从常量池中引用一个CONSTANT_Utf8_info类型的常量来表示,而属性值的结构则是完全自定义的,只需要说明属性值所占用的位数长度即可。一个符合规则的属性表结构如下:
类型 | 名称 | 数量 |
u2 | attribute_name_index | 1 |
u2 | attribute_length | 1 |
u1 | info | attribute_length |
Code属性
JAVA程序方法体里面的代码经过javac编译器处理之后,最终变为字节码指令存储在Code属性内。Code属性出现在方法表的属性集合之中,但并非所有的方法表都必须存在这个属性,例如接口或者抽象类中的方法就不存在Code属性。
Code属性结构如下:
类型 | 名称 | 数量 |
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | max_stack | 1 |
u2 | max_locals | 1 |
u4 | code_length | 1 |
u1 | code | code_length |
u2 | exception_table_length | 1 |
exception_info | exception_table | exception_table_length |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
attribute_name_index是一项指向CONSTANT_Utf8_info型常量的索引,常量值固定为Code,它代表了该属性的属性名称。
attribute_length指示了属性值的长度,由于属性名称索引与属性长度一共是6个字节,所以,属性值得长度固定为整个属性表的长度减去6个字节。
max_stack代表了操作数栈深度的最大值。在方法执行的任意时刻,操作数栈都不会超过这个深度。虚拟机运行的时候需要根据这个值来分配栈帧中的操作栈深度。
max_locals代表了局部变量表所需的存储空间。在这里,max_locals的单位是Slot,Slot是虚拟机为局部变量分配内存所使用的最小单位。对于byte、char、float、int、short、boolean、reference、returnAddress等长度不超过32位的数据类型,每个局部变量占用1个Slot,而double和long这2种64位的数据类型则需要2个Slot来存放。方法参数(包括实例方法中隐藏参数this)、显式异常处理器的参数(即try-catch语句中catch块所定义的异常)、方法体中定义的局部变量都需要使用局部变量表来存放。另外,并不是在方法中用到了多少个局部变量,就把这些局部变量所占的Slot之和来作为max_locals的值,原因是局部变量表中的Slot可以重用,当代码执行超过一个局部变量的作用域时,这个局部变量所占的Slot就可以被其他局部变量所使用,编译器会根据变量的作用域来分类Slot并分配给各个变量使用,然后计算出max_locals的大小。
code_length和code用来存储JAVA源程序编译后生成的字节码指令。code_length代表字节码长度,code是用于存储字节码指令的一系列字节流。每一个字节码指令就是一个u1类型的单字节,当虚拟机读取到code中的一个字节码时,就可以相应地找出这个字节码代表的是什么指令,并且可以知道这条指令后面是否需要跟随参数,以及参数应当如何理解。u1数据类型的取值范围是0x00-0xFF,对应的十进制的0-255,也就是一共可以表达256条指令。目前JAVA虚拟机规范已经定义了其中大约200条编码值对应的指令含义。
虽然code_length是一个u4类型的长度值,理论上最大值可以达到2^32 - 1,但是虚拟机规范中限制了一个方法不允许超过65535条字节码指令,如果超过这个限制,javac编译器就会拒绝编译。一般来讲,普通的java代码只要不是刻意编写超长方法,就不会超过这个最大值的限制。但是在编译复杂JSP文件时,可能会因为这个原因而导致编译失败。
Exceptions属性
该属性是在方法表中与Code属性平级的一项属性。Exceptions属性作用就是列举出方法中可能抛出的受查异常,也就是方法描述时在throws关键字后面列举的异常。其结构如下:
类型 | 名称 | 数量 |
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | number_of_exception | 1 |
u2 | exception_index_table | number_of_exception |
number_of_exception表示方法可能抛出number_of_exception种受查异常,每一种受查异常使用一个exception_index_table项表示。
exception_index_table是一个指向常量池中CONSTANT_Class_info型常量的索引,代表了该受查异常的类型。
LineNumberTable属性
该属性用于描述JAVA源码行号与字节码行号(字节码的偏移量)之间的对应关系。该属性并不是运行时必需的属性,但默认会生成到Class文件之中,可以在javac -g:none或-g:lines选项来取消或要求生成这项信息。如果选择不生成LineNumberTable属性,对程序运行产生的最主要的影响就是:在抛出异常时,堆栈中将不会显示出错的行号,并且在调试程序的时候也无法按照源码来设置断点。
该属性结构如下:
类型 | 名称 | 数量 |
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | line_number_table_length | 1 |
line_number_info | line_number_table | line_number_table_length |
line_number_table是一个数量为line_number_table_length、类型为line_number_info的集合。
line_number_info表包括了start_pc、line_number这2个u2类型的数据项,start_pc是字节码行号,line_number是源码行号。
LocalVariableTable属性
该属性用于描述栈帧中局部变量表中的变量与JAVA源码中定义的变量之间的关系,该属性不是运行时必需的属性,默认也不会生成到Class文件中,可以在javac中使用-g:none或-g:vars选项来取消或要求生成这项信息。如果没有生成这项属性,最大的影响就是当其他人引用这个方法时,所有的参数名称都将丢失,IDE可能会使用诸如arg0、arg1之类的占位符来代替原有的参数名,虽然对程序运行没有影响,但是会给代码编写带来较大的不便,而且在调试期间调试器也无法根据参数名称从运行上下文中获得参数值。该属性结构如下所示:
类型 | 名称 | 数量 |
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | local_variable_table_length | 1 |
local_variable_info | local_variable_table | local_variable_table_length |
local_variable_info代表了一个栈帧与源码中的局部变量的关联。其结构如下所示:
类型 | 名称 | 数量 |
u2 | start_pc | 1 |
u2 | length | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | index | 1 |
start_pc和length属性分别代表了这个局部变量的生命周期开始的字节码偏移量以及其作用范围覆盖的长度,2者结合起来就是这个局部变量在字节码之中的作用域范围。
name_index和descriptor_index都是指向常量池中CONSTANT_Utf8_info型常量的索引,分别代表了局部变量的名称以及该局部变量的描述符。
index是这个局部变量在栈帧局部变量表中Slot的位置。当这个变量的数据类型是64位类型时(double和long),它占用的Slot为index和index+1这两个位置。
在JDK1.5引入泛型之后,LocalVariableTable属性增加了一个“姐妹属性”:LocalVariableTypeTable,这个新增的属性结构与LocalVariableTable非常类似,仅仅把记录的字段描述符的descriptor_index替换为字段的特征签名。对于非泛型类型来说,描述符和特征签名能描述的信息基本是一致的,但引入泛型之后,由于描述符中泛型的参数化类型被擦除掉,描述符就不能准确地描述泛型类型了,这才出现了LocalVariableTypeTable。
SourceFile属性
该属性用于记录生成这个Class文件的源码文件名称。这个属性也是可选的,可以使用javac的-g:none或者-g:source选项来关闭或要求生成这项信息。在JAVA中,对于大多数类来说,类名和文件名是一致的,但在一些特殊情况下(如内部类)例外。
如果不生成这项属性,那么当抛出异常时,堆栈中将不会显示出错误代码所属的文件名。这个属性是一个定长的属性,其结构如下所示:
类型 | 名称 | 数量 |
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | sourcefile_index | 1 |
sourcefile_index数据项是指向常量池中CONSTANT_Utf8_info型常量的索引,常量值就是源码文件的文件名。
ConstantValue属性
该属性作用是通知虚拟机自动为静态变量赋值。只有被static关键字修饰的变量/类变量才可以使用这项属性。虚拟机在对static变量赋值的方式、时间与其他变量不同:
①非static类型的变量(实例变量)赋值是在实例构造器<init>方法中进行的;
②static类型的变量,要么赋值操作在类构造器<clinit>方法中进行,要么使用ConstantValue属性来赋值。
目前Sun javac编译器的规则是:如果同时使用final、static来修饰一个变量,且这个变量的数据类型是基本类型或者java.lang.String类型的话,就生成ConstantValue属性来进行初始化,否则选择在<clinit>方法中进行初始化。
虽然有final关键字才更符合ConstantValue的语义,但虚拟机规范并没有强制要求字段必须设置了ACC_FINAL标志,只是要求了有ConstantValue属性时,字段必须设置成ACC_STATIC标志,对于final关键字要求是javac编译器自己加入的限制。
ConstantValue属性结构如下:
类型 | 名称 | 数量 |
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | constantvalue_index | 1 |
attribue_length数据项固定为2。
constantvalue_index数据项代表了常量池中一个字面量常量的引用,根据字段类型的不同,字面量可以是CONSTANT_Long_info/CONSTANT_Float_info/CONSTANT_Double_info/CONSTANT_Integer_info/CONSTANT_String_info常量中的某一种。
InnerClasses属性
该属性用于记录内部类与宿主类之间的关联。如果一个类中定义了内部类,那编译器将会为它及它所包含的内部类生成InnerClasses属性。该属性结构如下:
类型 | 名称 | 数量 |
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | number_of_classes | 1 |
inner_classes_info | inner_classes | number_of_classes |
number_of_classes代表了需要记录多少个内部类信息,每一个内部类的信息都由一个inner_classes_info表进行描述。
inner_classes_info表的结构如下所示:
类型 | 名称 | 数量 |
u2 | inner_class_info_index | 1 |
u2 | outer_class_info_index | 1 |
u2 | inner_name_index | 1 |
u2 | inner_class_access_flags | 1 |
inner_class_info_index和outer_class_info_index都是指向常量池中CONSTANT_Class_info型常量的索引,分别代表了内部类、宿主类的符号引用。
inner_name_index是指向常量池中CONSTANT_Utf8_info型常量的索引,代表这个内部类的名称,如果是匿名内部类,则这项值为0。
inner_class_access_flags是内部类的访问标志,类似于类的access_flags,取值如下所示:
标志名称 | 标志值 | 含义 |
ACC_PUBLIC | 0x0001 | 内部类是否为public |
ACC_PRIVATE | 0x0002 | 内部类是否为private |
ACC_PROTECTED | 0x0004 | 内部类是否为protected |
ACC_STATIC | 0x0008 | 内部类是否为static |
ACC_FINAL | 0x0010 | 内部类是否为final |
ACC_INTERFACE | 0x0020 | 内部类是否为synchronized |
ACC_ABSTRACT | 0x0400 | 内部类是否为abstract |
ACC_SYNTHETIC | 0x1000 | 内部类是否为并非由用户代码产生的 |
ACC_ANNOTATION | 0x2000 | 内部类是否为一个注解 |
ACC_ENUM | 0x4000 | 内部类是否为一个枚举 |
Deprecated以及Synthetic属性
Deprecated和Synthetic属性都属于标志类型的布尔属性,只存在有、没有的区别,没有属性值的概念。
Deprecated属性用于表示某个类、字段、方法,已经被程序作者定为不再推荐使用,可以通过在代码中使用@deprecated注解进行设置。
Synthetic属性代表此字段或方法并不是由JAVA源码直接产生,而是由编译器自行添加。在JDK1.5之后,用于标识一个类、字段、方法是编译器自动产生的,也可以设置它们访问标志中的ACC_SYNTHETIC标志位。所有由非用户代码产生的类、方法、字段都应当至少设置Synthetic属性和ACC_SYNTHETIC标志位中的一项,唯一例外的是实例构造器<init>方法和类构造器<clinit>方法。
Deprecated和Synthetic属性结构如下所示:
类型 | 名称 | 数量 |
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
其中attribute_length值必须为0x00000000,这是因为没有任何属性值需要设置。