1. 概述
计算机只认识0和1,我们编写的程序需要经编译器翻译为由0和1构成的二进制文件才能被计算机执行。伴随着虚拟机和大量建立在虚拟机上程序语言的出现,将程序编译为本地字节码文件已不再是唯一的选择,越来越多的程序语言选择了与操作系统无关的,平台中立的格式作为程序编译后的存储格式。
2. 无关性
虚拟机提供商发布了许多可以运行在各种不同平台上的虚拟机,这些虚拟机都可以载入和执行同一种平台无关的字节码,实现了程序的”一次编写,到处运行”。各个不同平台的虚拟机与所有平台都统一使用相同的程序存储格式——字节码,它是构成平台无关性的基石。
Java虚拟机还支持语言无关性,它同时支持Java,Clojure,Groovy,JRuby,Jython,Scala等,实现语言无关性的基础仍然是虚拟机和字节码存储格式。Java虚拟机不和包含Java在内的任何语言进行绑定,它只与”class文件“这种特定的二进制文件格式关联,class文件中包含了Java虚拟机指令集和符号表以及若干其他辅助信息。基于安全方面的考虑,Java虚拟机规范要求在class文件中使用许多强制性的语法和结构化约束。Java虚拟机支持的不同的语言通过不同的编译器都可以翻译为字节码文件,最终在Java虚拟机上运行。如下图:
3. class文件结构
注意:任何一个class文件都对应着唯一一个类或接口的定义信息,但反过来说,类或接口并不一定都得定义在文件里(譬如类或接口也可以通过类加载器直接生成)
class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在class文件之中,中间没有任何分隔符,使得整个class文件中存储的内容几乎全部是程序运行的必要数据,没有空隙存在,当遇到需要占用8位字节以上空间的数据项时,则会按照高位在前的方式分割成若干个8位字节进行存储。
Big-Endian:高位在前存储方式,高位字节放在内存的低地址端,低位字节放在内存的高地址端
Little-Endian:低位在前存储方式,高位字节放在内存的高地址端,低位字节放在内存的低地址端
据Java虚拟机规范的规定,class文件格式采用类似c语言结构体的伪结构来存储数据,伪结构中只有两种数据类型无符号数和表,后面的解析都要以这两种数据类型为基础。
无符号数属于基本的数据类型,以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成的字符串值。
表是由多个无符号数或者其它表作为数据项构成的复合数据类型,所有表都习惯性地以”_info”结尾。表用于描述有层次关系的复合结构的数据,整个class文件本质上就是一张表。
无论是无符号数还是表,当需要描述同一类型但数量不定的多个数据时,经常会使用一个前置的容量计数器加若干个连续的数据项的格式,这时称这一系列连续的某一类型的数据为某一类型的集合。
3.1 魔数
每个class文件的头4个字节称为魔数,它的唯一作用是确定这个文件是否为一个能被虚拟机接受的class文件。许多文件存储标准中都使用魔数来进行身份的识别,譬如图片格式,如gif或者jpeg等在文件头中都存在有魔数。使用魔数而不是扩展名来进行识别主要是基于安全方面的考虑,因为文件扩展名可以随意地改动。文件的制定者可以自由地选择魔数值,只要这个魔数值还没有被广泛采用过同时又不会引起混淆即可。class文件的魔数值为:0xCAFEBABE
紧接着魔数的4个字节存储的是class文件的版本号:第5个和第6个字节是次版本号,第7个和第8个字节是主版本号,Java的版本号是从45开始的。JDK1.1之后的每个JDK大版本发布主版本号向上加1,高版本的JDK能向下兼容以前版本的class文件,但不能执行以后版本的class文件,即使文件格式并未发生任何变化,虚拟机也必须拒绝执行超过其版本号的class文件。如下代码:
package com.test;
public class Test{
public static void main(String[] args){
System.out.println("Hello World");
}
}
用WinHex打开对应class文件结果:
其中开头4个字节的十六进制表示的是0xCAFEBABE,代表此版本号的第5个和第6个字节值为0x0000,而主版本号的值为0x0034,也即是十进制的54
3.2 常量池
紧接着主次版本号;class文件之中的资源仓库;与其他项目关联最多的数据类型;占用class文件空间最大的数据项目之一;class文件中第一个出现的表类型数据项目。入口放置一项u2类型的数据,代表常量池计数值,从1开始;第0项空出来原因,满足后面某些指向常量池的索引值的数据在特定情况下需要表达”不引用任何一个常量池项目”,这种情况可以把索引值置为0。class文件结构中只有常量池的容量计算是从1开始,对于其他集合类型,包括接口索引集合、字段表集合、方法表集合等容量计算都从0开始。
常量池中主要放两大类常量:字面量和符号引用。字面量接近于Java语言的常量概念,如文本字符串,final修饰的常量值等。符号引用包括三类常量:类和接口的全限定名、字段的名称和描述符、方法的名称和描述符。
在class文件中不会保存各个方法、字段的最终内存布局信息,这些字段、方法的符号引用不经过运行期转换的话无法得到真正的内存入口地址,无法直接被虚拟机使用。当虚拟机运行时,需要从常量池获得对应的符号引用,再在类创建时或运行时进行解析、翻译到具体的内存地址之中。
类型 | 标志 | 描述 |
CONSTANT_Utf8_info | 1 | UTF-8编码的字符串 |
CONSTANT_Ingeter_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_MethodType_info | 16 | 标识方法类型 |
CONSTANT_InvokeDynamic_info | 18 | 表示一个动态方法调用点 |
常量 | 项目 | 类型 | 描述 |
CONSTANT_Utf8_info | tag | u1 | 值为1 |
length | u2 | UTF-8编码的字符串占用的字节数 | |
bytes | u1 | 长度为length的UTF-8编码的字符串 | |
CONSTANT_Ingeter_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 | 值为8 |
index | u2 | 指向字符串字面量项的索引 | |
CONSTANT_Fieldref_info | tag | u1 | 值为9 |
index | u2 | 指向声明字段的类或接口描述符CONSTANT_Class_info的索引项 | |
index | u2 | 指向声明字段描述符CONSTANT_NameAndType_info的索引项 | |
CONSTANT_Methodref_info | tag | u1 | 值为10 |
index | u2 | 指向声明方法的类描述符CONSTANT_Class_info的索引项 | |
index | u2 | 指向名称及类型描述符CONSTANT_NameAndType_info的索引项 | |
CONSTANT_InterfaceMethodref_info | tag | u1 | 值为11 |
index | u2 | 指向声明方法的类描述符CONSTANT_Class_info的索引项 | |
index | u2 | 指向名称及类型描述符CONSTANT_NameAndType_info的索引项
| |
CONSTANT_NameAndType_info | tag | u1 | 值为12 |
index | u2 | 指向该字段或方法名称常量项的索引 | |
index | u2 | 指向该字段或方法描述符常量项的索引
| |
CONSTANT_MethodHandle_info | reference_kind | u1 | 值必须在1~9之间(包括1和9),它决定了方法句柄的类型。方法句柄类型的值表示方法句柄的字节码行为 |
reference_index | u2 | 值必须是对常量池的有效索引 | |
CONSTANT_MethodType_info | tag | u1 | 值为16 |
descriptor_index | u2 | 值必须是对常量池的有效索引,常量池在该索引处的项必须是CONSTANT_Utf8_info结构,表示方法的描述符 | |
CONSTANT_NameAndType_info | tag | u1 | 值为18 |
bootstrap_method_attr_index | u2 | 值必须是对当前class文件中引导方法表的bootstrap_methods[]数值的有效索引 | |
name_and_type_index | u2 | 值必须是对当前常量池的有效索引,常量池在该索引处的项必须是CONSTANT_NameAndType_info结构,表示方法名和方法描述符 |
使用命令 javap -verbose com/test/Test.class 输出Test.class文件的字节码内容
Classfile /E:/com/test/Test.class
Last modified 2018-7-31; size 422 bytes
MD5 checksum 2307b9cf744b10b89d0f30adb62d1f5d
Compiled from "Test.java"
public class com.test.Test
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 // com/test/Test
#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 Test.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 com/test/Test
#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 com.test.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 2: 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 4: 0
line 5: 8
}
SourceFile: "Test.java"
3.3 访问标志
常量池结束之后,紧接着两个字节代表访问标志,用于识别一些类或接口层次的访问信息,包括:这个class是类还是接口;是否定义为public类型;是否定义为abstract类型;若是类,是否被声明为final等。
访问标志,如下图
标志名称 | 标志值 | 含义 |
ACC_PUBLIC | 0x0001 | 是否为public类型 |
ACC_FINAL | 0x0010 | 是否被声明为final,只有类可设置 |
ACC_SUPER | 0x0020 | 是否允许使用invokespecial字节码指令的新语意,invokespecial指令的语意在JDK 1.0.2发生过改变,为了区别这条指令使用哪种语意,JDK 1.0.2之后编译出来的类的这个标志都必须为真 |
ACC_INTERFACE | 0x0200 | 标识这是一个接口 |
ACC_ABSTRACT | 0x0400 | 是否为abstract类型,对于接口或者抽象类来说,此标志值为真,其它类型为假 |
ACC_SYNTHETIC | 0x1000 | 标识这个类并非由用户代码产生 |
ACC_ANNOTATION | 0x2000 | 标识这是一个注解 |
ACC_ENUM | 0x4000 | 标识这是一个枚举 |
如下类,对应的访问标志为
public abstract class Demo02{
public static void main(String[] args){
System.out.println("Demo02");
}
}
因其为public修饰的抽象类,所以访问标识对应为:0x0400+0x0020+0x0001=0x0421
3.4 类索引、父类索引与接口索引集合
类索引和父类索引都是一个u2类型的数据,而接口索引集合是一组u2类型的数据的集合,class文件中由这三项数据确定这个类的继承关系。类索引用于确定类的全限定名,父类索引用于确定类的父类的全限定名(Java的单继承特性,父类索引只有一个,除java.lang.Object外,其它类都有父类,父类索引都不为0)。接口索引集合用来描述类实现了那些接口,被实现的接口将按implements或extends后的接口顺序从左到右排列在接口索引集合。
类索引和父类索引引用两个u2类型的索引值表示,各自指向一个类型为CONSTANT_Class_info的类描述符常量,通过CONSTANT_Class_info类型的常量中的索引值找到定义在CONSTANT_Utf-8_info类型的常量中的全权限名字符串。接口索引集合,第一项 u2类型的数据为接口计数器,表示索引表的容量。若该类没有实现任何借口,则计数器值为0,后面接口的索引表不再占用任何字节。
由之前winhex查看class文件可知:类索引为0x0005,父类索引为0x0006,接口索引计数器为0x0000
据字节码内容得到如下信息:
#5 = Class #21 // com/test/Test
#6 = Class #22 // java/lang/Object
#21 = Utf8 com/test/Test
#22 = Utf8 java/lang/Object
由此可看到这个class的类全限定名,父类全限定名
3.5 字段表集合
字段表用于描述接口或者类中声明的变量。包括类级变量以及实例级变量,但不包括在方法内部声明的局部变量。包括的信息由:字段的作用域(public,private,protected)、是实例变量还是类变量(static修饰符)、可变性(final)、并发可见性(volatile修饰符,是否强制从主内存读写)、可否被序列化(transient修饰符)、字段数据类型(基本类型、对象、数组)、字段名称。修饰符都是布尔值;字段名,字段数据类型引用常量池中的常量来描述。
类型 | 名称 | 数量 |
u2 | access_flags | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | attributes_count | 1 |
attribate_info | attributes | attributas_count |
由字段表结构可以知道,它使用3个连续的u2数据类型存储
access_flags是字段的访问标识
name_index和descriptor_index,都是对常量池的引用,分别代表字段的简单名称和字段与方法的描述符
字段的简单名称就是类中的字段名和方法名
描述符的作用:用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值
标志名称 | 标志值 | 含义 |
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修饰 |
由Java本身的语言规则决定:ACC_PUBLIC、ACC_PRIVATE、ACC_PROTECTED中只能选一个;ACC_FINAL、ACC_VOLATILE不能同时选择;接口中字段必须有ACC_PUBLIC、ACC_STATIC、ACC_FINAL标志。
标识字符 | 含义 |
B | 基本类型 byte |
C | 基本类型 char |
D | 基本类型 double |
F | 基本类型 float |
I | 基本类型 int |
J | 基本类型 long |
S | 基本类型 short |
Z | 基本类型 boolean |
V | 特殊类型 void |
L | 对象类型,如Ljava/lang/Object |
字段表集合中不会列出从超类或者父类接口中继承而来的字段,但有可能列出原本Java代码中不存在的字段,譬如在内部类中为了保持对外部类的访问性,会自动添加指向外部类实例的字段。Java语言中不管两个字段的数据类型、修饰符是否相同,都不能使用一样的名称;但对于字节码而言,两个字段的描述符不一致,字段名可以相同。
3.6 方法表集合
方法的描述与字段的描述几乎采用了完全一致的方式,方法表的结构如同字段表一样,依次包括了访问标志(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;sysnchronized、native、strictfp和abstract可以修饰方法,因此方法表的访问标志中添加了ACC_SYNCHRONIZED、ACC_NATIVE、ACC_STRICTFP和ACC_ABSTRACT标志。
标志名称 | 标志值 | 含义 |
ACC_PUBLIC | 0x0001 | 方法是否被public修饰 |
ACC_PRIVATE | 0x0002 | 字方法是否被private修饰 |
ACC_PROTECTED | 0x0004 | 方法是否被protected修饰 |
ACC_STATIC | 0x0008 | 方法是否被static修饰 |
ACC_FINAL | 0x0010 | 方法是否被final修饰 |
ACC_SYNCHRONIZED | 0x0020 | 方法是否被sysnchronized修饰 |
ACC_BRIDGE | 0x0040 | 方法是否是由编辑器产生的桥接方法 |
ACC_VARARGS | 0x0080 | 方法是否接受不定参数 |
ACC_NATIVE | 0x0100 | 方法是否被native修饰 |
ACC_ABSTRACT | 0x0400 | 方法是否被abstract修饰 |
ACC_STRICTFP | 0x0800 | 方法是否被strictfp修饰 |
ACC_SYNTHETIC | 0x1000 | 方法是否是由编译器自动产生的 |
如果父类方法在子类中没有被重写,方法表集合中就不会出现来自父类的方法信息,可能会出现编译器自动添加的方法,如类构造器”<clinit>”,方法和实例构造器”<init>”方法。
Java中重载方法,要与原方法具有相同的简单名称,与原方法不同的特征签名;在对应的class文件中,特征签名就是方法各个参数在常量池中的字段符号引用的集合,但返回值不在其中,因此Java里无法仅仅依靠返回值对一个方法进行重载。但在class文件中,两个方法名称和特征签名相同,返回值不同的方法,可以合法存在与同一个class文件中。
3.7 属性表集合
class文件、字段表、方法表都可以携带自己的属性表集合,用于描述某些场景专有的信息;不要求各个属性表具有严格顺序,只要不与已有属性名重复,任何人实现的编译器都可以向属性表中写入自己的定义的属性信息,JVM运行时会忽略掉它不认识的属性。
类型 | 名称 | 数量 |
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u1 | info | attribute_length |
3.7.1 Code属性
方法体中代码经Javac编译后,变为字节码指令存储在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_lebth |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
attribute_name_index:指向CONSTANT_Utf8_info型常量的索引,常量值固定为”Code”,代表了该属性的属性名称
attribute_length:属性值的长度,由于属性名称索引与属性长度共6个字节,所以固定为整个属性表的长度减6个字节
max_stack:代表了操作数栈深度的最大值。JVM运行时需要根据它来分配栈帧中的操作栈深度
max_locals:代表局部变量表所需的存储空间。单位时Slot,Slot是JVM为局部变量分配内存所使用的最小单位。byte、char、float、int、short、boolean和returnAddress等长度不超过32为的数据类型,每个局部变量占用1个Slot;double和long两种64位的数据类型占用2个Slot。方法参数(包括实例方法中隐藏参数”this“)、显示异常处理器的参数(Exception Handler Parameter,就是try-catch语句中catch块所定义的异常)、方法体中定义的局部变量都需要使用局部变量表来存放。
code_length和code用来存储Java源程序编译后生产的字节码指令。code_length代表字节码长度,code是用于存储字节码指令的一系列字节流。字节码指令,占用1个字节;字节码指令后面可以跟随参数。code_length是u4类型的长度值,理论最大值为Java中int的最大值,但JVM明确限制其不超过65535,即实际只使用u2的长度。若超过65535了,javac则会拒绝编译。
若将Java程序分为代码(Code,方法体中的代码)和元数据(Metadata,包括类、字段、方法定义及其他信息)两部分,那么Code属性用于描述代码,所有的其他数据项目都用与描述元数据。
在任何实例方法中,都可以通过”this”关键字访问到此方法所属的对象,原因是javac编译器在编译时把this关键字的访问转变为对一个普通方法参数的访问,然后在JVM调用实例方法时自动传入此参数。所以在实例方法的局部变量表中至少存在一个指向当前对象实例的局部变量,局部变量表这也会预留出第一个Slot为来存放对象实例的引用,方法参数值从1开始计算。以上处理只对实例方法有效,若将inc方法声明为static,Args_size则为0。
3.7.2 Exceptions属性
Exceptions属性在方法表中与Code属性平级,列出了方法中可能抛出的手查异常,也就是方法描述是throws关键字后面列举的异常。Exceptions属性中,number_of_exceptions项表示方法可能抛出number_of_exceptions种受查异常;每种受查异常用一个exception_index_table项表示,exception_index_table是一个指向常量池中CONSTANT_Class_info型常量的索引,代表了该受查异常的类型。
类型 | 名称 | 数量 |
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | number_of_exceptions | 1 |
u2 | exception_index_table | number_of_exceptions |
3.7.3 LineNumberTable属性
LineNumberTable属性用于描述Java源码行号与字节码行号(字节码的偏移量)之间的对应关系,不是运行时必需的属性,默认会生成到class文件中,可在javac中使用-g:none或g:lines选项来取消或要求生成这项信息。若没有生成LineNumberTable属性,在程序运行抛出异常时,堆栈中将不会显示出错的行号;并在调试程序时,无法按照源码行进行设置断点。line_number_table是一个数量为line_number_table_length、类型为line_number_info的集合,line_number_info表包括start_pc和line_number两个u2类型的数据项,前者是字节码行号,后者是Java源码行号。
类型 | 名称 | 数量 |
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 |
3.7.4 LocalVariableTable属性
LIneNumberTable属性用于描述栈帧中局部变量表中的变量与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属性分别代表了这个局部变量的生命周期开始的字节码偏移量及其作用范围覆盖的长度,两者结合起来就是这个局部变量在字节码中的作用域范围。name_index和descriptor_index都是指向常量池中CONSTANT_Utf8_info型常量的索引,分别代表了局部变量的名称以及局部变量的描述符。index是这个局部变量在栈帧局部变量表中Slot的位置,若这个变量是double或long类型的,它占用的Slot为index和index+1两个。
在JDk1.5引入泛型之后,增加了LocalVariableTypeTable属性,与LocalVariableTable非常相似,仅把字段描述符descriptor_index替换成了字段特征签名Signfure;对于非泛型来说,二者描述的信息基本一致;但泛型引入之后描述符就不能准确地描述泛型类型。
3.7.5 SourceFile 属性
SourceFile属性用于记录class文件的源码文件名称,可选的,定长的,可以分别使用javac的-g:none和-g:source选项来关闭或要求生成这项信息。在Java中,大多数类的class文件名与源码文件名相同,但有特殊情况(如内部类,class未被public修饰)。若没有生产这项属性,当抛出异常时,堆栈中将不会显示出错代码所属的文件名。
类型 | 名称 | 数量 |
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | sourcefile_index | 1 |
sourcefile_index数据项时指向常量池中CONSTANT_Utf8_info型常量的索引,常量值是源码文件的文件名。
3.7.6 ConstantValue属性
ConstantValue属性是通知JVM自动为静态变量赋值。只有被static修饰的变量(类变量)才可以使用这项属性。
JVM对”int x = 123“和”static int x = 123″两种变量赋值的方式和时刻是不同的,对于非static类型的变量(实例变量)的赋值是在实例构造器<init>方法中进行的;对于类变量,有两种方式可以选择:在类构造器<clinit>方法中或者使用ConstantValue属性;目前Sun Javac编译器的选择是:若同时使用static和final修饰变量(常量),并且这个变量的数据类型是基本类型或者java.lang.String,就生成ConstantValue属性来进行初始化;若变量没有被fina修饰,或者并非基本类型及字符串,则会选择在<clinit>方法中进行初始化。
类型 | 名称 | 数量 |
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常量中的一种。
3.7.7 InnerClasses属性
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表进行描述。
类型 | 名称 | 数量 |
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_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_SYNITHETIC | 0x1000 | 内部类是否并非由用户代码产生的 |
ACC_ANNOTATION | 0x2000 | 内部类是否是一个注解 |
ACC_ENUM | 0x4000 | 内部类是否是enum |
3.7.8 Deprecated及Synthetic属性
Deprecated和Ssnthetic都属于标志类型的布尔属性,只存在有和没有的区别,没有属性值的概念。
Deprecated属性用于表示某个类,字段或方法,已经不再推荐使用,可以通过在代码中使用@deprecated注释进行设置
Syntheti属性代表次字段或方法并不是由Java源码直接产生的,而是由编译器自行添加的。JDK1.5之后,标识一个类、字段、或者方法是编译器自动产生的,也可以设置它的访问标志中的ACC_SYNTHETIC标志位。所有由非用户代码产生的类、方法及字段都应当至少设置Synthetic属性和ACC_SYNTHETIC标志位中的一项,实例构造器”<init>”方法和类构造器”<clinit>”方法除外。
类型 | 名称 | 数量 |
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
attribute_length数据项的值必须为0x00000000,因为没有任何属性值需要设置。
3.7.9 StackMapTable属性
StackMapTable属性在JDK1.6后增加到class文件规范中,是一个复杂的变长属性,位于Code属性的属性表中。会在虚拟机类加载的字节码验证阶段被新类型检查验证器,代替提前消耗性能的基于数据流分析的类型推导验证器。StackMapTable属性中包含零至多个栈帧,每个栈映射栈帧都显示或隐式地代表一个字节码偏移量,用于表示执行到该字节码时局部变量表和操作数栈的验证类型。类型检查器会通过检查目标方法的局部变量和操作数栈所需要的类型来确定一段字节码指令是否符合逻辑约束。
类型 | 名称 | 数量 |
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | number_of_entries | 1 |
stack_map_frame | stack_map_frame_entries | number_of_entries |
《Java虚拟机规范(Java SE 7版)》明确规定:在版本号大于或等于50.0的class文件中,如果方法的Code属性没有附带StackMapTable属性,它将带有一个隐式的StackMapTable属性。它等同于number_of_erntries值为0的StackMapTable属性。一个方法的Code属性最多只能有一个StackMapTable属性,否则将抛出ClassFormatError异常。
3.7.10 Signature属性
JDK1.5后增加的,可选的定长属性,可以出现在类、属性表和方法表结构的属性表中,与泛型有关。
类型 | 名称 | 数量 |
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | signature_length | 1 |
signature_length项的值必须是一个对常量池的有效索引,且必须是CONSTANT_Utf8_info结构,表示类的签名,方法类型签名或字段类型签名。
3.7.11 BootstrapMethods属性
JDK1.7后增加的,复杂的变长属性,位于类文件的属性表中,用于保存invokedynamic指令的引导方法限定符。与JSR-292中的InvokeDynamic指令、java.lang.Invoke包关系密切。
4. 字节码指令简介
Java虚拟机指令由一个字节长度的、代表着某种特定操作含义的数字(称为操作码)以及跟随其后的零至多个代表此操作所需参数(操作数)构成。Java虚拟机采用面向操作数栈而不是寄存器的架构,大多数的指令都补包含操作数,只有一个操作码。由于限制了Java虚拟机操作码的长度为1个字节,意味着指令集的操作码总数不可能超过256条。
4.1 字节码与数据类型
Java虚拟机的指令集中,大多数的指令都包含了其操作所对应的数据类型信息。i代表对int类型的数据类型操作、l代表long、s代表short、b代表byte、c代表char、f代表float、d代表double、a代表reference
4.2 加载和存储指令
用于将数据在栈帧中的局部变量表和操作数栈之间来回传输。
将一个局部变量加载到操作栈:iload、iload_<n>、lload、lload_<n>、fload、fload_<n>、dload、dload_<n>、aload、aload_<n>。
将一个数值从操作数栈到局部变量表:istore、istore_<n>、lstore、lstore_<n>、fstore、fstore_<n>、dstore、dstore_<n>、astore、astore_<n>。
将一个常量加载到操作数栈:bipush、sipush、ldc、ldc_w、ldc2_w、aconst_null、iconst_m1、iconst_<i>、lconst_<l>、fconst_<f>、dconst_<d>。
扩充局部变量表的访问索引的指令:wide。
以尖括号结尾的,如iload_<n>,实际代表了一组指令(iload_0、iload_1、iload_2和iload_3),都是某个带有操作数的通用指令(例如iload)d的特殊形式,省略掉显式的操作数,不需要进行取操作数的动作,实际操作数隐藏在指令中,例如iload_0的语义与操作数为0时的iload指令完全一致。
4.3 运算指令
运算符或算术指令用于两个操作数栈上的值进行某种特定的运算,并把结果重新存入到操作栈帧。算术指令分为两种:对整数型数据进行运算的指令与对浮点数型数据进行运算的指令,它们都使用Java虚拟机的数据类型,由于,欸有直接支持buye、short、char和boolean类型的算术指令,对这类数据类型的运算,应使用操作int类型的指令代替。整数与浮点数在溢出和被零除的时候有各自不同的行为表现。
加法指令:iadd,ladd,fadd,dadd。
减法指令:isub,lsub,fsub,dsub。
乘法指令:imul,lmul,fmul,dmul。
除法指令:idiv,ldiv,fdiv,ddiv。
求余指令:irem,lrem,frem,drem。
取反指令:ineg,lneg,fneg,dneg。
位移指令:ish1,ishr,iushr,lshl,lshr,lushr。
按位或指令:ior,lor。
按位与指令:iand,land。
按位异或指令:ixor,lxor。
局部变量自增指令:iinc。
比较指令:dcmpg,dcmpl,fcmpg,fcmpl,lcmp。
Java虚拟机规定在处理整型数据时,只有除法(idiv和ldiv)以及求余指令(irem和lrem)中当出现除数为零时会导致虚拟机抛出ArithmeticException异常,其余任何整型数运算场景都不应该抛出运行时异常。
Java虚拟机必须完全支持IEEE 754中定义的非正规浮点数值和逐级下溢的运算规则。Java虚拟机在处理浮点数运算时,不会抛出任何运行时异常(Java语言中的异常),当一个人操作产生溢出时,将会使用有符号的无穷大来表示;如果走个操作结果没有明确的数学定义,将会使用NaN值表示。所有使用NaN值作为操作数的算术操作,结果都会返回NaN。
4.4 类型转换指令
类型转换指令可以将两种不同的数值类型进行相互转换,一般用于实现用户代码中的显示类型转换操作,或者用来处理字节码指令集中类型相关指令无法与数据类型一一对应的问题。
Java虚拟机直接支持(即转换时无需显式的转换指令)一下数值类型的宽化类型转换(即小范围类型子昂大范围类型的安全转换):
int 类型到long、float、或者double类型。
long类型到float、double类型。
float类型到double。
处理窄化类型转换时,必须显式地使用转换指令完成,包括:i2b、i2c、i2s、l2i、f2i、f2l、d2i、d2l和d2f。窄化类型转换可能会导致转换结果产生不同地正负号,不同的数值级的情况,转换过程很可能会导致数值的精度丢失。
将int或long类型窄化转换为整数类型T的时候,转换过程仅仅是简单地丢弃除最低位N个字节以外地内容,N值类型T地数据类型长度。并且得到是转换后结果地补码形式。
浮点值窄化转换为整数类型T(T限于int或long类型之一)时,将遵循:如果浮点值时NaN,转换结果时int或long类型的0。如果浮点值不是无穷大,浮点值使用IEEE 754的项零舍入模式取整,或者整数值v,如果v在目标类型T(int或long)的表示范围之内,转换结果就是v;否则,将根据v的符号,转换为T所能表示的最大或者最小数。如果浮点值时Infinity,则转换结果为对应T类型的最大值或最小值。
Java虚拟机规范中明确规定数值类型的窄化转换指令永远不可能导致虚拟机抛出运行时异常。
4.5 对象创建与访问指令
Java虚拟机对类实例和数组的创建与操作使用了不同的字节码指令。,对象创建后,可以通过对象访问指令获取对象实例或许数组实例中三位字段或者数组元素。
创建类实例的指令:new。
常见数组的指令:newarray、anewarray、multianewarray。
访问类字段(static字段,类变量)和实例字段(非static字段,实例变量)的指令:getstatic、putstatic;getfield、putfield。
把一个数组元素加载到操作数栈的指令:baload、caload、saload、iaload、laload、faload、daload、aaload。
将一个操作数栈的值存储到数组元素中的指令:bastore、castore、sastore、iastore、fastore,dastore,aastore。
取数组长度的指令:arraylength。
检查类实例类型的指令:instanceof、checkcast。
4.6 操作数栈管理指令
Java虚拟机提供了一些用于直接操作操作数栈的指令。
将操作数栈的栈顶一个或两个元素出栈:pop、pop2。
复制栈顶一个或两个数值并将复制值或双份的复制重新压入栈顶:dup、dup2、dup_x1、dup2_x1、dup_x2、dup1_x2。
将栈最顶端的两个数值互换:swap。
4.7 控制转移指令
可以让Java虚拟机有条件或无条件地指定的位置指令而不是控制转移指令的下一条指令继续执行程序。可以认为控制转移指令就是在有条件或无条件地修改PC寄存器地值。
条件分支:ifeq、iflt、ifle、ifne、ifgt、ifge、ifnull、ifnonnull、if_icmpeq、if_icmpne、if_icmplt、if_icmpgt、if_icmple、if_icmpge、if_acmpeq和if_acmpne。
复合条件分支:tableswitch、lookupswitch。
无条件分支:goto、goto_w、jsr、jsr_w、ret。
对于boolean类型、byte类型、char类型和shprt类型地条件分支比较操作,都是使用int类型地比较指令完成的;对于long类型、float类型和double类型的条件分支比较操作,则会先执行相应类型的比较运算指令(dcmpg、dcmpl、fcmpg、fcmpl、lcmp),运算指令会返回一个整型值到操作数栈中,再执行int类型的条件分支比较操作完成整个分支跳转。
4.8 方法调用和返回指令
invokevirtual指令用于调用对象的实例方法,根据对象的实例类型进行分派(虚方法分派),这也是Java语言中最常见的方法分派方式。
invokeinterface指令用于调用接口方法,它会再运行时搜索一个实现了这个接口方法的对象,找出适合的方法进行调用。
invokespecial指令用于调用一些需要特殊处理的实例方法,包括实例初始化方法,私有方法和分类方法。
invokestatic指令用于调用类方法(static方法)。
invokedynamic指令用于在运行时动态解析出调用点限定符所引用的方法,并执行该方法。
方法调用指令与数据类型无关,二方法返回指令时根据返回值的类型区分的,包括ireture(当返回值时boolean,byte,char,short和int类型时使用)、lreturn、freture、dreturn和areturn,return指令供声明为void的方法、实例初始化方法以及类和接口的类初始化方法使用。
4.9 异常处理指令
Java程序中显式抛出异常的操作(throw语句)都是由athrow指令来实现的;除用throw显式抛出异常外,Java虚拟机规定许多运行时异常会在其他Java虚拟机指令检测到异常状况时自动抛出。
在Java虚拟机中,处理异常不是由字节码指令来实现的,而是采用异常表来完成的。
4.10 同步指令
Java虚拟机支持方法级的同步和方法内部一段指令序列的同步,都是使用管程来支持的。Java中同步一段代码指令序列通常是由synchronized语句块来表示,Java虚拟机的指令集中有monitorenter和monitorexit两条指令支持synchronized关键字的语义。