JVM之Class文件结构
对于Java虚拟机来说,Class文件是虚拟机的一个重要接口。无论使用何种语言进行软件开发,只要能将源文件编译为正确的Class文件,那么这种语言就可以在Java虚拟机上执行。可以说,Class文件就是Java虚拟机的基石。
本文将详细介绍Class文件的组成,帮助大家更进一步了解虚拟机的运行过程。 本文涉及的主要知识点有:
- Class文件的基本结构。
- 使用jClassLib查看Class文件。
- 使用ASM手工生成Class文件。
不仅跨平台,还能跨语言:语言无关性
Java虚拟机提供了Java语言的跨平台功能。使用不同平台的Java虚拟机,可以让同一份Class文件运行在不同的平台上。
作为一个平台,Java虚拟机不仅提供了跨平台功能,甚至还提供了跨语言的特性。从理论上说,无论使用何种语言编写的软件,都可以在任意平台上执行。这看上去是一个何等诱人的特性啊!而实现这一切的基础,就是统一而强大的Class文件,它是异构语言和Java虚拟机之间的重要桥梁。
随着Java虚拟机的不断发展,在这个方面,依靠强大的社区力量,大有赶超.NET平台的趋势。当下,诸如Clojure(Lisp语言的一种方言)、Groovy、Scala、Jython(Python)等语言都活跃在Java虚拟机平台上。各种语言都可以由源代码被编译成Class文件,并最终得以在Java虚拟机上执行。
虚拟机的基石:Class文件
Java文件经过编译后生产Class字节码文件。JVM时通过字节码来执行。熟悉Class文件的数据结构很重要。随着虚拟机的发展,不可避免会对Class文件进行调整,但是其基本结构和框架是非常稳定的。
Class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在Class文件之中,中间没有添加任何分隔符,这使得整个Class文件中存储的内容几乎全部是程序运行的必要数据,没有空隙存在。当遇到需要占用8位字节以上空间的数据项时,则会按照高位在前的方式分割成若干个8位字节进行存储。
根据Java虚拟机规范的规定,Class文件格式采用一种类似于C语言结构体的伪结构来存储数据,这种伪结构中只有两种数据类型:无符号数和表,后面的解析都要以这两种数据类型为基础,所以这里要先介绍这两个概念。
- 无符号数属于基本的数据类型,以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值。
- 表是由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都习惯性地以“_info”结尾。表用于描述有层次关系的复合结构的数据,整个Class文件本质上就是一张表,它由下面的数据项构成。
无论是无符号数还是表,当需要描述同一类型但数量不定的多个数据时,经常会使用一个前置的容量计数器加若干个连续的数据项的形式,这时称这一系列连续的某一类型的数据为某一类型的集合。
Class文件结构定义
ClassFile
{
u4 magic;
u2 minor_version;
u2 major_version;
u2 constant_pool_count;
cp_info constant_pool[constant_pool_count-1];
u2 access_flags;
u2 this_class;
u2 super_class;
u2 interfaces_count;
u2 interfaces[interfaces_count];
u2 fields_count;
field_info fields[fields_count];
u2 methods_count;
method_info methods[methods_count];
u2 attributes_count;
attribute_info attributes[attributes_count];
}
Class 文件格式
| 类型 | 名称 | 数量 | |:—————|———————|————————-| | u4 | magic | 1 | | u2 | minor_version | 1 | | u2 | major_version | 1 | | u2 | constant_pool_count | 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文件的结构严格按照该结构体的定义:
- (1)文件以一个4字节的Magic(被称为魔数)开头,紧跟着大、小版本号。
- (2)在版本号之后是常量池,常量池的个数为constant_pool_count,常量池中的表项有constant_pool_count-1项。
- (3)常量池之后是类的访问修饰符、代表自身类的引用、父类引用及接口数量和实现的接口引用。
- (4)在接口之后,有字段的数量和字段描述、方法数量及方法的描述。
- (5)存放类文件的属性信息。 Class由于它没有任何分隔符号,所以在表中的数据项,无论是顺序还是数量,甚至于数据存储的字节序(Byte Ordering,Class文件中字节序为Big-Endian)这样的细节,都是被严格限定的,哪个字节代表什么含义,长度是多少,先后顺序如何,都不允许改变。接下来我们将一起看看这个表中各个数据项的具体含义。
Class文件的标志:魔数(MagicNumber)
魔数(Magic Number)作为Class文件的标志,用来告诉Java虚拟机,这是一个Class文件。魔数是一个4字节的无符号整数,它固定为0xCAFEBABE。众所周知,Java的名字和咖啡有着不解之缘。当你看到CAFEBABE时,会想到什么?像不像Café Baby(咖啡宝贝)呢?Java的开发者用这个整数来表示Class文件。 每个Class文件的头4个字节称为魔数(Magic Number),它的唯一作用是确定这个文件是否为一个能被虚拟机接受的Class文件。 如果一个Class文件不以0xCAFEBABE开头,虚拟机在进行文件校验的时候就会直接抛出以下错误:
注意: 魔数是Class文件的标识符,在程序开发时,软件设计者喜欢使用一些特殊的数字来表示固定的文件类型或者特殊的含义。除了Java的Class文件,常用的TAR文件、PE文件,甚至网络DHCP报文内部,都有类似的设计手法。
Class文件的版本:版本号(MinorVersion&MajorVersion)
在魔数后面,紧跟着Class的小版本号和大版本号。这表示当前Class文件是由哪个版本的编译器编译产生的。首先出现的是小版本号,是一个2字节的无符号整数,在此之后为大版本号,也用2字节表示。
魔数后面的4个字节存储的是 Class 文件的版本号,分为两类:
- 次版本号(Minor Version):第5、6个字节
- 主版本号(Major Version):第7、8个字节
JDK1.0 主版本号为45, 1.1 为46, 依次类推,到JDK8的版本号为52, 16进制为0x33.
一个 JVM实例只能支持特定范围内的主版本号 (Mi 至Mj) 和 0 至特定范围内 (0 至 m) 的副版 本号。假设一个 Class 文件的格式版本号为 V, 仅当Mi.0 ≤ v ≤ Mj.m成立时,这个 Class 文件 才可以被此 Java 虚拟机支持。不同版本的 Java 虚拟机实现支持的版本号也不同,高版本号的 Java 虚拟机实现可以支持低版本号的 Class 文件,反之则不成立。 JVM在加载class文件的时候,会读取出主版本号,然后比较这个class文件的主版本号和JVM本身的版 本号,如果JVM本身的版本号 < class文件的版本号,JVM会认为加载不了这个class文件,会抛出我 们经常见到的” java.lang.UnsupportedClassVersionError: Bad version number in .class file “ Error 错误;反之,JVM会认为可以加载此class文件,继续加载此class文件。
存放所有常数:常量池(ConstantPool)
常量池是Class文件中内容最丰富的区域之一。随着Java虚拟机的不断发展,常量池的内容也日渐丰富。同时,常量池对于Class文件中的字段和方法解析也有至关重要的作用,可以说,常量池是整个Class文件的基石。在版本号之后,紧跟着的是常量池的数量,以及若干个常量池表项。
主版本号之后是常量池入口,常量池可以理解为 Class 文件之中的资源仓库,它是 Class 文件结构中与其他项目关联最多的数据类型,也是占用 Class 文件空间最大的数据项目之一,同是它还是 Class 文件中第一个出现的表类型数据项目。
因为常量池中常量的数量是不固定的,所以在常量池入口需要放置一个 u2 类型的数据来表示常量池的容量「constant_pool_count」,和计算机科学中计数的方法不一样,这个容量是从 1 开始而不是从 0 开始计数
。之所以将第 0 项常量空出来是为了满足后面某些指向常量池的索引值的数据在特定情况下需要表达「不引用任何一个常量池项目」的含义,这种情况可以把索引值置为 0 来表示。具体用来干什么,如果你仔细观察Object类的Class文件,你会发现Object这个顶级类的父类索引指向的是这个0的槽位。
Class 文件结构中只有常量池的容量计数是从 1 开始的,其它集合类型,包括接口索引集合、字段表集合、方法表集合等容量计数都是从 0 开始。
常量池中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References)。
- 字面量比较接近 Java 语言层面的常量概念,如字符串、声明为 final 的常量值等。
- 符号引用属于编译原理方面的概念,包括了以下三类常量:
- 类和接口的全限定名(Fully Qualified Name)
- 字段的名称和描述符(Descriptor)
- 方法的名称和描述符
经过javac编译后的Class文件不会保存方法、字段最终在内存中的布局信息,而是保存其具体地址的符号引用。当虚拟机做类加载时,将会从常量池获得对应的符号引用,再在类创建时或运行时解析翻译到具体的内存地址中。
引用替换的时机
符号引用替换为直接引用的操作发生在类加载过程(加载 -> 连接(验证、准备、解析) -> 初始化)中的解析阶段,会将符号引用转换(替换)为对应的直接引用,放入运行时常量池中。
常量池中每一项常量都是一个表,在JDK 1.7之前共有11种结构各不相同的表结构数据,在JDK 1.7中为了更好地支持动态语言调用,又额外增加了4种(CONSTANT_MethodHandle_info、CONSTANT_MethodType_info、CONSTANT_Dynamic_info和CONSTANT_InvokeDynamic_info)。后来为了支持 Java 模块化,又加入了 2 个常量,所以截止 JDK13,常量表中有 17 种不同类型的常量。这种表都有一个共同的特点,就是表开始的第一位是一个u1类型的标志位(tag,取值标志列),代表当前这个常量属于哪种常量类型。
截至 JDK 13,常量表中分别有17种不同数据类型的常量如下表。
常量池元素名称 | tag标识 | 含义 |
---|---|---|
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 | 字段和方法的名称以及类型的符号引用 |
CONSTANT_MethodHandler_info | 15 | 表示方法句柄 |
CONSTANT_MethodType_info | 16 | 标识方法类型 |
CONSTANT_Dynamic_info | 17 | 表示一个动态计算常量 |
CONSTANT_InvokeDynamic_info | 18 | 表示一个动态方法调用点 |
CONSTANT_Module_info | 19 | 表示一个模块 |
CONSTANT_Package_info | 20 | 表示一个模块中开放或者导出的包 |
常量项结构表
`CONSTANT_Class_info` {
u1 tag; // 值为1
u2 name_index; // 指向权限定名常量项的索引
}
`CONSTANT_Fieldref_info` {
u1 tag; // 值为9
u2 class_index; // 指向声明字段的类或接口描述符CONSTANT_Class_info的索引项
u2 name_and_type_index; // 指向字段描述符CONSTANT_NameAndType的索引项
}
`CONSTANT_Methodref_info` {
u1 tag; // 值为10
u2 class_index; // 指向声明方法的类描述符CONSTANT_Class_info的索引项
u2 name_and_type_index; // 指向名称及类型描述符CONSTANT_NameAndType的索引项
}
`CONSTANT_InterfaceMethodref_info` {
u1 tag; // 值为11
u2 class_index; // 指向声明方法的接口描述符CONSTANT_Class_info的索引项
u2 name_and_type_index; // 指向名称及类型描述符CONSTANT_NameAndType的索引项
}
`CONSTANT_String_info` {
u1 tag; // 值为8
u2 string_index; // 指向字符串字面量的索引
}
`CONSTANT_Integer_info` {
u1 tag; // 值为3
u4 bytes; // 按照高位在前存储的int值
}
`CONSTANT_Float_info` {
u1 tag; // 值为4
u4 bytes; // 按照高位在前存储的float值
}
`CONSTANT_Long_info` {
u1 tag; // 值为5
u4 high_bytes; // 按照高位在前存储的long值
u4 low_bytes; //
}
`CONSTANT_Double_info` {
u1 tag; // 值为6
u4 high_bytes; // 按照高位在前存储的double值
u4 low_bytes; //
}
`CONSTANT_NameAndType_info` {
u1 tag; // 值为12
u2 name_index; // 指向该字段或方法名称常量项的索引
u2 descriptor_index; // 指向该字段或方法描述符常量项的索引
}
`CONSTANT_Utf8_info` {
u1 tag; // 值为1
u2 length; // UTF8编码的字符串占用的字节数
u1 bytes[length]; // 长度为length的UTF8编码的字符串
}
`CONSTANT_MethodHandle_info` {
u1 tag; // 值为15
u1 reference_kind; // 值必须在[1~9]之间,它决定了方法句柄的类型。
//方法句柄类型的值表示方法句柄的字节码行为
u2 reference_index; // 值必须是对常量池的有效索引
}
`CONSTANT_MethodType_info` {
u1 tag; // 值为16
u2 descriptor_index; // 值必须是对常量池的有效索引,
//常量池在该索引处的项必须是CONSTANT_Utf8_info结构,表示方法的描述符
}
`CONSTANT_InvokeDynamic_info` {
u1 tag; // 值为18
u2 bootstrap_method_attr_index; // 值必须是对当前Class文件中引导方法表的
//bootstrap_methods[]数组的有效索引
u2 name_and_type_index; // 值必须是对当前常量池的有效索引,常量池在该索引处的项必须
// 是CONSTANT_NameAndType_info结构,表示方法名和方法描述符
}
`CONSTANT_Module_info` {
u1 tag; //
u2 name_index; //
}
`CONSTANT_Package_info` {
u1 tag; //
u2 name_index; //
}
17种数据类型的常量结构
常量池中的17种数据类型的结构如下:
1)CONSTANT_Utf8_info
UTF-8 编码的字符串。
结构 | 类型 | 描述 |
---|---|---|
tag | u1 | 值为1 |
length | u2 | UTF-8 编码的字符串占用的字节数 |
bytes | u1 | 长度为 length 的 UTF-8 编码的字符串 |
2)CONSTANT_Integer_info
整型字面量。
结构 | 类型 | 描述 |
---|---|---|
tag | u1 | 值为3 |
bytes | u4 | 按照高位在前存储的 int 值 |
3)CONSTANT_Float_info
浮点型字面量。
结构 | 类型 | 描述 |
---|---|---|
tag | u1 | 值为4 |
bytes | u4 | 按照高位在前存储的 float 值 |
4)CONSTANT_Long_info
长整型字面量。
结构 | 类型 | 描述 |
---|---|---|
tag | u1 | 值为5 |
bytes | u8 | 按照高位在前存储的 long 值 |
5)CONSTANT_Double_info
双精度浮点型字面量。
结构 | 类型 | 描述 |
---|---|---|
tag | u1 | 值为6 |
bytes | u8 | 按照高位在前存储的 double 值 |
6)CONSTANT_Class_info
类或接口的符号引用。
结构 | 类型 | 描述 |
---|---|---|
tag | u1 | 值为7 |
index | u2 | 指向全限定名常量项的索引 |
7)CONSTANT_String_info
字符串类型字面量。
结构 | 类型 | 描述 |
---|---|---|
tag | u1 | 值为8 |
index | u2 | 指向字符串字面量的索引 |
8)CONSTANT_Fieldref_info
字段的符号引用。
结构 | 类型 | 描述 |
---|---|---|
tag | u1 | 值为9 |
index | u2 | 指向声明字段的类或者接口描述符 CONSTANT_Class_info 的索引项 |
index | u2 | 指向字段描述符 CONSTANT_NameAndType 的索引项 |
9)CONSTANT_Methodref_info
类中方法的符号引用。
结构 | 类型 | 描述 |
---|---|---|
tag | u1 | 值为10 |
index | u2 | 指向声明方法的类描述符 CONSTANT_Class_info 的索引项 |
index | u2 | 指向名称及类型描述符 CONSTANT_NameAndType 的索引项 |
10)CONSTANT_InterfaceMethodref_info
接口中方法的符号引用。
结构 | 类型 | 描述 |
---|---|---|
tag | u1 | 值为11 |
index | u2 | 指向声明方法的接口描述符 CONSTANT_Class_info 的索引项 |
index | u2 | 指向名称及类型描述符 CONSTANT_NameAndType 的索引项 |
11)CONSTANT_NameAndType_info
字段或方法的部分符号引用。
结构 | 类型 | 描述 |
---|---|---|
tag | u1 | 值为12 |
index | u2 | 指向该字段或方法名称常量项的索引 |
index | u2 | 指向该字段或方法描述符常量项的索引 |
12)CONSTANT_MethodHandle_info
表示方法句柄。
结构 | 类型 | 描述 |
---|---|---|
tag | u1 | 值为15 |
reference_kind | u1 | 值必须在1至9之间(包括1和9),它决定方法句柄的类型。方法句柄类型的值表示方法句柄的字节码行为 |
reference_index | u2 | 值必须是对常量池的有效索引 |
13)CONSTANT_MethodType_info
表示方法类型。
结构 | 类型 | 描述 |
---|---|---|
tag | u1 | 值为16 |
descriptor_index | u2 | 值必须是对常量池的有效索引,常量池在该索引处的项必须是 CONSTANT_Utf8_info 结构,表示方法的描述符 |
14)CONSTANT_Dynamic_info
表示一个动态计算常量。
结构 | 类型 | 描述 |
---|---|---|
tag | u1 | 值为17 |
bootstrap_method_attr_index | u2 | 值必须是对当前 Class 文件中引导方法表的 bootstrap_methods[] 数组的有效索引 |
name_and_type_index | u2 | 值必须是对当前常量池的有效索引,常量池在该索引处的项必须是 CONSTANT_NameAdnType_info 结构,表示方法名和方法描述符 |
15)CONSTANT_InvokeDynamic_info
表示一个动态方法调用点。
结构 | 类型 | 描述 |
---|---|---|
tag | u1 | 值为18 |
bootstrap_method_attr_index | u2 | 值必须是对当前 Class 文件中引导方法表的 bootstrap_methods[] 数组的有效索引 |
name_and_type_index | u2 | 值必须是对当前常量池的有效索引,常量池在该索引处的项必须是 CONSTANT_NameAdnType_info 结构,表示方法名和方法描述符 |
16)CONSTANT_Module_info
表示一个模块。
结构 | 类型 | 描述 |
---|---|---|
tag | u1 | 值为19 |
name_index | u2 | 值必须是对当前常量池的有效索引,常量池在该索引处的项必须是 CONSTANT_Utf8_info 结构,表示模块名字 |
17)CONSTANT_Package_info
表示一个模块中开放或者导出的包。
结构 | 类型 | 描述 |
---|---|---|
tag | u1 | 值为20 |
name_index | u2 | 值必须是对当前常量池的有效索引,常量池在该索引处的项必须是 CONSTANT_Utf8_info 结构,表示包名称 |
Class文件结构实战
Test类源码解读Class文件结构
public class Test {
private int m;
public int inc(){
return m + 1;
}
}
查看二进制信息
对Test.java使用javac编译后,使用vim查看Test.class文件,此时显示文件为乱码信息,输入:%!xxd
即可显示二进制信息
# 魔数 次版本号 主版本号 1 2
0000000: cafe babe 0000 0034 0013 0a00 0400 0f09 .......4........
# 3 4 5
0000010: 0003 0010 0700 1107 0012 0100 016d 0100 .............m..
0000020: 0149 0100 063c 696e 6974 3e01 0003 2829 .I...<init>...()
0000030: 5601 0004 436f 6465 0100 0f4c 696e 654e V...Code...LineN
0000040: 756d 6265 7254 6162 6c65 0100 0369 6e63 umberTable...inc
0000050: 0100 0328 2949 0100 0a53 6f75 7263 6546 ...()I...SourceF
0000060: 696c 6501 000e 5465 7374 436c 6173 732e ile...TestClass.
0000070: 6a61 7661 0c00 0700 080c 0005 0006 0100 java............
# 18 length 后面的字符到0x21之前表示java/lang/Object
0000080: 0954 6573 7443 6c61 7373 0100 106a 6176 .TestClass...jav
# 访问标志
0000090: 612f 6c61 6e67 2f4f 626a 6563 7400 2100 a/lang/Object.!.
00000a0: 0300 0400 0000 0100 0200 0500 0600 0000 ................
00000b0: 0200 0100 0700 0800 0100 0900 0000 1d00 ................
00000c0: 0100 0100 0000 052a b700 01b1 0000 0001 .......*........
00000d0: 000a 0000 0006 0001 0000 0001 0001 000b ................
00000e0: 000c 0001 0009 0000 001f 0002 0001 0000 ................
00000f0: 0007 2ab4 0002 0460 ac00 0000 0100 0a00 ..*....`........
0000100: 0000 0600 0100 0000 0600 0100 0d00 0000 ................
0000110: 0200 0e0a
根据以上信息可以获得主版本号信息 0x0034 的十进制52,52对应的JDK版本是JDK8,可以向下兼容45-51的JDK版本。JDK版本是从45开始的,JDK1.0-1.1 使用了45.0-45.3的版本号。
次版本号在JDK12之前都没有使用过,全为0。
常量池容量是十六进制0x0013,十进制为19,因此常量池中有18项常量,索引范围在1-18。
常量池容量大小0x0013之后有18个常量,每个常量所占占用的字节大小都不相同,以第一个常量 0x0a(十进制为10)为例,查常量池项目类型表可知(常量类型表的结构一般为 tag(u1) 和 index(u2)),10为类中方法的符号引用CONSTANT_Methodref_info,该引用 有一个tag (u1类型占用一个字节 = 0x0a = 10)、两个index(u2类型占用两个字节 0x0004和0x000f), 第一个index = 4
指向声明方法描述符CONSTANT_Class_info的索引项,第二个index = 15
指向名称及类型描述符CONSTANT_NameAndType的索引项。根据第一个index索引值0x0004 代表第四个常量,第四个常量的 tag 是0x07为CONSTANT_Class_info,结构为tag和index(指向全限定名常量项的索引),则tag = 7(0x07),index = 18(0x0012)索引为18表示为最后一个常量,查常量池知,第18个常量为 0x01是CONSTANT_Utf8_info常量,其长度为0x0010 = 16个字节 ,由上面的Constant pool
知,这16个字节表示 java/lang/Object。依次类推即可查询所有的常量信息
查看字节码信息javap
javap是jdk自带的反解析工具。它的作用就是根据class字节码文件,反解析出当前类对应的code区 (汇编指令)、本地变量表、异常表和代码行偏移量映射表、常量池等等信息。
javap的用法格式:javap
options如下:
-help --help -? 输出此用法消息
-version 版本信息,其实是当前javap所在jdk的版本信息,不是class在哪个jdk下生成的。
-v -verbose 输出附加信息(包括行号、本地变量表,反汇编等详细信息)
-l 输出行号和本地变量表
-public 仅显示公共类和成员
-protected 显示受保护的/公共类和成员
-package 显示程序包/受保护的/公共类 和成员 (默认)
-p -private 显示所有类和成员
-c 对代码进行反汇编
-s 输出内部类型签名
-sysinfo 显示正在处理的类的系统信息 (路径, 大小, 日期, MD5 散
列)
-constants 显示静态最终常量
-classpath <path> 指定查找用户类文件的位置
-bootclasspath <path> 覆盖引导类文件的位置
一般常用的是 -v -l -c三个选项。
javap -v classxx,不仅会输出行号、本地变量表信息、反编译汇编代码,还会输出当前类用 到的常量池等信息。
javap -l 会输出行号和本地变量表信息。
javap -c 会对当前class字节码进行反编译生成汇编代码。
使用javap -verbose工具可以参看当前Class文件的字节码信息
Classfile /F:/Test.class
Last modified 2020-8-24; size 265 bytes
MD5 checksum 0d5efc4b65ae7eb6d64f84136ce58ff9
Compiled from "Test.java"
public class Test
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #4.#15 // java/lang/Object."<init>":()V
#2 = Fieldref #3.#16 // Test.m:I
#3 = Class #17 // Test
#4 = Class #18 // java/lang/Object
#5 = Utf8 m
#6 = Utf8 I
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 inc
#12 = Utf8 ()I
#13 = Utf8 SourceFile
#14 = Utf8 Test.java
#15 = NameAndType #7:#8 // "<init>":()V
#16 = NameAndType #5:#6 // m:I
#17 = Utf8 Test
#18 = Utf8 java/lang/Object
{
public Test();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 1: 0
public int inc();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: getfield #2 // Field m:I
4: iconst_1
5: iadd
6: ireturn
LineNumberTable:
line 6: 0
}
SourceFile: "Test.java"
常量池实战
int 和 float在class 文件的存储结构
int类型和float 类型的数据类型占用 4 个字节的空间。
public class Constant {
private final int a = 10;
private final int b = 10;
private float c = 11f;
private float d = 11f;
private float e = 11f;
}
用javap -v Constant 或字节码工具查看,如下图。常量池里有只有一个float和int常量
Classfile /F:/Constant.class
Last modified 2022-4-9; size 400 bytes
MD5 checksum 508b4808a5a820bd54923cb8a4d0796e
Compiled from "Constant.java"
public class Constant
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #9.#25 // java/lang/Object."<init>":()V
#2 = Fieldref #8.#26 // Constant.a:I
#3 = Fieldref #8.#27 // Constant.b:I
#4 = Float 11.0f
#5 = Fieldref #8.#28 // Constant.c:F
#6 = Fieldref #8.#29 // Constant.d:F
#7 = Fieldref #8.#30 // Constant.e:F
#8 = Class #31 // Constant
#9 = Class #32 // java/lang/Object
#10 = Utf8 a
#11 = Utf8 I
#12 = Utf8 ConstantValue
#13 = Integer 10
#14 = Utf8 b
#15 = Utf8 c
#16 = Utf8 F
#17 = Utf8 d
#18 = Utf8 e
#19 = Utf8 <init>
#20 = Utf8 ()V
#21 = Utf8 Code
#22 = Utf8 LineNumberTable
#23 = Utf8 SourceFile
#24 = Utf8 Constant.java
#25 = NameAndType #19:#20 // "<init>":()V
#26 = NameAndType #10:#11 // a:I
#27 = NameAndType #14:#11 // b:I
#28 = NameAndType #15:#16 // c:F
#29 = NameAndType #17:#16 // d:F
#30 = NameAndType #18:#16 // e:F
#31 = Utf8 Constant
#32 = Utf8 java/lang/Object
{
public Constant();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: bipush 10
7: putfield #2 // Field a:I
10: aload_0
11: bipush 10
13: putfield #3 // Field b:I
16: aload_0
17: ldc #4 // float 11.0f
19: putfield #5 // Field c:F
22: aload_0
23: ldc #4 // float 11.0f
25: putfield #6 // Field d:F
28: aload_0
29: ldc #4 // float 11.0f
31: putfield #7 // Field e:F
34: return
LineNumberTable:
line 1: 0
line 2: 4
line 3: 10
line 4: 16
line 5: 22
line 6: 28
}
SourceFile: "Constant.java"
代码中所有用到 int 类型 10 的地方,会使用指向常量池的指针值#13个常量池项 (cp_info),即值为 10的结构体CONSTANT_Integer_info,而用到float类型的11f时,也会指向常量池的指针值#4来定位到第#4个常量池项(cp_info) 即值为11f的结构体CONSTANT_Float_info。
long和 double数据类型的常量在常量池中是怎样表示和存储 的?
Java语言规范规定了 long 类型和 double类型的数据类型占用8 个字节的空间。那么存在于class 字节码文件中的该类型的常量是如何存储的呢?
public class Constant {
private long k = -6076574518398440533L;
private long m = -6076574518398440533L;
private long n = -6076574518398440533L;
private double o = 10.1234567890D;
private double p = 10.1234567890D;
private double q = 10.1234567890D;
}
用字节码工具查看,发现#2,#3表示一个Long类型的常量, #7,#8表示一个Double类型的常量。
Classfile /F:/Constant.class
Last modified 2022-4-9; size 414 bytes
MD5 checksum bbb6a887cae025ee13f6e15e56a0234b
Compiled from "Constant.java"
public class Constant
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #13.#28 // java/lang/Object."<init>":()V
#2 = Long -6076574518398440533l
#4 = Fieldref #12.#29 // Constant.k:J
#5 = Fieldref #12.#30 // Constant.m:J
#6 = Fieldref #12.#31 // Constant.n:J
#7 = Double 10.123456789d
#9 = Fieldref #12.#32 // Constant.o:D
#10 = Fieldref #12.#33 // Constant.p:D
#11 = Fieldref #12.#34 // Constant.q:D
#12 = Class #35 // Constant
#13 = Class #36 // java/lang/Object
#14 = Utf8 k
#15 = Utf8 J
#16 = Utf8 m
#17 = Utf8 n
#18 = Utf8 o
#19 = Utf8 D
#20 = Utf8 p
#21 = Utf8 q
#22 = Utf8 <init>
#23 = Utf8 ()V
#24 = Utf8 Code
#25 = Utf8 LineNumberTable
#26 = Utf8 SourceFile
#27 = Utf8 Constant.java
#28 = NameAndType #22:#23 // "<init>":()V
#29 = NameAndType #14:#15 // k:J
#30 = NameAndType #16:#15 // m:J
#31 = NameAndType #17:#15 // n:J
#32 = NameAndType #18:#19 // o:D
#33 = NameAndType #20:#19 // p:D
#34 = NameAndType #21:#19 // q:D
#35 = Utf8 Constant
#36 = Utf8 java/lang/Object
{
public Constant();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=3, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: ldc2_w #2 // long -6076574518398440533l
8: putfield #4 // Field k:J
11: aload_0
12: ldc2_w #2 // long -6076574518398440533l
15: putfield #5 // Field m:J
18: aload_0
19: ldc2_w #2 // long -6076574518398440533l
22: putfield #6 // Field n:J
25: aload_0
26: ldc2_w #7 // double 10.123456789d
29: putfield #9 // Field o:D
32: aload_0
33: ldc2_w #7 // double 10.123456789d
36: putfield #10 // Field p:D
39: aload_0
40: ldc2_w #7 // double 10.123456789d
43: putfield #11 // Field q:D
46: return
LineNumberTable:
line 1: 0
line 2: 4
line 3: 11
line 4: 18
line 5: 25
line 6: 32
line 7: 39
}
SourceFile: "Constant.java"
String类型的字符串常量在常量池中是怎样表示和存储的?
对于字符串而言,JVM会将字符串类型的字面量以UTF-8 编码格式存储到在class字节码文件中。这么 说可能有点摸不着北,我们先从直观的Java源码中中出现的用双引号”” 括起来的字符串来看,在编译器编译的时候,都会将这些字符串转换成CONSTANT_String_info结构体,然后放置于常量池中。
源码如下
public class Constant {
private String s="Java JVM";
}
CONSTANT_String_info 指向了地址#15,而#15是CONSTANT_Utf8_info存储的真正的字符串常量
Classfile /F:/Constant.class
Last modified 2022-4-9; size 257 bytes
MD5 checksum ea6aee7f91756d274022da413e1991d4
Compiled from "Constant.java"
public class Constant
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #5.#14 // java/lang/Object."<init>":()V
#2 = String #15 // Java JVM
#3 = Fieldref #4.#16 // Constant.s:Ljava/lang/String;
#4 = Class #17 // Constant
#5 = Class #18 // java/lang/Object
#6 = Utf8 s
#7 = Utf8 Ljava/lang/String;
#8 = Utf8 <init>
#9 = Utf8 ()V
#10 = Utf8 Code
#11 = Utf8 LineNumberTable
#12 = Utf8 SourceFile
#13 = Utf8 Constant.java
#14 = NameAndType #8:#9 // "<init>":()V
#15 = Utf8 Java JVM
#16 = NameAndType #6:#7 // s:Ljava/lang/String;
#17 = Utf8 Constant
#18 = Utf8 java/lang/Object
{
public Constant();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: ldc #2 // String Java JVM
7: putfield #3 // Field s:Ljava/lang/String;
10: return
LineNumberTable:
line 1: 0
line 2: 4
}
SourceFile: "Constant.java"
类文件中定义的类名和类中使用到的类在常量池中是怎样被组织和存储的?
JVM会将某个Java 类中所有使用到了的类的完全限定名以二进制形式的完全限定名封装成 CONSTANT_Class_info结构体中,然后将其放置到常量池里。CONSTANT_Class_info 的tag值为 7。
Note :类的完全限定名和二进制形式的完全限定名在某个Java源码中,我们会使用很多个类,比如我们定义了一个 TestClass的类,并把它放到 test包下,则 TestClass类的完全限定名为test.ClassTest,将JVM编译 器将类编译成class文件后,此完全限定名在class文件中,是以二进制形式的完全限定名存储的,即它会把完全限定符的”.”换成”/” ,即在class文件中存储的 TestClass类的完全限定名称是”test/ClassTest”。因为这种形式的完全限定名是放在了class二进制形式的字节码文件中,所以就称之为二进制形式的完全限定名。
package Test;
import java.util.Date;
public class TestClass {
private Date date = new Date();
}
javap -v Test.TestClass, 可以发现常量池里有3个CONSTANT_Class_info结构体,一个是Test/TestClass, 一个是java/lang/Object,还有一个是java/util/Date. 他们分别指向了地址#21,#22和#19的CONSTANT_Utf8_info字符串。
为什么有3个类呢?首先Test/TestClass是当前类,在常量池出现毋庸置疑。JVM规定所有类都是Object的子类,所以JVM在编译后都会把java/lang/Object加上。至于java/util/Date是因为程序里引进了此类,并且使用此类创建了对象,所以会出现在常量池。
注意点: 对于某个类而言,其class文件中至少要有两个CONSTANT_Class_info常量池项,用来表示自己的类 信息和其父类信息。(除了java.lang.Object类除外,其他的任何类都会默认继承自 java.lang.Object)如果类声明实现了某些接口,那么接口的信息也会生成对应的 CONSTANT_Class_info常量池项。除此之外,如果在类中使用到了其他的类,只有真正使用到了相应的类,JDK编译器才会将类的信息组 成CONSTANT_Class_info常量池项放置到常量池中。
如果把代码修改为
import java.util.Date;
public class TestClass {
private Date date;
}
javap 后java/util/Date类就不见了。因为Date类只是申明了变量,没有真正实例化和使用,将类信息放置到常量池中的目的,是为了在后续的代码中有可能会反复用到它。很显然,JDK在编译TestClass类的时候,解析到Date类有没有用到,发现该类在代码中就没有用到过,所以就认为没有必要将它的信息放置到常量 池中了。
总结:
1. 对于某个类或接口而言,其自身、父类和继承或实现的接口的信息会被直接组装成 CONSTANT_Class_info常量池项放置到常量池中;
2. 类中或接口中使用到了其他的类,只有在类中实际使用到了该类时,该类的信息才会在常量池中有 对应的CONSTANT_Class_info常量池项;
3. 类中或接口中仅仅定义某种类型的变量,JDK只会将变量的类型描述信息以UTF-8字符串组成 CONSTANT_Utf8_info常量池项放置到常量池中,上面在类中的private Date date;JDK编译器 只会将表示date的数据类型的“Ljava/util/Date”字符串放置到常量池中。
哪些字面量会进入常量池中?
-
final类型的8种基本类型的值会进入常量池。
-
非final类型(包括static的)的8种基本类型的值,只有double、float、long的值会进入常量池。
-
常量池中包含的字符串类型字面量(双引号引起来的字符串值)。
public class TestConstant {
private int int_num = 110;
private char char_num = 'a';
private short short_num = 120;
private float float_num = 130.0f;
private double double_num = 140.0;
private byte byte_num = 111;
private long long_num = 3333L;
private long long_delay_num;
private boolean boolean_flage = true;
public void init() {
this.long_delay_num = 5555L;
}
访问标志(AccessFlags)
在常量池结束之后,紧接着的两个字节代表访问标志(access_flags),这个标志用于识别一些类或者接口层次的访问信息,包括:这个Class是类还是接口;是否定义为public类型;是否定义为abstract类型;如果是类的话,是否被声明为final等。具体的标志位以及标志的含义如下。
标志名称 | 标志值 | 含义 |
---|---|---|
ACC_PUBLIC | 0x0001 | 是否为public类型 |
ACC_FINAL | 0x0010 | 是否为final,只有类可以设置 |
ACC_SUPER | 0x0020 | 是否允许使用invokespecial字节码指令,1.2版本以后为真 |
ACC_INTERFACE | 0x0200 | 标识这是一个接口 |
ACC_ABSTRACT | 0x0400 | 是否为abstract类型,对于接口和抽象类为真 |
ACC_SYNTHETIC | 0x1000 | 标识这个类型并非由用户代码产生 |
ACC_ANNOTATION | 0x2000 | 标识这是一个注解 |
ACC_ENUM | 0x4000 | 标识这是一个枚举 |
access_flags中一共有16个标志位可以使用,当前只定义了其中8个,没有使用到的标志位要求一律为0。
实战
以Test.Class为例,他是一个普通Java类,不是接口、枚举、注解等,被public修饰但没有被声明为final或abstract,使用了JDK1.2之后的编译器进行编译 ,因此他的ACC_SUPER和ACC_PUBLIC为真,其余为假。因此access_flag的值为:0x0001|0x0020 = 0x0021
# 访问标志
0000090: 612f 6c61 6e67 2f4f 626a 6563 74 0021 00
类索引(ThisClass)、父类索引(SuperClass)与接口索引集合(Interfaces)
类索引(this_class)、父类索引(super_class)和接口索引(interfaces)都按顺序排列在访问标志之后,类索引和父类索引用两个 u2 类型的索引值表示,而接口索引是一组 u2 类型的数据的集合。
类索引用于确定该类的全限定名,父类索引确定该类的父类的全限定名,由于 Java 不允许多继承,因此父类索引只有一个,Object 类的父类索引为 0。接口索引集合就用来描述这个类实现了哪些接口,这些被实现的接口将按implements语句(如果这个类本身是一个接口,则应当是extends语句)后的接口顺序从左到右排列在接口索引集合中。类索引和父类索引各自指向一个 CONSTANT_Class_info 的类描述符常量,通过这个索引值可以找到定义在 CONSTANT_Utf8_info 类型的常量中的全限定名字符串。
对于接口索引集合,入口的第一项 u2 类型的数据为接口计数器(interfaces_count),表示索引表的容量,如果该类没有实现任何接口,则该计数器的值为 0,后面接口的索引表不再占用任何字节。
实战
类索引(this_class)和父类索引(super_class)都是一个 u2 类型的数据,而接口索引集合(interfaces)是一组 u2 类型的数据集合,Class 文件中由这三项数据来确定这个类的继承关系。
- 类索引用于确定这个类的全限定名
- 父类索引用于确定这个类的父类的全限定名
- 接口索引集合用于描述这个类实现了哪些接口
类索引、父类索引、接口索引都排在访问标志之后。由于所有的类都是java.lang.Object类的子类,因此除了Object类之外所有类的父类索引都不为0。
类索引和父类索引各自指向CONSTANT_Class_info的类描述常量,通过CONSTANT_Class_info的类型常量中的索引可以找到
CONSTANT_Utf8_info类型的常量中的全限定名字符串。从而获取到该类的全限定名
0000090: 612f 6c61 6e67 2f4f 626a 6563 7400 2100 a/lang/Object.!.
# this_class
00000a0: 0300 0400 0000 0100 0200 0500 0600 0000 ................
根据上述字节码文件:0x0021之后就是类索引(this_class)0x0003,即常量池索引为3的第三个常量0x07(CONSTANT_Class_info类型常量),后面的0x0011指向全限定名常量项的索引,即第17个常量,该常量一定是一个CONSTANT_Utf8_info类型的常量,该常量除了tag索引值外,后面的u2为length表示UTF-8编码的字符串长度(以字节为单位),length后面的16进制字节码就是相应的字符串。
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#3 = Class #17 // Test
#17 = Utf8 Test
类索引之后为父类索引(super_class) = 0x0004,十进制表示第4个常量
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#4 = Class #18 // java/lang/Object
#18 = Utf8 java/lang/Object
父类索引之后为接口索引(interfaces) = 0x0000,因为没有实现任何接口,因此为全0.
字段表集合(FieldInfo)
字段表集合(field_info)用于描述接口或类中声明的变量,包括类级变量以及实例级变量,但不包括在方法内部声明的局部变量。字段包含待信息有字段的作用域(public、private、protected)、是实例变量还是类变量(static)、可变性(final)等等。这些信息要么有,要么没有,很适合用标志位来表示,而字段叫什么,被定义为什么数据类型,这些都无法固定,只能用常量池中的常量来描述。
字段表fields结构组成格式如下。
类型 | 名称 | 数量 |
---|---|---|
u2 | access_flags | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | attributes_count | 1 |
attrubute_info | attributes | attributes_count |
- access_flags:标识变量的访问表示,该值可选由JVM规范规定。在实际情况中,ACC_PUBLIC、ACC_PRIVATE、ACC_PROTECTED三个标志最多只能选择其一,ACC_FINAL、ACC_VOLATILE不能同时选择。接口之中的字段必须有ACC_PUBLIC、ACC_STATIC、ACC_FINAL标志,这些都是由Java本身的语言规则所决定的。
- name_index:变量的简单名称引用,指向常量池索引。简单名称是指没有类型和参数修饰的方法或者字段名称
- descriptor_index:变量的类型信息,引用字段和方法的描述符,指向常量池索引。描述符的作用是用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值。根据描述符规则,基本数据类型(byte、char、double、float、int、long、short、boolean)以及代表无返回值的void类型都用一个大写字符来表示,而对象类型则用字符L加对象的全限定名来表示。
字段修饰符放在access_flags项目中,它与类中的access_flags项目是非常类似的,都是一个u2的数据类型,access_flags可选项如下:
标志名称 | 标志值 | 含义 |
---|---|---|
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 |
方法和字段的描述符
标识符 | 含义 |
---|---|
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,int sourceOffset,int sourceCount,char[]target,int targetOffset,int targetCount,int fromIndex)的描述符为“([CII[CIII)I”。
实战
Test.java类中声明的变量有 private int m;
查看下列字节码标注和字段修饰符表知access_flag = 0x0002= ACC_PRIVATE
# fields_count access_flag name_index descriptor_index attributes_count
00000a0: 03 0004 0000 0001 0002 0005 0006 0000
name_index = 0x0005 = 5 ,查看常量池表第五个常量是CONSTANT_Utf8_info,其值为 m。而 name_index = 0x0006=6,也是CONSTANT_Utf8_info常量,值为 I
常量池信息:
#5 = Utf8 m
#6 = Utf8 I
attributes_count
表示属性表计数器为0,也就是没有需要额外描述的信息。但是如果将 字段 m 的声明改为 private int m = 123
,则会存在一项名称为ConstantVaule的属性,其值指向常量123。
方法表集合(MethodInfo)
Class文件存储格式中对方法的描述与对字段的描述几乎采用了完全一致的方式,方法表的结构如同字段表一样,依次包括了访问标志(access_flags)、名称索引(name_index)、描述符索引(descriptor_index)、属性表集合(attributes)几项。这些数据项目的含义也非常类似,仅在访问标志和属性表集合的可选项中有所区别。
methods结构组成格式
类型 | 名称 | 数量 |
---|---|---|
u2 | access_flags | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | attributes_count | 1 |
attrubute_info | attributes | attributes_count |
因为volatile关键字和transient关键字不能修饰方法,所以方法表的访问标志中没有了ACC_VOLATILE标志和ACC_TRANSIENT标志。与之相对的,synchronized、native、strictfp和abstract关键字可以修饰方法,所以方法表的访问标志中增加了ACC_SYNCHRONIZED、ACC_NATIVE、ACC_STRICTFP和ACC_ABSTRACT标志。对于方法表,所有标志位及其取值如下。
access_flags可选项
标志名称 | 标志值 | 含义 |
---|---|---|
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_STRICTFP | 0x0800 | 是否为strictfp |
ACC_SYNTHETIC | 0x1000 | 是否为编译器自动产生 |
对于方法里的代码,经过编译器编译成字节码指令后,存放在方法属性表中一个名为「Code」的属性里面。
实战
根据methods_count = 2
知Test.java类中有两个方法,除了inc()方法外,还有一个编译器添加的实例构造方法
第一个方法的访问标志值为access_flag = 0x0001即ACC_PUBLIC方法,name_index = 0x0007 查常量池知名为
# methods_count access_flag name_index descriptor_index attributes_count
00000b0: 0002 0001 0007 0008 0001
# attributes_name_index
0009 00 00 00 1d 00
常量池信息:
Constant pool:
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
如果父类方法没有被子类重写,方法表集合中就不会出现来自父类的方法信息。否则编译器会自动添加方法,最常见的添加的方法便是类构造器
属性表集合(AttributeInfo)
在 Class 文件、字段表、方法表中都可以携带自己的属性表(attribute_info)集合,用于描述某些场景专有的信息。
属性表集合不像 Class 文件中的其它数据项要求这么严格,不强制要求各属性表的顺序,并且只要不与已有属性名重复,任何人实现的编译器都可以向属性表中写入自己定义的属性信息,Java 虚拟机在运行时会略掉它不认识的属性。
虚拟机中预定义的属性:
属性名称 | 使用位置 | 含义 |
---|---|---|
Code | 方法表 | Java代码编译成的字节码指令 |
ConstantValue | 字段表 | final关键字定义的常量池 |
Deprecated | 类,方法,字段表 | 被声明为deprecated的方法和字段 |
Exceptions | 方法表 | 方法抛出的异常 |
EnclosingMethod | 类文件 | 仅当一个类为局部类或者匿名类是才能拥有这个属性,这个属性用于标识这个类所在的外围方法 |
InnerClass | 类文件 | 内部类列表 |
LineNumberTable | Code属性 | Java源码的行号与字节码指令的对应关系 |
LocalVariableTable | Code属性 | 方法的局部变量描述 |
StackMapTable | Code属性 | JDK1.6中新增的属性,供新的类型检查检验器检查和处理目标方法的局部变量和操作数有所需要的类是否匹配 |
Signature | 类,方法表,字段表 | 用于支持泛型情况下的方法签名 |
SourceFile | 类文件 | 记录源文件名称 |
SourceDebugExtension | 类文件 | 用于存储额外的调试信息 |
Synthetic | 类,方法表,字段表 | 标志方法或字段为编译器自动生成的 |
LocalVariableTypeTable | 类 | 使用特征签名代替描述符,是为了引入泛型语法之后能描述泛型参数化类型而添加 |
RuntimeVisibleAnnotations | 类,方法表,字段表 | 为动态注解提供支持 |
RuntimeInvisibleAnnotations | 表,方法表,字段表 | 用于指明哪些注解是运行时不可见的 |
RuntimeVisibleParameterAnnotation | 方法表 | 作用与RuntimeVisibleAnnotations属性类似,只不过作用对象为方法 |
RuntimeInvisibleParameterAnnotation | 方法表 | 作用与RuntimeInvisibleAnnotations属性类似,作用对象哪个为方法参数 |
AnnotationDefault | 方法表 | 用于记录注解类元素的默认值 |
BootstrapMethods | 类文件 | 用于保存invokeddynamic指令引用的引导方式限定符 |
对于每个属性,它的名称需要从常量池中引用一个CONSTANT_Utf8_info类型的常量来表示,而属性值的结构则是完全自定义的,只需要通过一个u4的长度属性去说明属性值所占用的位数即可。一个符合规则的属性表应该满足表6-14中所定义的结构。
对于每个属性,它的名称需要从常量池中引用一个CONSTANT_utf8_info类型的常量类表示,而属性值的结构则是完全自定义的,只需要通过一个u4的长度属性区说明属性值所占用的位数即可。
属性表定义的结构:
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u2 | attribute_length | 1 |
u1 | info | attribute_length |
Code属性
Java程序方法体中的代码经过Javac编译器处理后,最终变为字节码指令存储在Code属性内。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_bale | 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:代表了操作数栈(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类型的长度值,理论上最大值可以达到2(32次方)-1,但是虚拟机规范中明确限制了一个方法不允许超过65535条字节码指令,即它实际只使用了u2的长度,如果超过这个限制,Javac编译器也会拒绝编译。一般来讲,编写Java代码时只要不是刻意去编写一个超长的方法来为难编译器,是不太可能超过这个最大值的限制。但是,某些特殊情况,例如在编译一个很复杂的JSP文件时,某些JSP编译器会把JSP内容和页面输出的信息归并于一个方法之中,就可能因为方法生成字节码超长的原因而导致编译失败。
实战
Code属性是Class文件中最重要的一个属性,如果把一个Java程序中的信息分为代码(Code,方法体里面的Java代码)和元数据(Metadata,包括类、字段、方法定义及其他信息)两部分,那么在整个Class文件中,Code属性用于描述代码,所有的其他数据项目都用于描述元数据。了解Code属性是学习后面关于字节码执行引擎内容的必要基础,能直接阅读字节码也是工作中分析Java代码语义问题的必要工具和基本技能,因此笔者准备了一个比较详细的实例来讲解虚拟机是如何使用这个属性的。
继续以TestClass.class文件为例,实例构造器“<init>”方法的Code属性。
public Test();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 1: 0
它的操作数栈的最大深度和本地变量表的容量都为0x0001,字节码区域所占空间的长度为0x0005。虚拟机读取到字节码区域的长度后,按照顺序依次读入紧随的5个字节,并根据字节码指令表翻译出所对应的字节码指令。翻译“2A B7 00 0A B1”的过程为: 1)读入2A,查表得0x2A对应的指令为aload_0,这个指令的含义是将第0个Slot中为reference类型的本地变量推送到操作数栈顶。 2)读入B7,查表得0xB7对应的指令为invokespecial,这条指令的作用是以栈顶的reference类型的数据所指向的对象作为方法接收者,调用此对象的实例构造器方法、private方法或者它的父类的方法。这个方法有一个u2类型的参数说明具体调用哪一个方法,它指向常量池中的一个CONSTANT_Methodref_info类型常量,即此方法的方法符号引用。 3)读入00 0A,这是invokespecial的参数,查常量池得0x000A对应的常量为实例构造器“<init>”方法的符号引用。 4)读入B1,查表得0xB1对应的指令为return,含义是返回此方法,并且返回值为void。这条指令执行后,当前方法结束。
这段字节码虽然很短,但是至少可以看出它的执行过程中的数据交换、方法调用等操作都是基于栈(操作栈)的。我们可以初步猜测:Java虚拟机执行字节码是基于栈的体系结构。但是与一般基于堆栈的零字节指令又不太一样,某些指令(如invokespecial)后面还会带有参数。
我们使用javap命令把此Class文件中的另外一个方法的字节码指令也计算出来。
{
public Test();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 1: 0
public int inc();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: getfield #2 // Field m:I
4: iconst_1
5: iadd
6: ireturn
LineNumberTable:
line 6: 0
}
如果大家注意到javap中输出的“Args_size”的值,可能会有疑问:这个类有两个方法——实例构造器<init>()和inc(),这两个方法很明显都是没有参数的,为什么Args_size会为1?而且无论是在参数列表里还是方法体内,都没有定义任何局部变量,那Locals又为什么会等于1?如果有这样的疑问,大家可能是忽略了一点:在任何实例方法里面,都可以通过“this”关键字访问到此方法所属的对象。这个访问机制对Java程序的编写很重要,而它的实现却非常简单,仅仅是通过Javac编译器编译的时候把对this关键字的访问转变为对一个普通方法参数的访问,然后在虚拟机调用实例方法时自动传入此参数而已。因此在实例方法的局部变量表中至少会存在一个指向当前对象实例的局部变量,局部变量表中也会预留出第一个Slot位来存放对象实例的引用,方法参数值从1开始计算。这个处理只对实例方法有效,如果代码清单6-1中的inc()方法声明为static,那Args_size就不会等于1而是等于0了。
在字节码指令之后的是这个方法的显式异常处理表(下文简称异常表)集合,异常表对于Code属性来说并不是必须存在的,如代码清单6-4中就没有异常表生成。
异常表的格式如表6-16所示,它包含4个字段,这些字段的含义为:如果当字节码在第start_pc行到第end_pc行之间(不含第end_pc行)出现了类型为catch_type或者其子类的异常(catch_type为指向一个CONSTANT_Class_info型常量的索引),则转到第handler_pc行继续处理。当catch_type的值为0时,代表任意异常情况都需要转向到handler_pc处进行处理。
异常表实际上是Java代码的一部分,编译器使用异常表而不是简单的跳转命令来实现Java异常及finally处理机制。
代码清单6-5是一段演示异常表如何运作的例子,这段代码主要演示了在字节码层面中try-catch-finally是如何实现的。在阅读字节码之前,大家不妨先看看下面的Java源码,想一下这段代码的返回值在出现异常和不出现异常的情况下分别应该是多少?
代码清单6-5 异常表运作演示
//Java源码
public int inc(){
int x;
try{
x=1;
return x;
}catch(Exception e){
x=2;
return x;
}finally{
x=3;
}}
//编译后的ByteCode字节码及异常表
public int inc();
Code:
Stack=1,Locals=5,Args_size=1
0:iconst_1//try块中的x=1
1:istore_1
2:iload_1//保存x到returnValue中,此时x=1
3:istore 4
5:iconst_3//finaly块中的x=3
6:istore_1
7:iload 4//将returnValue中的值放到栈顶,准备给ireturn返回
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条执行路径分别为:
- 如果try语句块中出现属于Exception或其子类的异常,则转到catch语句块处理。
- 如果try语句块中出现不属于Exception或其子类的异常,则转到finally语句块处理。
- 如果catch语句块中出现任何异常,则转到finally语句块处理。
返回到我们上面提出的问题,这段代码的返回值应该是多少?对Java语言熟悉的读者应该很容易说出答案:如果没有出现异常,返回值是1;如果出现了Exception异常,返回值是2;如果出现了Exception以外的异常,方法非正常退出,没有返回值。我们一起来分析一下字节码的执行过程,从字节码的层面上看看为何会有这样的返回结果。
字节码中第0~4行所做的操作就是将整数1赋值给变量x,并且将此时x的值复制一份副本到最后一个本地变量表的Slot中(这个Slot里面的值在ireturn指令执行前将会被重新读到操作栈顶,作为方法返回值使用。为了讲解方便,笔者给这个Slot起了个名字:returnValue)。如果这时没有出现异常,则会继续走到第5~9行,将变量x赋值为3,然后将之前保存在returnValue中的整数1读入到操作栈顶,最后ireturn指令会以int形式返回操作栈顶中的值,方法结束。如果出现了异常,PC寄存器指针转到第10行,第10~20行所做的事情是将2赋值给变量x,然后将变量x此时的值赋给returnValue,最后再将变量x的值改为3。方法返回前同样将returnValue中保留的整数2读到了操作栈顶。从第21行开始的代码,作用是变量x的值赋为3,并将栈顶的异常抛出,方法结束。
尽管大家都知道这段代码出现异常的概率非常小,但并不影响它为我们演示异常表的作用。如果大家到这里仍然对字节码的运作过程比较模糊,其实也不要紧,关于虚拟机执行字节码的过程,本书第8章中将会有更详细的讲解。
Exceptions属性
这里的Exceptions属性是在方法表中与Code属性平级的一项属性,读者不要与前面刚刚讲解完的异常表产生混淆。Exceptions属性的作用是列举出方法中可能抛出的受查异常(Checked Excepitons),也就是方法描述时在throws关键字后面列举的异常。如下。
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | number_of_exceptions | 1 |
u2 | exception_index_table | number_of_exceptions |
Exceptions属性中的number_of_exceptions项表示方法可能抛出number_of_exceptions种受查异常,每一种受查异常使用一个exception_index_table项表示,exception_index_table是一个指向常量池中CONSTANT_Class_info型常量的索引,代表了该受查异常的类型。
LineNumberTable属性
LineNumberTable属性用于描述Java源码行号与字节码行号(字节码的偏移量)之间的对应关系。它并不是运行时必需的属性,但默认会生成到Class文件之中,可以在Javac中分别使用-g:none或-g:lines选项来取消或要求生成这项信息。如果选择不生成LineNumberTable属性,对程序运行产生的最主要的影响就是当抛出异常时,堆栈中将不会显示出错的行号,并且在调试程序的时候,也无法按照源码行来设置断点。LineNumberTable属性的结构如下。
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | line_number_table_length | 1 |
line_number_table_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两个u2类型的数据项,前者是字节码行号,后者是Java源码行号。
line_number_table_info属性结构表
类型 | 名称 | 数量 | 备注 |
---|---|---|---|
u2 | start_pc | 1 | 字节码行号 |
u4 | line_number | 1 | 源码行号 |
LocalVariableTable属性
LocalVariableTable属性用于描述栈帧中局部变量表中的变量与Java源码中定义的变量之间的关系,它也不是运行时必需的属性,但默认会生成到Class文件之中,可以在Javac中分别使用-g:none或-g:vars选项来取消或要求生成这项信息。如果没有生成这项属性,最大的影响就是当其他人引用这个方法时,所有的参数名称都将会丢失,IDE将会使用诸如arg0、arg1之类的占位符代替原有的参数名,这对程序运行没有影响,但是会对代码编写带来较大不便,而且在调试期间无法根据参数名称从上下文中获得参数值。LocalVariableTable属性的结构如下。
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | local_variable_table_length | 1 |
local_variable_table_info | local_variable_table | local_variable_table_length |
其中,local_variable_info项目代表了一个栈帧与源码中的局部变量的关联,结构见表6-20。
local_variable_info结构表
类型 | 名称 | 数量 | 备注 |
---|---|---|---|
u2 | start_pc | 1 | 局部变量的生命周期开始的字节码偏移量 |
u2 | length | 1 | 局部变量作用范围覆盖长度 |
u2 | name_index | 1 | 局部变量名称索引 |
u2 | description_index | 1 | 局部变量描述符 |
u2 | index | 1 | 局部变量在栈帧局部变量表中Slot位置,如果为64位,会占用Slot的index和index+1位置 |
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。
SourceFile属性
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属性
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属性如下:
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | constantvalue_index | 1 |
从数据结构中可以看出,ConstantValue属性是一个定长属性,它的attribute_length数据项值必须固定为2。constantvalue_index数据项代表了常量池中一个字面量常量的引用,根据字段类型的不同,字面量可以是CONSTANT_Long_info、CONSTANT_Float_info、CONSTANT_Double_info、CONSTANT_Integer_info、CONSTANT_String_info常量中的一种。
InnerClasses属性
InnerClasses属性用于记录内部类与宿主类之间的关联。如果一个类中定义了内部类,那编译器将会为它以及它所包含的内部类生成InnerClasses属性。该属性的结构如下。
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | number_of_classes | 1 |
u2 | inner_classes | number_of_classes |
数据项number_of_classes代表需要记录多少个内部类信息,每一个内部类的信息都由一个inner_classes_info表进行描述。inner_classes_info表的结构如下:
inner_class_info表结构
类型 | 名称 | 数量 | 备注 |
---|---|---|---|
u2 | inner_class_info_index | 1 | 指向常量池Class索引 |
u2 | outer_class_info_index | 1 | 指向常量池Class索引 |
u2 | inner_class_name_index | 1 | 指向常量池utf-8类型索引,为内部类名称,如果为匿名类则为0 |
u2 | inner_name_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源码直接产生的,而是由编译器自行添加的,在JDK 1.5之后,标识一个类、字段或者方法是编译器自动产生的,也可以设置它们访问标志中的ACC_SYNTHETIC标志位,其中最典型的例子就是Bridge Method。所有由非用户代码产生的类、方法及字段都应当至少设置Synthetic属性和ACC_SYNTHETIC标志位中的一项,唯一的例外是实例构造器“<init>”方法和类构造器“<clinit>”方法。
Deprecated和Synthetic属性的结构非常简单。如下:
类型 | 名称 | 数量 | 备注 |
---|---|---|---|
u2 | attribute_name_index | 1 | |
u4 | attribute_length | 1 | 0x00000000 |
其中attribute_length数据项的值必须为0x00000000,因为没有任何属性值需要设置。 |
StackMapTable属性
StackMapTable属性在JDK 1.6发布后增加到了Class文件规范中,它是一个复杂的变长属性,位于Code属性的属性表中。这个属性会在虚拟机类加载的字节码验证阶段被新类型检查验证器(Type Checker)使用(见7.3.2节),目的在于代替以前比较消耗性能的基于数据流分析的类型推导验证器。
这个类型检查验证器最初来源于Sheng Liang(听名字似乎是虚拟机团队中的华裔成员)为Java ME CLDC实现的字节码验证器。新的验证器在同样能保证Class文件合法性的前提下,省略了在运行期通过数据流分析去确认字节码的行为逻辑合法性的步骤,而是在编译阶段将一系列的验证类型(Verification Types)直接记录在Class文件之中,通过检查这些验证类型代替了类型推导过程,从而大幅提升了字节码验证的性能。这个验证器在JDK 1.6中首次提供,并在JDK 1.7中强制代替原本基于类型推断的字节码验证器。关于这个验证器的工作原理,《Java虚拟机规范(Java SE 7版)》花费了整整120页的篇幅来讲解描述,并且分析证明新验证方法的严谨性,笔者在此不再赘述。
StackMapTable属性中包含零至多个栈映射帧(Stack Map Frames),每个栈映射帧都显式或隐式地代表了一个字节码偏移量,用于表示该执行到该字节码时局部变量表和操作数栈的验证类型。类型检查验证器会通过检查目标方法的局部变量和操作数栈所需要的类型来确定一段字节码指令是否符合逻辑约束。StackMapTable属性的结构见表6-27。
《Java虚拟机规范(Java SE 7版)》明确规定:在版本号大于或等于50.0的Class文件中,如果方法的Code属性中没有附带StackMapTable属性,那就意味着它带有一个隐式的StackMap属性。这个StackMap属性的作用等同于number_of_entries值为0的StackMapTable属性。一个方法的Code属性最多只能有一个StackMapTable属性,否则将抛出ClassFormatError异常。
Signature属性
Signature属性在JDK 1.5发布后增加到了Class文件规范之中,它是一个可选的定长属性,可以出现于类、属性表和方法表结构的属性表中。在JDK 1.5中大幅增强了Java语言的语法,在此之后,任何类、接口、初始化方法或成员的泛型签名如果包含了类型变量(Type Variables)或参数化类型(Parameterized Types),则Signature属性会为它记录泛型签名信息。之所以要专门使用这样一个属性去记录泛型类型,是因为Java语言的泛型采用的是擦除法实现的伪泛型,在字节码(Code属性)中,泛型信息编译(类型变量、参数化类型)之后都通通被擦除掉。使用擦除法的好处是实现简单(主要修改Javac编译器,虚拟机内部只做了很少的改动)、非常容易实现Backport,运行期也能够节省一些类型所占的内存空间。但坏处是运行期就无法像C#等有真泛型支持的语言那样,将泛型类型与用户定义的普通类型同
等对待,例如运行期做反射时无法获得到泛型信息。Signature属性就是为了弥补这个缺陷而增设的,现在Java的反射API能够获取泛型类型,最终的数据来源也就是这个属性。关于Java泛型、Signature属性和类型擦除,在第10章介绍编译器优化的时候会通过一个具体的例子来讲解。Signature属性的结构见表6-28。
其中signature_index项的值必须是一个对常量池的有效索引。常量池在该索引处的项必须是CONSTANT_Utf8_info结构,表示类签名、方法类型签名或字段类型签名。如果当前的Signature属性是类文件的属性,则这个结构表示类签名,如果当前的Signature属性是方法表的属性,则这个结构表示方法类型签名,如果当前Signature属性是字段表的属性,则这个结构表示字段类型签名。
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指令的运作原理,笔者将在第8章专门用1节篇幅去介绍它们,在此先暂时略过。
目前的Javac暂时无法生成InvokeDynamic指令和BootstrapMethods属性,必须通过一些非常规的手段才能使用到它们,也许在不久的将来,等JSR-292更加成熟一些,这种状况就会改变。BootstrapMethods属性的结构见表6-29。
其中引用到的bootstrap_method结构见表6-30。
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。
运行时注解相关属性
JDK 5时期,Java语言的语法进行了多项增强,其中之一是提供了对注解(Annotation)的支持。为了存储源码中注解信息,Class文件同步增加了RuntimeVisibleAnnotations、RuntimeInvisibleAnnotations、RuntimeVisibleParameterAnnotations和RuntimeInvisibleParameter-Annotations四个属性。JDK 8,进一步加强了Java语言的注解使用范围,又新增类型注解(JSR 308),所以Class文件中也同步增加了RuntimeVisibleTypeAnnotations和RuntimeInvisibleTypeAnnotations两个属性。这六个属性不论结构还是功能都比较雷同,以RuntimeVisibleAnnotations为例:
RuntimeVisibleAnnotations是一个变长属性,它记录了类、字段或方法的声明上记录运行时可见注解,当我们使用反射API来获取类、字段或方法上的注解时,返回值就是通过这个属性来取到的。