Class文件是一组以8个字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在文
件之中,中间没有添加任何分隔符,这使得整个Class文件中存储的内容几乎全部是程序运行的必要数
据,没有空隙存在。当遇到需要占用8个字节以上空间的数据项时,则会按照高位在前[2]的方式分割
成若干个8个字节进行存储。
根据《Java虚拟机规范》的规定,Class文件格式采用一种类似于C语言结构体的伪结构来存储数
据,这种伪结构中只有两种数据类型:“无符号数”和“表”。后面的解析都要以这两种数据类型为基
础,所以这里笔者必须先解释清楚这两个概念。
- 无符号数属于基本的数据类型,以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节和8个
字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值。 - 表是由多个无符号数或者其他表作为数据项构成的复合数据类型,为了便于区分,所有表的命名
都习惯性地以“_info”结尾。表用于描述有层次关系的复合结构的数据,整个Class文件本质上也可以视作是一张表,这张表由表6-1所示的数据项按严格顺序排列构成。
魔数与Class文件的版本
前四个字节(0-3)被称为魔数,表示他是否是【class】类型的文件。魔数值:CAFEBABE
紧接着魔数的4个字节存储的是Class文件的版本号:第5和第6个字节是次版本号(Minor Version),第7和第8个字节是主版本号(Major Version)。Java的版本号是从45开始的,JDK 1.1之后的每个JDK大版本发布主版本号向上加1(JDK 1.0~1.1使用了45.0~45.3的版本号),高版本的JDK能向下兼容以前版本的Class文件,但不能运行以后版本的Class文件,因为《Java虚拟机规范》在Class文件校验部分明确要求了即使文件格式并未发生任何变化,虚拟机也必须拒绝执行超过其版本号的Class文件。
常量池
紧接着主、次版本号之后的是常量池入口,常量池可以比喻为Class文件里的资源仓库,它是Class
文件结构中与其他项目关联最多的数据,通常也是占用Class文件空间最大的数据项目之一,另外,它
还是在Class文件中第一个出现的表类型数据项目。
第九第十个字节表示常量池容量计数值(constant_pool_count),因为常量池中常量的数量是不固定的,所以需要记录个数。常量池的计数是从#1项开始,#0不计入,这样设计的目的在于,如果后面某些指向常量池的索引值的数据在特定情况下需要表达“不引用任何一个常量池项目”的含义,可以把索引值设置为#0来表示。Class文件结构中只有常量池的容量计数是从#1开始,对于其他集合类型,包括接口索引集合、字段表集合、方法表集合等的容量计数都与一般习惯相同,是从#0开始。
常量池中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References)。字面量比较接近于Java语言层面的常量概念,如文本字符串、被声明为final的常量值等。而符号引用则属于编译原理方面的概念,主要包括下面几类常量:
- 被模块导出或者开放的包(Package)
- 类和接口的全限定名(Fully Qualified Name)
- 字段的名称和描述符(Descriptor)
- 方法的名称和描述符
- 方法句柄和方法类型(Method Handle、Method Type、Invoke Dynamic)
- 动态调用点和动态常量(Dynamically-Computed Call Site、Dynamically-Computed Constant)
Java代码在进行Javac编译的时候,并不像C和C++那样有“连接”这一步骤,而是在虚拟机加载Class文件的时候进行动态连接。也就是说,在Class文件中不会保存各个方法、字段最终在内存中的布局信息,这些字段、方法的符号引用不经过虚拟机在运行期转换的话是无法得到真正的内存入口地址,也就无法直接被虚拟机使用的。当虚拟机做类加载时,将会从常量池获得对应的符号引用,再在类创建时或运行时解析、翻译到具体的内存地址之中。关于类的创建和动态连接的内容,在后面再介绍。截至JDK 13,常量表中分别有17种不同类型的常量。
常量池中每一项常量都是一个表,每一个表结构起始的第一个字节是类型的标志位(tag,取值见下图)代表当前的常量属于哪种常量类型。
参见:https://docs.oracle.com/javase/specs/jvms/se11/html/jvms-4.html
类型 | 标志 | 描述 |
---|---|---|
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_MethodHandle_info | 15 | 表示方法句柄 |
CONSTANT_MothodType_info | 16 | 标志方法类型 |
CONSTANT_Dynamic_info | 17 | 表示动态计算常量 |
CONSTANT_InvokeDynamic_info | 18 | 表示一个动态方法调用点 |
CONSTANT_Module_info | 19 | 表示一个模块 |
CONSTANT_Package_info | 20 | 表示一个模块中开放或者导出的包 |
如下是常量池中17种数据类型的结构总表:
字节码翻译示例:
package cn.itcast.jvm.t5;
// 二进制字节码(类基本信息,常量池,类方法定义,包含了虚拟机指令)
public class HelloWorld {
public static void main(String[] args) {
System.out.println("hello world");
}
}
如上一段简单的代码在javac编译之后用notepad++的十六进制小插件打开
开始翻译:
前四个ca fe ba be是魔数,如果被篡改就不能执行了。接下去00 00 00 34表示版本号,34表示我用的Java 8版本,即十进制的52,接下去00 1d代表常量池中的常量个数,表示十进制的29,也就是代表常量池中有28项常量索引范围为1~28,接下去就是常量池的具体内容了,每一项常量表第一个字节是标志位,0a转成十进制是11,也就是对应上面表格的“CONSTANT_InterfaceMethodref_info”,再查找对应的结构总表中的信息,00 06表示引用了常量池的#6项获取这个方法的所属类,00 0f表示引用了常量池的#15项获取这个方法的方法名,自此第一个常量表的信息解读完毕,接下去看第二个,09查表可知这是一个字段的引用,00 10和00 11分别表示引用了常量池的#16和#17项获取这个成员的所属类和成员变量名。以此类推,查表得到全部的信息,没有特别的规律可言。
当然了,我们其实也可以利用Java的命令直接计算出这些常量表含义:
javap -verbose HelloWorld.class
Last modified 2021-5-15; size 442 bytes
MD5 checksum 103606e24ec918e862312533fda15bbc
Compiled from "HelloWorld.java"
public class cn.itcast.jvm.t5.HelloWorld
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #6.#15 // java/lang/Object."<init>":()V
#2 = Fieldref #16.#17 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #18 // hello world
#4 = Methodref #19.#20 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Class #21 // cn/itcast/jvm/t5/HelloWorld
#6 = Class #22 // java/lang/Object
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 main
#12 = Utf8 ([Ljava/lang/String;)V
#13 = Utf8 SourceFile
#14 = Utf8 HelloWorld.java
#15 = NameAndType #7:#8 // "<init>":()V
#16 = Class #23 // java/lang/System
#17 = NameAndType #24:#25 // out:Ljava/io/PrintStream;
#18 = Utf8 hello world
#19 = Class #26 // java/io/PrintStream
#20 = NameAndType #27:#28 // println:(Ljava/lang/String;)V
#21 = Utf8 cn/itcast/jvm/t5/HelloWorld
#22 = Utf8 java/lang/Object
#23 = Utf8 java/lang/System
#24 = Utf8 out
#25 = Utf8 Ljava/io/PrintStream;
#26 = Utf8 java/io/PrintStream
#27 = Utf8 println
#28 = Utf8 (Ljava/lang/String;)V
{
public cn.itcast.jvm.t5.HelloWorld();
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 4: 0
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String hello world
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 6: 0
line 7: 8
}
SourceFile: "HelloWorld.java"
访问标志
在常量池结束之后,紧接着的2个字节代表访问标志(access_flags),这个标志用于识别一些类或者接口层次的访问信息,包括:这个Class是类还是接口;是否定义为public类型;是否定义为abstract类型;如果是类的话,是否被声明为final;等等。
标记名 | 值 | 含义 |
---|---|---|
ACC_PUBLIC | 0x0001 | 是否为Public类型 |
ACC_FINAL | 0x0010 | 是否被声明为final,只有类可设置 |
ACC_SUPER | 0x0020 | 当用到 invokespecial 指令时,需要特殊处理的父类方法,JDK 1.0.2之后这个标志都为真 |
ACC_INTERFACE | 0x0200 | 标识定义的是接口而不是类 |
ACC_ABSTRACT | 0x0400 | 是否为abstract类型 |
ACC_SYNTHETIC | 0x1000 | 标识这个类并非用户代码产生 |
ACC_ANNOTATION | 0x2000 | 标识这是一个注解类型 |
ACC_ENUM | 0x4000 | 标识这是一个枚举类型 |
ACC_MODULE | 0x8000 | 标识这是一个模块 |
在上述的Helloworld示例中,Helloworld是一个普通Java类,不是接口、枚举、注解或者模块,被public关键字修饰但没有被声明为final和abstract,并且它使用了JDK 1.2之后的编译器进行编译,因此它的ACC_PUBLIC、ACC_SUPER标志应当为真,而ACC_FINAL、ACC_INTERFACE、ACC_ABSTRACT、ACC_SYNTHETIC、ACC_ANNOTATION、ACC_ENUM、ACC_MODULE这七个标志应当为假,因此它的access_flags的值应为:0x0001|0x0020=0x0021。
类索引、父索引、与接口索引集合
类索引、父类索引和接口索引集合都按顺序排列在访问标志之后,类索引和父类索引用两个u2类型的索引值表示,它们各自指向一个类型为CONSTANT_Class_info的类描述符常量,通过CONSTANT_Class_info类型的常量中的索引值可以找到定义在CONSTANT_Utf8_info类型的常量中的全限定名字符串,因为Java允许实现多个接口,所以接口索引集合(interfaces)是一组u2类型的数据的集合,它的起始位置必须有一个u2类型表示实现的接口数量然后才是具体的接口索引信息。
Class文件中由类索引、父类索引和接口索引三项数据来确定该类型的继承关系。类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名。由于Java语言不允许多重继承,所以父类索引只有一个,除了java.lang.Object之外,所有的Java类都有父类,因此除了java.lang.Object外,所有Java类的父类索引都不为0。接口索引集合就用来描述这个类实现了哪些接口,这些被实现的接口将按implements关键字(如果这个Class文件表示的是一个接口,则应当是extends关键字)后的接口顺序从左到右排列在接口索引集合中。
结合HelloWorld示例,00 05表示根据常量池的#5找到本类的全限定名,00 06表示根据常量池的#6找到父类全限定名,我们也可以结合上面javap命令计算出来的截图找到对应项的描述。00 00 表示这个类没有实现任何接口。
字段表集合
字段表(field_info)用于描述接口或者类中声明的变量。Java语言中的“字段”(Field)包括类级变量以及实例级变量,但不包括在方法内部声明的局部变量。字段可以包括的修饰符有字段的作用域(public、private、protected修饰符)、是实例变量还是类变量(static修饰符)、可变性(final)、并发可见性(volatile修饰符,是否强制从主内存读写)、可否被序列化(transient修饰符)、字段数据类型(基本类型、对象、数组)、字段名称。上述这些信息中,各个修饰符都是布尔值,要么有某个修饰符,要么没有,很适合使用标志位来表示。
在字段表集合前同样有一个u2类型的field数量字段,接下去才是真正的字段表数据。每个字段表格式如下
类型 | 名称 | 数量 |
---|---|---|
u2 | access_flags | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
可设置的标志位:
Flag Name | Value | Interpretation |
---|---|---|
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 . |
跟随access_flags标志的是两项索引值:name_index和descriptor_index。它们都是对常量池项的引用,分别代表着字段的简单名称以及字段和方法的描述符。现在需要解释一下“简单名称”“描述符”以及前面出现过多次的“全限定名”这三种特殊字符串的概念。
全限定名和简单名称很好理解。全限定名,仅仅是把类全名中的“.”替换成了“/”而已,为了使连续的多个全限定名之间不产生混淆,在使用时最后一般会加入一个“;”号表示全限定名结束。简单名称则就是指没有类型和参数修饰的方法或者字段名。相比于全限定名和简单名称,方法和字段的描述符就要复杂一些。描述符的作用是用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值。根据描述符规则,基本数据类型(byte、char、double、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/String |
对于数组类型,每一维度将使用一个前置的“[”字符来描述,如一个定义为“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”。
因为helloworld没有成员变量,我们拿了另一个示例HelloByteCode:
package cn.itcast.jvm.t5;
/**
* @author chenly
* @create 2021-05-22 15:11
*/
public class HelloByteCode {
public static final String STR = "hello world";
public static final int A_NUM = 20;
public static final int B_NUM = 50;
public static void main(String[] args) {
System.out.println(STR);
int c_num = A_NUM + B_NUM;
System.out.println(c_num);
}
}
用java命令翻译class文件
javap -verbose HelloByteCode.class
class
Last modified 2021-5-22; size 604 bytes
MD5 checksum 920a5ad640483fb1308139b0925cdf2c
Compiled from "HelloByteCode.java"
public class cn.itcast.jvm.t5.HelloByteCode
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #7.#24 // java/lang/Object."<init>":()V
#2 = Fieldref #25.#26 // java/lang/System.out:Ljava/io/PrintStream;
#3 = Class #27 // cn/itcast/jvm/t5/HelloByteCode
#4 = String #28 // hello world
#5 = Methodref #29.#30 // java/io/PrintStream.println:(Ljava/lang/String;)V
#6 = Methodref #29.#31 // java/io/PrintStream.println:(I)V
#7 = Class #32 // java/lang/Object
#8 = Utf8 STR
#9 = Utf8 Ljava/lang/String;
#10 = Utf8 ConstantValue
#11 = Utf8 A_NUM
#12 = Utf8 I
#13 = Integer 20
#14 = Utf8 B_NUM
#15 = Integer 50
#16 = Utf8 <init>
#17 = Utf8 ()V
#18 = Utf8 Code
#19 = Utf8 LineNumberTable
#20 = Utf8 main
#21 = Utf8 ([Ljava/lang/String;)V
#22 = Utf8 SourceFile
#23 = Utf8 HelloByteCode.java
#24 = NameAndType #16:#17 // "<init>":()V
#25 = Class #33 // java/lang/System
#26 = NameAndType #34:#35 // out:Ljava/io/PrintStream;
#27 = Utf8 cn/itcast/jvm/t5/HelloByteCode
#28 = Utf8 hello world
#29 = Class #36 // java/io/PrintStream
#30 = NameAndType #37:#38 // println:(Ljava/lang/String;)V
#31 = NameAndType #37:#39 // println:(I)V
#32 = Utf8 java/lang/Object
#33 = Utf8 java/lang/System
#34 = Utf8 out
#35 = Utf8 Ljava/io/PrintStream;
#36 = Utf8 java/io/PrintStream
#37 = Utf8 println
#38 = Utf8 (Ljava/lang/String;)V
#39 = Utf8 (I)V
{
public static final java.lang.String STR;
descriptor: Ljava/lang/String;
flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
ConstantValue: String hello world
public static final int A_NUM;
descriptor: I
flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
ConstantValue: int 20
public static final int B_NUM;
descriptor: I
flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
ConstantValue: int 50
public cn.itcast.jvm.t5.HelloByteCode();
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 7: 0
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #4 // String hello world
5: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: bipush 70
10: istore_1
11: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
14: iload_1
15: invokevirtual #6 // Method java/io/PrintStream.println:(I)V
18: return
LineNumberTable:
line 13: 0
line 14: 8
line 15: 11
line 16: 18
}
SourceFile: "HelloByteCode.java"
根据下图的十六进制class解析,找到类里定义的变量,00 03表示有3个字段(从0开始),对应STR、A_NUM、B_NUM,00 19表示access_flags,对应上面的表格也就是public static final
,00 08表示对应常量池第八项 #8 = Utf8 STR
也就是上文提到的“简单名称”,00 09表示常量池第9项#9 = Utf8 Ljava/lang/String;
也就是方法的描述符,00 01表示这个变量有一个额外信息,00 0a表示存在一项名称为ConstantValue的属性,其值指向hello world
对应常量池第10项#10 = Utf8 ConstantValue
。关于attribute_info的其他内容我们下面再讲。
方法表集合
Class文件存储格式中对方法的描述与对字段的描述采用了几乎完全一致的方式,方法表的结构如同字段表一样,依次包括访问标志(access_flags)、名称索引(name_index)、描述符索引(descriptor_index)、属性表
集合(attributes)。这些数据项目的含义也与字段表中的非常类似,仅在访问标志和属性表集合的可选项中有所区别。
类型 | 名称 | 数量 |
---|---|---|
u2 | access_flags | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
因为volatile关键字和transient关键字不能修饰方法,所以方法表的访问标志中没有了ACC_VOLATILE标志和ACC_TRANSIENT标志。与之相对,synchronized、native、strictfp和abstract关键字可以修饰方法,方法表的访问标志中也相应地增加了ACC_SYNCHRONIZED、ACC_NATIVE、ACC_STRICTFP和ACC_ABSTRACT标志。
Flag Name | Value | Interpretation |
---|---|---|
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代码,经过Javac编译器编译成字节码指令之后,存放在方法属性表集合中一个名为“Code”的属性里面,属性表作为Class文件格式中最具扩展性的一种数据项目,将在后续分析。
我们回到helloworld的class源码中,入口地址0x00000150的00 02表示集合有2个方法,这两个方法为编译器添加的实例构造器<init>和源码中定义的方法main()方法,接下去的00 01表示第一个方法的访问标志只有ACC_PUBLIC标志为真,名称索引值为00 07查上面的常量池得方法名为“<init>”,描述符索引值为00 08,对应常量为“()V”,属性表计数器attributes_count的值为00 01,表示此方法的属性表集合有1项属性,属性名称的索引值为00 09,对应常量为“Code”,说明此属性是方法的字节码描述。
与字段表集合相对应地,如果父类方法在子类中没有被重写(Override),方法表集合中就不会出现来自父类的方法信息。但同样地,有可能会出现由编译器自动添加的方法,最常见的便是类构造器“<clinit>()”方法和实例构造器“<init>()”方法。
属性表集合
属性表(attribute_info)在前面的讲解之中已经出现过数次,Class文件、字段表、方法表都可以携带自己的属性表集合,以描述某些场景专有的信息。
与Class文件中其他的数据项目要求严格的顺序、长度和内容不同,属性表集合的限制稍微宽松一些,不再要求各个属性表具有严格顺序,并且《Java虚拟机规范》允许只要不与已有属性名重复,任何人实现的编译器都可以向属性表中写入自己定义的属性信息,Java虚拟机运行时会忽略掉它不认识的属性。为了能正确解析Class文件,《Java虚拟机规范》最初只预定义了9项所有Java虚拟机实现都应当能识别的属性,而在最新的《Java虚拟机规范》的Java SE 12版本中,预定义属性已经增加到29项。
参见:https://docs.oracle.com/javase/specs/jvms/se11/html/jvms-4.html#jvms-4.7
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | info | attribute_length |
这么多类型全部叙述就太多了,我们挑选其中的2个Code和ConstanValue来解读以解释上文的疑惑。
Code
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | max_stack | attribute_length |
u2 | max_locals | 1 |
u4 | code_length | 1 |
u1 | code | code_length |
u2 | exception_table_length | 1 |
exception_info | exception_table | 1 |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
attribute_name_index是一项指向CONSTANT_Utf8_info型常量的索引,此常量值固定为“Code”,它代表了该属性的属性名称,attribute_length指示了属性值的长度,由于属性名称索引与属性长度一共为6个字节,所以属性值的长度固定为整个属性表长度减去6个字节。
max_stack代表了操作数栈(Operand Stack)深度的最大值。在方法执行的任意时刻,操作数栈都不会超过这个深度。虚拟机运行的时候需要根据这个值来分配栈帧(Stack Frame)中的操作栈深度。
max_locals代表了局部变量表所需的存储空间。在这里,max_locals的单位是变量槽(Slot),变量槽是虚拟机为局部变量分配内存所使用的最小单位。对于byte、char、float、int、short、boolean和returnAddress等长度不超过32位的数据类型,每个局部变量占用一个变量槽,而double和long这两种64位的数据类型则需要两个变量槽来存放。方法参数(包括实例方法中的隐藏参数“this”)、显式异常处理程序的参数(Exception Handler Parameter,就是try-catch语句中catch块中所定义的异常)、方法体中定义的局部变量都需要依赖局部变量表来存放。注意,并不是在方法中用了多少个局部变量,就把这些局部变量所占变量槽数量之和作为max_locals的值,操作数栈和局部变量表直接决定一个该方法的栈帧所耗费的内存,不必要的操作数栈深度和变量槽数量会造成内存的浪费。Java虚拟机的做法是将局部变量表中的变量槽进行重用,当代码执行超出一个局部变量的作用域时,这个局部变量所占的变量槽可以被其他局部变量所使用,Javac编译器会根据变量的作用域来分配变量槽给各个变量使用,根据同时生存的最大局部变量数量和类型计算出max_locals的大小。
code_length和code用来存储Java源程序编译后生成的字节码指令。code_length代表字节码长度,code是用于存储字节码指令的一系列字节流。既然叫字节码指令,那顾名思义每个指令就是一个u1类型的单字节,当虚拟机读取到code中的一个字节码时,就可以对应找出这个字节码代表的是什么指令,并且可以知道这条指令后面是否需要跟随参数,以及后续的参数应当如何解析。我们知道一个u1数据类型的取值范围为0x00~0xFF,对应十进制的0~255,也就是一共可以表达256条指令。目前,《Java虚拟机规范》已经定义了其中约200条编码值对应的指令含义,编码与指令之间的对应关系可查阅:https://docs.oracle.com/javase/specs/jvms/se11/html/jvms-6.html#jvms-6.5
关于code_length,有一件值得注意的事情,虽然它是一个u4类型的长度值,理论上最大值可以达到2的32次幂,但是《Java虚拟机规范》中明确限制了一个方法不允许超过65535条字节码指令,即它实际只使用了u2的长度,如果超过这个限制,Javac编译器就会拒绝编译。一般来讲,编写Java代码时只要不是刻意去编写一个超级长的方法来为难编译器,是不太可能超过这个最大值的限制的。但是,某些特殊情况,例如在编译一个很复杂的JSP文件时,某些JSP编译器会把JSP内容和页面输出的信息归并于一个方法之中,就有可能因为方法生成字节码超长的原因而导致编译失败。Code属性是Class文件中最重要的一个属性,如果把一个Java程序中的信息分为代码(Code,方法体里面的Java代码)和元数据(Metadata,包括类、字段、方法定义及其他信息)两部分,那么在整个Class文件里,Code属性用于描述代码,所有的其他数据项目都用于描述元数据。了解Code属性是学习后面两章关于字节码执行引擎内容的必要基础,能直接阅读字节码也是工作中分析Java代码语义问题的必要工具和基本技能,为此,笔者准备了一个比较详细的实例来讲解虚拟机是如何使用这个属性的。
我们继续分析Helloworld的类信息,入口地址0x00000150的00 00 00 1d表示此属性的长度是29, 00 01表示操作数栈的最大深度也就是1,00 01表示局部变量表的最大槽数,00 00 00 05表示字节码长度,本例为5,2a b7 00 01 b1是字节码指令,00 00表示exception_table_length长度是0,接下去00 01表示方法细节属性数量,本例是1,00 0a查常量池表得知 #10 = Utf8 LineNumberTable
,也就是表示字节码指令和Java源码行号的对应关系,具体结构可以看甲骨文官方文档。
ConstantValue
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | constantvalue_index | 1 |
为了解释上面的属性表里的ConstantValue,我们回到HelloByteCode的字节码中,入口地址ox 000001b0,上面提到了00 0a查常量池是ConstantValue,ConstantValue的属性如上表,00 00 02表示此属性的长度是2,00 04表示常量值对应在常量池的位置也就是#4 = String #28 // hello world