Sun推出Java语言时,一句展示其跨平台特性的口号:WORA (Write Once Run Anywhere)。
刚推出时,主要是指编写一次Java文件,就可以在各个环境中运行。
随着时间的流逝,越来越多的语言被改编或设计运行在JVM上。除了java语言,比较知名的JVM上的编程语言还有:
class文件是一种8位字节的二进制流文件, 各个数据项按顺序紧密的从前向后排列, 相邻的项之间没有间隙, 这样可以使得class文件非常紧凑, 体积轻巧, 可以被JVM快速的加载至内存, 并且占据较少的内存空间。我们的Java源文件, 在被编译之后, 每个类(或者接口)都单独占据一个class文件, 并且类中的所有信息都会在class文件中有相应的描述, 由于class文件很灵活, 它甚至比Java源文件有着更强的描述能力。
class文件中的信息是一项一项排列的, 每项数据都有它的固定长度, 有的占一个字节, 有的占两个字节, 还有的占四个字节或8个字节, 数据项的不同长度分别用u1, u2, u4, u8表示, 分别表示一种数据项在class文件中占据一个字节, 两个字节, 4个字节和8个字节。可以把u1, u2, u3, u4看做class文件数据项的“类型” 。对于字符串,则使用u1数组进行表示。
数据类型 | 含义 |
---|---|
u1 | 无符号单字节整数 |
u2 | 无符号2字节整数 |
u4 | 无符号4字节整数 |
u8 | 无符号8字节整数 |
u1数组 | 字符串 |
Class文件的结构严格按照该结构体的定义:
类型 | 数量 | 名称 | 中文含义 |
---|---|---|---|
u4 | 1 | magic | 魔数 |
u2 | 1 | minor_version | 小版本号 |
u2 | 1 | major_version | 大版本号 |
u2 | 1 | constant_pool_count | 常量数 |
cp_info | constant_pool_count - 1 | constant_pool | 常量池 |
u2 | 1 | access_flags | 访问标记 |
u2 | 1 | this_class | 当前类 |
u2 | 1 | super_class | 父类 |
u2 | 1 | interfaces_count | 实现的接口数 |
u2 | interfaces_count | interfaces | 接口列表 |
u2 | 1 | fields_count | 字段个数 |
field_info | fields_count | fields | 字段列表 |
u2 | 1 | methods_count | 方法个数 |
method_info | methods_count | methods | 方法列表 |
u2 | 1 | attribute_count | 属性个数 |
attribute_info | attributes_count | attributes | 属性列表 |
魔数(MagicNumber)作为Class文件的标志,用来告诉Java虚拟机,这是一个Class文件。魔数是一个4字节的无符号整数,它固定为0xCAFEBABE。谐音 “Cafe Baby”。
我们先创建一个空的Java类
package bytecode;
/**
* 这是一个空的Java类
*
* @author liaojunyong
*/
public class EmptyClass {
}
看看生成的 class 文件的内容
在魔数后面,紧跟着Class的小版本号和大版本号。这表示当前Class文件是由哪个版本的编译器编译产生的。首先出现的是小版本号,是一个2字节的无符号整数,在此之后为大版本号,也用2字节表示。
Class文件的版本号和Java编译器的对应关系:
大版本(十进制) | 大版本(HEX) | 小版本 | java版本 |
---|---|---|---|
45 | 2D | 3 | 1.1 |
46 | 2E | 0 | 1.2 |
47 | 2F | 0 | 1.3 |
48 | 30 | 0 | 1.4 |
49 | 31 | 0 | 1.5(5) |
50 | 32 | 0 | 1.6(6) |
51 | 33 | 0 | 1.7(7) |
52 | 34 | 0 | 1.8(8) |
53 | 35 | 0 | 9 |
54 | 36 | 0 | 10 |
55 | 37 | 0 | 11 |
56 | 38 | 0 | 12 |
57 | 39 | 0 | 13 |
58 | 3A | 0 | 14 |
回到刚才的class文件查看版本,可以看到版本号是 0000 0034,对应java版本是8。
向下兼容,否则报错
如果低版本JVM运行高版本class文件,会抛出异常。比如,JVM7运行编译版本为8的class。
java.lang.UsupportedClassVersionError: Unsupported major.minor version 52.0
以Maven为例
% mvn -version
Apache Maven 3.6.2 (40f52333136460af0dc0d7232c0dc0bcf0d9e117; 2019-08-27T23:06:16+08:00)
Maven home: /usr/local/Cellar/maven/3.6.2/libexec
Java version: 13, vendor: Oracle Corporation, runtime: /jdk-13.jdk/Contents/Home
Default locale: zh_CN_#Hans, platform encoding: UTF-8
OS name: "mac os x", version: "10.15.1", arch: "x86_64", family: "mac"
如果电脑上装了多个jdk,有可能同java -version不一致。
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.0</version>
<configuration>
<source>13</source>
<target>13</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
% mvn clean compile
一个额外的话题:应该用什么版本编译?
常量池是Class文件中内容最丰富的区域之一。随着Java虚拟机的不断发展,常量池的内容也日渐丰富。同时,常量池对于Class文件中的字段和方法解析也有至关重要的作用,可以说,常量池是整个Class文件的基石。在版本号之后,紧跟着的是常量池的数量,以及若干个常量池表项。
刚才的EmptyClass字节码,可以看到常量个数为 0010=16。
常量池类型 | TAG | 常量池类型 | TAG |
---|---|---|---|
CONSTANT_Class | 7 | CONSTANT_Fieldref | 9 |
CONSTANT_Methodref | 10 | CONSTANT_InterfaceMethodref | 11 |
CONSTANT_String | 8 | CONSTANT_Integer | 3 |
CONSTANT_Float | 4 | CONSTANT_Long | 5 |
CONSTANT_Double | 6 | CONSTANT_NameAndType | 12 |
CONSTANT_Utf8 | 1 | CONSTANT_MethodHandle | 15 |
CONSTANT_MethodType | 16 | CONSTANT_InvokeDynamic | 18 |
常量池底层的数据类型:CONSTANT_Utf8、CONSTANT_Integer、CONSTANT_Float、CONSTANT_Long、CONSTANT_Double。
CONSTANT_Utf8_info {
u1 tag;
u2 length;
u1 bytes[length];
}
CONSTANT_Integer_info {
u1 tag;
u4 bytes;
}
CONSTANT_Float_info {
u1 tag;
u4 bytes;
}
CONSTANT_Long_info {
u1 tag;
u4 high_bytes;
u4 low_bytes;
}
CONSTANT_Double_info {
u1 tag;
u4 high_bytes;
u4 low_bytes;
}
CONSTANT_String_info {
u1 tag;
u2 string_index;
}
会引用其他 UTF8 常量
CONSTANT_Class_info {
u1 tag;
u2 name_index;
}
CONSTANT_NameAndType_info {
u1 tag;
u2 name_index;
u2 decription_index;
}
CONSTANT_Methodref_info {
u1 tag;
u2 class_index;
u2 name_and_type_index;
}
CONSTANT_Fieldref_info {
u1 tag;
u2 class_index;
u2 name_and_type_index;
}
CONSTANT_Fieldref_info {
u1 tag;
u2 class_index;
u2 name_and_type_index;
}
CONSTANT_MethodType_info {
u1 tag;
u2 description_index;
}
CONSTANT_InvokeDynamic_info {
u1 tag;
u2 bootstrap_method_attr_index; // 定位到一个引导方法
u2 name_and_type_index;
}
它可以用来表示方法、类的字段或者构造函数等。方法句柄指向一个方法、字段,和C语言中的函数指针或者C#中的委托有些类似。
CONSTANT_MethodHandle_info {
u1 tag;
u2 reference_kind;
u2 reference_index;
}
Index | HEX | 常量类型 | 值 | 引用1 | 引用2 |
---|---|---|---|---|---|
1 | 0A 0002 0003 | Methodref | 2 | 3 | |
2 | 07 0004 | Class | 4 | ||
3 | 0C 0005 0006 | NameAndType | 5 | 6 | |
4 | 01 0010 6A...74(16字符) | Utf8 | java/lang/Object | ||
5 | 01 0006 3C696E69743E | Utf8 | <init> | ||
6 | 01 0003 282956 | Utf8 | ()V | ||
7 | 07 0008 | Class | 8 | ||
8 | 01 0013 62...73(19字符) | Utf8 | bytecode/EmptyClass | ||
9 | 01 0004 436F6465 | Utf8 | Code | ||
10 | 01 000F 4C...65(15字符) | Utf8 | LineNumberTable | ||
11 | 01 0012 4C...65(18字符) | Utf8 | LocalVariableTable | ||
12 | 01 0004 74686973 | Utf8 | this | ||
13 | 01 0015 4C...3B(21字符) | Utf8 | Lbytecode/EmptyClass; | ||
14 | 01 000A 53...65(10字符) | Utf8 | SourceFile | ||
15 | 01 000F 45...61(15字符) | Utf8 | EmptyClass.java |
图示
Java 编译器在编译每个类时都会为该类至少生成一个实例初始化方法--即 "()" 方法。此方法与源代码中的每个构造方法相对应,如果类没有明确地声明任何构造方法,编译器则为该类生成一个默认的无参构造方法,这个默认的构造器仅仅调用父类的无参构造器,与此同时也会生成一个与默认构造方法对应的 "()" 方法.
在类中添加一个无参数构造函数,再重新看看生成的常量池。
package bytecode;
/**
* 这是一个空的Java类
*
* @author liaojunyong
*/
public class EmptyClass {
/**
* 缺省构造函数
*/
public EmptyClass() {
super();
}
}
可以看到常量池的个数没有变化,仍然只有一个 Methodref,引用指向 <init>()。
这个类有属性、有函数
package bytecode;
/**
* 这是一个简单的Java类
*
* @author liaojunyong
*/
public class PageClass {
/** 定义常量 */
private static final int size = 10;
/** 当前第几页 */
private int page;
/** 构造函数 */
public PageClass(int page) {
this.page = page;
}
/** 计算偏移量 */
public int calculateOffset() {
return page * size;
}
/** 程序入口 */
public static void main(String[] args) {
PageClass page = new PageClass(3);
System.out.println(page.calculateOffset());
}
}
打开class看二进制内容,常量池中常量个数已经变成 002D(45)
整理后的常量池对象(白色背景是 EmptyClass 也有的常量)
在常量池后,紧跟着访问标记。该标记使用2字节表示,用于表明该类的访问信息,如public、final、abstract等。从表可以看出,每一种类型的表示都是通过设置访问标记32位中的特定位来实现的。比如,若是publicfinal的类,则该标记为ACC_PUBLIC|ACC_FINAL。
标记名称 | 数值 | 描述 |
---|---|---|
ACC_PUBLIC | 0x0001 | public 类 |
ACC_FINAL | 0x0010 | final 类 |
ACC_SUPER | 0x0020 | 使用增强的方法调用父类(?) |
ACC_INTERFACE | 0x0200 | 是否为接口 |
ACC_ABSTRACT | 0x0400 | 是否为抽象类 |
ACC_SYNTHETIC | 0x1000 | 由编译器产生的类,没有源码 |
ACC_ANNOTATION | 0x2000 | 是否为注解 |
ACC_ENUM | 0x4000 | 是否为枚举 |
看看我们PageClass的访问标记,0021,public
在访问标记后,会指定该类的类别、父类类别及实现的接口,格式如下:
u2 : 当前类 (指向常量池中的Class)
u2 : 父类 (指向常量池中的Class)
u2 : 实现的接口数 (实现的接口个数,没有实现接口为0)
u2 : 接口数组 (指向常量池中的Class,必须是接口)
没有实现任何接口的PageClass相关数据。
changd | 数据长度 | HEX | 对应常量值 |
---|---|---|---|
当前类 | u2 | 0008 | bytecode/PageClass |
父类 | u2 | 0002 | Java/lang/Object |
接口个数 | u2 | 0000 | 0 |
为了看看实现了接口后的数据,为PageClass增加两个接口 Serializable, Comparable<PageClass>,重新编译。代码
public class PageClass implements Serializable, Comparable<PageClass>{...}
字节码变化,原来的 0000 变成了 0002 0023 0025。(增加了接口,常量池也会发生变化)
page-class-class2
changd | 数据长度 | HEX | 对应常量值 |
---|---|---|---|
当前类 | u2 | 0008 | bytecode/PageClass |
父类 | u2 | 0002 | Java/lang/Object |
接口个数 | u2 | 0002 | 2 |
接口数组 | u2*2 | 0023 0025 | java/io/Serializable java/lang/Comparable |
后续内容会包括各种属性表,先做一些介绍。
属性表(attribute_info),在Class文件、字段表、方法表都可以携带自己的属性表集合,以用于描述某些场景专有的信息。与Class文件中其他的数据项目要求严格的顺序、长度和内容不同,属性表集合的限制稍微宽松了一些,不再要求各个属性表具有严格顺序,并且只要不与已有属性名重复,任何人实现的编译器都可以向属性表中写入自己定义的属性信息,Java虚拟机运行时会忽略掉它不认识的属性。
方法也可以附带若干个属性,用于描述一些额外信息,比如方法字节码等,attributes_count表示该方法中属性的数量,紧接着就是attributes_count个属性的描述。对于属性来说,它们的统一格式为:
attribute_info {
u2 : attribute_name_index; (当前attribute的名称)
u4 : attribute_length; (当前attribute的长度)
u1 : info[attribute_length]; (当前attribute的值)
}
下面是一些常见属性。
属性名称 | 使用位置 | 含义 |
---|---|---|
Code | 方法表 | Java代码编译成的字节码指令 |
ConstantValue | 字段表 | final关键字定义的常量值 |
Deprecated | 类、方法表、字段表 | 被声明为deprecated的类、方法、字段 |
Exceptions | 方法表 | 方法抛出的异常 |
EnclosingMethod | 类文件 | 一个类为局部类或匿名类时才有这个属性,用于标示这个类所在的外围方法 |
InnerClasses | 类文件 | 内部类列表 |
LineNumberTable | Code属性 | Java源码的行号与字节码的对应关系 |
LocalVariableTable | Code属性 | 方法的局部变量描述 |
StackMapTable | Code属性 | 供类型检查验证器(Type Checker)检查和处理目标方法的局部变量和操作数栈所需要的类型是否匹配。 |
Signature | 类、方法表、字段表 | 支持范型情况下的方法签名 |
SourceFile | 类文件 | 源文件名称 |
SourceDebugExtension | 类文件 | 用于存储额外的调试信息,比如使用JSP开发,可以用于记录JSP的行号 |
Synthetic | 类、方法表、字段表 | 表示方法或字段为编译器自动生成的 |
LocalVariableTypeTable | 类 | 使用特征签名代替描述符,用于描述范型参数化类型 |
RuntimeVisibleAnnotations | 类、方法表、字段表 | 为动态注解提供支持,表示注解是运行时可见的 |
RuntimeInvisibleAnnotations | 类、方法表、字段表 | 为动态注解提供支持,表示注解是运行时不可见的 |
RuntimeVisible ParameterAnnotations | 方法表 | 类似RuntimeVisibleAnnotations,作用于方法 |
RuntimeInvisible ParameterAnnotations | 方法表 | 类似RuntimeInvisibleAnnotations,作用于方法 |
AnnotationDefault | 方法表 | 注解类元素的默认值 |
BootstrapMethods | 类文件 | 保存invokedynamic指令引用的引导方法限定符 |
方法的主要内容存放在其属性中,其中最重要的一个属性就是Code,它存放着方法的字节码等信息,结构如下:
Code_attribute{
u2 : attribute_name_index; (属性名,指向常量池的常量,总是 Code)
u4 : attribute_length; (Code属性的剩余长度,不包括前6个字节 u2+u4)
u2 : max_stack; (操作数栈的最大深度)
u2 : max_locals; (局部变量的最大个数)
u4 : code_length; (字节码长度)
u1 : code[code_length]; (字节码内容)
u2 : exception_table_length; (异常表的个数)
exception_info : exception_table[exception_table_length]; (异常表)
u2 : attributes_count;
attribute_info : attributes[attributes_count];
}
exception_info{
u2 : start_pc; (字节码的开始偏移量)
u2 : end_pc; (字节码的结束偏移量)
u2 : handler_pc; (catch异常的处理代码偏移量)
u2 : catch_type; (需要catch的异常类型,指向常量池的索引)
}
LineNumberTable用来记录字节码偏移量和行号的对应关系,在软件调试时,该属性有至关重要的作用,若没有它,调试器无法定位到对应的源码。LineNumberTable属性的结构如下:其中,attribute_name_index为指向常量池的索引,在LineNumberTable属性中,该值为“LineNumberTable”,attribute_length为4字节无符号整数,表示属性的长度(不含前6字节),line_number_table_length表明表项有多少条记录,line_number_table为表的实际内容,它包含line_number_table_length个<start_pc,line_number>元组,其中,start_pc为字节码偏移量,line_number为对应的行号。
数据结构定义
{
u2 : attribute_name_index;
u4 : attribute_length;
u2 : line_number_table_length;
line_number_info : line_number_tables[line_number_table_length];
}
line_number_info {
u2 : start_pc;
u2 : line_number;
}
LocalVariableTable属性用于描述栈帧中局部变量表中的变量与Java源码中定义的变量之间的关系,它也不是运行时必需的属性,但默认会生成到Class文件之中,可以在Javac中分别使用g:none或g:vars选项来取消或要求生成这项信息。如果没有生成这项属性,最大的影响就是当其他人引用这个方法时,所有的参数名称都将会丢失,IDE将会使用诸如arg0、arg1之类的占位符代替原有的参数名,这对程序运行没有影响,但是会对代码编写带来较大不便,而且在调试期间无法根据参数名称从上下文中获得参数值。
{
u2 : attribute_name_index;
u4 : attribute_length;
u2 : local_variable_table_length;
local_variable_info : local_variable_tables[local_variable_table_length];
}
local_variable_info{
u2 : start_pc; (变量声明周期的开始量)
u2 : length; (变量声明周期的结束量)
u2 : name_index; (常量池引用)
u2 : decriptor_index; (描述引用)
u2 : index; (局部变量在栈帧局部变量表中Slot的位置,如果是64位变量long/double,占用两个Slot)
}
SourceFile属性用于记录生成这个Class文件的源码文件名称。这个属性也是可选的,可以分别使用Javac的g:none或g:source选项来关闭或要求生成这项信息。在Java中,对于大多数的类来说,类名和文件名是一致的,但是有一些特殊情况(如内部类)例外。如果不生成这项属性,当抛出异常时,堆栈中将不会显示出错代码所属的文件名。
{
u2 : attribute_name_index;
u4 : attribute_length;
u2 : sourcefile_index; (常量池引用,文件名)
}
ConstantValue属性的作用是通知虚拟机自动为静态变量赋值。只有被static关键字修饰的变量(类变量)才可以使用这项属性。类似int x=123
和static intx=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属性想支持别的类型也无能为力。
{
u2 : attribute_name;
u4 : attribute_length;
u2 : constant_value_index; (常量池引用)
}
在接口描述后,会有类的字段信息。由于一个类会有多个字段,所以需要首先指明字段的个数:
u2 : field_count
field_info : fields[field_count]
字段数量fields_count是一个2字节无符号整数,在PageClass生成的字节码中,这个值是0002,表示有两个字段。字段数量之后为字段的具体信息,每一个字段为一个field_info的结构,该结构如下:
field_info {
u2 : access_flags; (类似于class 的 访问标记)
u2 : name_index; (指向常量池中的 Utf8)
u2 : descriptor_index; (指向常量池中的 Utf8)
u2 : attributes_count; (字段可能还有一些额外的属性,这是属性个数)
attribute_info : attributes[attributes_count]
}
标记名称 | 数值 | 描述 |
---|---|---|
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 | 是否为临时字段,不序列化 |
ACC_SYNTHETIC | 0x1000 | 由编译器产生的字段,没有源码 |
ACC_ENUM | 0x4000 | 是否为枚举 |
private static final int size = 10;
001A 001F 000C 0001 0020 0000 0002 0021
field_info {
u2 : 001A; (final + private + static)
u2 : 001F; (指向常量池中的 31-Utf8 : size)
u2 : 000C; (指向常量池中的 12-Utf8 : l)
u2 : 0001; (一个额外属性)
attribute_info : {
u2 : 0020; (指向常量池中的 32-Utf8 : Constant Value)
u4 : 0000 0002; (2个字节)
u2 : 0021; (指向常量池中的 33-Integer : 10)
}
}
private int page;
0002 000B 000C 0000
field_info {
u2 : 0002; (private)
u2 : 000B; (指向常量池中的 11-Utf8 : page)
u2 : 000C; (指向常量池中的 12-Utf8 : l)
u2 : 0000; (没有额外属性)
}
在字段之后,就是类的方法信息。方法信息和字段类似,由两部分组成:
u2 : methods_count;
method_info : methods[methods_count];
其中methods_count为2字节整数,表示该类中有几个方法。接着就是methods_count个method_info结构,每一个method_info结构表示一个方法,如下所示:
method_info {
u2 : access_flags; (访问标记)
u2 : name_index; (方法名,指向常量池的索引)
u2 : descriptor_index; (描述符,指向常量池的索引)
u2 : attributes_count;
attribute_info : attributes[attributes_count];
}
方法的访问标记,用于标明方法的权限及相关特性
标记名称 | 数值 | 描述 |
---|---|---|
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 | 0x0080 | native方法 |
ACC_ABSTRACT | 0x0400 | 抽象方法 |
ACC_STRICT | 0x0800 | 浮点模式为 FP-Strict |
ACC_SYNTHETIC | 0x1000 | 由编译器产生的方法,没有源码 |
descriptor_index为方法描述符,它也是指向常量池的索引,是一个字符串,表示方法的签名(参数、返回值等),同时对方法签名的表示做了一些规定。它将函数的参数类型写在一对小括号中,并在括号右侧给出方法的返回值。比如,若有如下方法:
Object m(int i, double d, Thread t){...}
则它的方法描述符为:
(IDLjava/lang/Thread;)Ljava/lang/Object;
可以看到,方法的参数统一列在一对小括号中,“I”表示int,“D”表示double,“Ljava/lang/Thread;”表示Thread对象。小括号右侧的Ljava/lang/Object;表示方法的返回值为Object对象。
为了测试一下各种基础变量的符号,在PageClass中临时添加一个方法
/**
* 用于测试参数类型
*
* @param a
* @param b
* @param c
* @param d
* @param e
* @param f
* @param g
* @param h
* @param i
* @param j
* @return
*/
public static PageClass of(long a, float b, double c, int d, short e,
char f, byte g, boolean h, String i, EmptyClass j) {
return new PageClass(0);
}
重新编译后生成的字符串常量为
(JFDISCBZLjava/lang/String;Lbytecode/EmptyClass;)Lbytecode/PageClass;
因此可以看出各个基础数据类型的对应
long | float | double | int | short | char | byte | boolean |
---|---|---|---|---|---|---|---|
J | F | D | I | S | C | B | Z |
从图中可以看到类有三个方法。
0001 0005 000F 0001
0022 00000046 0002 0002 0000000A
2AB70001 2A1BB500 07B1 // 字节码
0000 0002
0023 0000000E 0003 // LineNumberTable
0000 0018
0004 0019
0009 001A
0024 00000016 0002 // LocalVariableTable
0000 000A 0025 0026 0000
0000 000A 000B 000C 0001
{
u2 : 0001; (public)
u2 : 0005; (5-Utf8 : <init>)
u2 : 000F; (15-Utf8 : (l)V)
u2 : 0001; (一个属性)
attribute_info {
u2 : 0022; (34-Utf8 : Code)
u4 : 0000 0046; (数据长度为70)
u2 : 0002; (操作数栈的最大深度)
u2 : 0002; (局部变量的最大个数)
u4 : 0000000A; (字节码长度=10)
Code { //2AB70001 2A1BB500 07B1; (字节码内容)
0 aload_0
1 invokespecial #1 <java/lang/Object.<init>>
4 aload_0
5 iload_1
6 putfield #7 <bytecode/PageClass.page>
9 return
}
u2 : 0000; (异常表的个数:0)
u2 : 0002; (两个属性)
attributes [
{
u2 : 0023; (35-Utf8 : LineNumberTable,源代码行号)
u4 : 0000000E; (长度 : 14)
u2 : 0003; (三个 line_number_info)
line_number_tables[
{
u2 : 0000; (start_pc : 0)
u2 : 0018; (line_number : 24) // public PageClass(int page) {
},
{
u2 : 0004; (start_pc : 4)
u2 : 0019; (line_number : 25) // this.page = page;
},
{
u2 : 0009; (start_pc : 9)
u2 : 001A; (line_number : 26) // }
}
]
},
{
u2 : 0024; (36-Utf8 : LocalVariableTable , 内部变量)
u4 : 00000016; (长度 : 22)
u1 : 0002; (内部变量个数 : 2)
local_variable_tables[
{
u2 : 0000; (start_pc : 0)
u2 : 000A; (length : 10)
u2 : 0025; (name_index,37-Utf8 : this)
u2 : 0026; (descriptor_index,38-Utf8 : Lbytecode/PageClass;)
u2 : 0000; (index : 0 , 第1个内部变量)
},
{
u2 : 0000; (start_pc : 0)
u2 : 000A; (length : 10)
u2 : 000B; (name_index,11-Utf8 : page)
u2 : 000C; (descriptor_index,12-Utf8 : l)
u2 : 0001; (index : 1 , 第2个内部变量)
}
]
}
]
}
}
0001 0018 0019 0001
0022 00000032 0002 0001 00000008
2AB40007 100A68AC // 字节码
0000 0002
0023 00000006 0001 // LineNumberTable
0000 0022
0024 0000000C 0001 // LocalVariableTable
0000 0008 0025 0026 0000
{
u2 : 0001; (public)
u2 : 0018; (24-Utf8 : calculateOffset)
u2 : 0019; (25-Utf8 : ()l)
u2 : 0001; (一个属性)
Code_attribute{
u2 : 0022; (34-Utf8 : Code)
u4 : 0000 0032; (数据长度为50)
u2 : 0002; (操作数栈的最大深度)
u2 : 0001; (局部变量的最大个数)
u4 : 00000008; (字节码长度=8)
Code { //2AB40007 100A68AC; (字节码内容)
0 aload_0
1 getfield #7 <bytecode/PageClass.page>
4 bipush 10
6 imul
7 ireturn
}
u2 : 0000; (异常表的个数:0)
u2 : 0002; (两个属性)
attributes[
{
u2 : 0023; (35-Utf8 : LineNumberTable,源代码行号)
u4 : 00000006; (长度 : 6)
u2 : 0001; (1个 line_number_info)
line_number_tables[
{
u2 : 0000; (start_pc : 0)
u2 : 0022; (line_number : 34) // return page * size;
}
]
},
{
u2 : 0024; (36-Utf8 : LocalVariableTable , 内部变量)
u4 : 0000000C; (长度 : 12)
u1 : 0001; (内部变量个数 : 1)
local_variable_tables[
{
u2 : 0000; (start_pc : 0)
u2 : 0008; (length : 8)
u2 : 0025; (name_index,37-Utf8 : this)
u2 : 0026; (descriptor_index,38-Utf8 : Lbytecode/PageClass;)
u2 : 0000; (index : 0 , 第1个内部变量)
}
]
}
]
}
}
0009 0027 0028 0001
0022 00000050 0003 0002 00000014
BB000859 06B7000D 4CB20010 2BB60016 B6001AB1 // 字节码
0000 0002
0023 0000000E 0003 //LineNumberTable
0000 002B
0009 002C
0013 002D
0024 00000016 0002 // LocalVariableTable
0000 0014 0029 002A 0000
0009 000B 000B 0026 0001
{
u2 : 0009; (public + static)
u2 : 0027; (39-Utf8 : main)
u2 : 0028; [ 40-Utf8 : ([Ljava/lang/String;)V ]
u2 : 0001; (一个属性)
Code_attribute{
u2 : 0022; (34-Utf8 : Code)
u4 : 0000 0050; (数据长度为80)
u2 : 0003; (操作数栈的最大深度)
u2 : 0002; (局部变量的最大个数)
u4 : 00000014; (字节码长度=20)
u1[20] : BB000859 06B7000D 4CB20010 2BB60016 B6001AB1; (字节码内容)
{
0 new #8 <bytecode/PageClass>
3 dup
4 iconst_3
5 invokespecial #13 <bytecode/PageClass.<init>>
8 astore_1
9 getstatic #16 <java/lang/System.out>
12 aload_1
13 invokevirtual #22 <bytecode/PageClass.calculateOffset>
16 invokevirtual #26 <java/io/PrintStream.println>
19 return
}
u2 : 0000; (异常表的个数:0)
u2 : 0002; (两个属性)
attributes[
{
u2 : 0023; (35-Utf8 : LineNumberTable,源代码行号)
u4 : 0000000E; (长度 : 14)
u2 : 0003; (3个 line_number_info)
line_number_tables[
{
u2 : 0000; (start_pc : 0)
u2 : 002B; (line_number : 33) // PageClass page = new PageClass(3);
},
{
u2 : 0009; (start_pc : 0)
u2 : 002C; (line_number : 44) // System.out.println(page.calculateOffset());
},
{
u2 : 0013; (start_pc : 0)
u2 : 002D; (line_number : 45) // }
}
]
}
{
u2 : 0024; (36-Utf8 : LocalVariableTable , 内部变量)
u4 : 00000016; (长度 : 22)
u1 : 0002; (内部变量个数 : 2)
local_variable_tables[
{
u2 : 0000; (start_pc : 0)
u2 : 0014; (length : 20)
u2 : 0029; (name_index,41-Utf8 : args)
u2 : 002A; (descriptor_index,42-Utf8 : Ljava/lang/String;)
u2 : 0000; (index : 0 , 第1个内部变量)
},
{
u2 : 0009; (start_pc : 0)
u2 : 000B; (length : 20)
u2 : 000B; (name_index,11-Utf8 : page)
u2 : 0026; (descriptor_index,38-Utf8 : Lbytecode/PageClass;)
u2 : 0001; (index : 1 , 第2个内部变量)
}
]
}
]
}
}
Class文件的最后一部分内容,是类额外的源代码信息。
{
u2 : 0001; (有一个扩展属性)
attributes[
{
u2 : 002B; (43-Utf8 : SourceFile)
u4 : 0000002C;
u2 : 002C; (44-Utf8 : PageClass.java)
}
]
}
简单列一下常用的内容查看工具。
16进制编辑器,看到的是原生态的数据,就是看着累。Mac上我下载了一个免费的 iHex。左边16进制,右边ascii码,状态栏可以显示十进制。
JDK自带的查看工具
javap是jdk自带的反解析工具。它的作用就是根据class字节码文件,反解析出当前类对应的code区(汇编指令)、本地变量表、异常表和代码行偏移量映射表、常量池等等信息。
javap的用法格式:javap <options> <classes>
,其中classes就是class文件。
在命令行中直接输入javap
或javap -help
可以看到javap的options有如下选项:
用法: javap <options> <classes>
其中, 可能的选项包括:
-? -h --help -help 输出此帮助消息
-version 版本信息,javap 的版本,不是 class 文件的编译版本
-v -verbose 输出附加信息
-l 输出行号和本地变量表
-public 仅显示公共类和成员
-protected 显示受保护的/公共类和成员
-package 显示程序包/受保护的/公共类
和成员 (默认)
-p -private 显示所有类和成员
-c 对代码进行反汇编
-s 输出内部类型签名
-sysinfo 显示正在处理的类的
系统信息(路径、大小、日期、SHA-256 散列)
-constants 显示最终常量
--module <模块>, -m <模块> 指定包含要反汇编的类的模块
--module-path <路径> 指定查找应用程序模块的位置
--system <jdk> 指定查找系统模块的位置
--class-path <路径> 指定查找用户类文件的位置
-classpath <路径> 指定查找用户类文件的位置
-cp <路径> 指定查找用户类文件的位置
-bootclasspath <路径> 覆盖引导类文件的位置
--multi-release <version> 指定要在多发行版 JAR 文件中使用的版本
用javap查看一下前面分析的 PageClass.class 文件。使用选项 -v ,基本上能看到所有的内容,其他就不一一看了。
% javap -v target/classes/bytecode/PageClass.class
Classfile /Users/liaojunyong/Workspaces/ResearchDistributed/research-java/target/classes/bytecode/PageClass.class
Last modified 2019年12月18日; size 743 bytes
SHA-256 checksum c8ea8361a2784888c50d6c7d3ad2fa825ffa34fa5945d437f1bdb194950dd58e
Compiled from "PageClass.java"
public class bytecode.PageClass
minor version: 0
major version: 52
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #8 // bytecode/PageClass
super_class: #2 // java/lang/Object
interfaces: 0, fields: 2, methods: 3, attributes: 1
Constant pool:
#1 = Methodref #2.#3 // java/lang/Object."<init>":()V
#2 = Class #4 // java/lang/Object
#3 = NameAndType #5:#6 // "<init>":()V
#4 = Utf8 java/lang/Object
#5 = Utf8 <init>
#6 = Utf8 ()V
#7 = Fieldref #8.#9 // bytecode/PageClass.page:I
#8 = Class #10 // bytecode/PageClass
#9 = NameAndType #11:#12 // page:I
#10 = Utf8 bytecode/PageClass
#11 = Utf8 page
#12 = Utf8 I
#13 = Methodref #8.#14 // bytecode/PageClass."<init>":(I)V
#14 = NameAndType #5:#15 // "<init>":(I)V
#15 = Utf8 (I)V
#16 = Fieldref #17.#18 // java/lang/System.out:Ljava/io/PrintStream;
#17 = Class #19 // java/lang/System
#18 = NameAndType #20:#21 // out:Ljava/io/PrintStream;
#19 = Utf8 java/lang/System
#20 = Utf8 out
#21 = Utf8 Ljava/io/PrintStream;
#22 = Methodref #8.#23 // bytecode/PageClass.calculateOffset:()I
#23 = NameAndType #24:#25 // calculateOffset:()I
#24 = Utf8 calculateOffset
#25 = Utf8 ()I
#26 = Methodref #27.#28 // java/io/PrintStream.println:(I)V
#27 = Class #29 // java/io/PrintStream
#28 = NameAndType #30:#15 // println:(I)V
#29 = Utf8 java/io/PrintStream
#30 = Utf8 println
#31 = Utf8 size
#32 = Utf8 ConstantValue
#33 = Integer 10
#34 = Utf8 Code
#35 = Utf8 LineNumberTable
#36 = Utf8 LocalVariableTable
#37 = Utf8 this
#38 = Utf8 Lbytecode/PageClass;
#39 = Utf8 main
#40 = Utf8 ([Ljava/lang/String;)V
#41 = Utf8 args
#42 = Utf8 [Ljava/lang/String;
#43 = Utf8 SourceFile
#44 = Utf8 PageClass.java
{
public bytecode.PageClass(int);
descriptor: (I)V
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=2, args_size=2
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: iload_1
6: putfield #7 // Field page:I
9: return
LineNumberTable:
line 24: 0
line 25: 4
line 26: 9
LocalVariableTable:
Start Length Slot Name Signature
0 10 0 this Lbytecode/PageClass;
0 10 1 page I
public int calculateOffset();
descriptor: ()I
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: getfield #7 // Field page:I
4: bipush 10
6: imul
7: ireturn
LineNumberTable:
line 34: 0
LocalVariableTable:
Start Length Slot Name Signature
0 8 0 this Lbytecode/PageClass;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=2, args_size=1
0: new #8 // class bytecode/PageClass
3: dup
4: iconst_3
5: invokespecial #13 // Method "<init>":(I)V
8: astore_1
9: getstatic #16 // Field java/lang/System.out:Ljava/io/PrintStream;
12: aload_1
13: invokevirtual #22 // Method calculateOffset:()I
16: invokevirtual #26 // Method java/io/PrintStream.println:(I)V
19: return
LineNumberTable:
line 43: 0
line 44: 9
line 45: 19
LocalVariableTable:
Start Length Slot Name Signature
0 20 0 args [Ljava/lang/String;
9 11 1 page Lbytecode/PageClass;
}
SourceFile: "PageClass.java"
图形化的字节码阅读器。https://github.com/ingokegel/jclasslib ,能在Windows/Mac/Linux下运行,也提供IDEA插件。
打开PageClass.class查看,前面我们分析的所有数据,都在这里面。
常量池的常量都有序号,各种 ref 也直接可以显示ref的具体值,比较方便。
直接输入 javac
查看参数说明
% javac
用法: javac <options> <source files>
其中, 可能的选项包括:
@<filename> 从文件读取选项和文件名
-Akey[=value] 传递给注释处理程序的选项
--add-modules <模块>(,<模块>)*
除了初始模块之外要解析的根模块; 如果 <module>
为 ALL-MODULE-PATH, 则为模块路径中的所有模块。
--boot-class-path <path>, -bootclasspath <path>
覆盖引导类文件的位置
--class-path <path>, -classpath <path>, -cp <path>
指定查找用户类文件和注释处理程序的位置
-d <directory> 指定放置生成的类文件的位置
-deprecation 输出使用已过时的 API 的源位置
--enable-preview 启用预览语言功能。要与 -source 或 --release 一起使用。
-encoding <encoding> 指定源文件使用的字符编码
-endorseddirs <dirs> 覆盖签名的标准路径的位置
-extdirs <dirs> 覆盖所安装扩展的位置
-g 生成所有调试信息
-g:{lines,vars,source} 只生成某些调试信息
-g:none 不生成任何调试信息
-h <directory> 指定放置生成的本机标头文件的位置
--help, -help, -? 输出此帮助消息
--help-extra, -X 输出额外选项的帮助
-implicit:{none,class} 指定是否为隐式引用文件生成类文件
-J<flag> 直接将 <标记> 传递给运行时系统
--limit-modules <模块>(,<模块>)*
限制可观察模块的领域
--module <模块>(,<模块>)*, -m <模块>(,<模块>)*
只编译指定的模块,请检查时间戳
--module-path <path>, -p <path>
指定查找应用程序模块的位置
--module-source-path <module-source-path>
指定查找多个模块的输入源文件的位置
--module-version <版本> 指定正在编译的模块版本
-nowarn 不生成任何警告
-parameters 生成元数据以用于方法参数的反射
-proc:{none,only} 控制是否执行注释处理和/或编译。
-processor <class1>[,<class2>,<class3>...]
要运行的注释处理程序的名称; 绕过默认的搜索进程
--processor-module-path <path>
指定查找注释处理程序的模块路径
--processor-path <path>, -processorpath <path>
指定查找注释处理程序的位置
-profile <profile> 请确保使用的 API 在指定的配置文件中可用
--release <release> 为指定的 Java SE 发行版编译。支持的发行版:7, 8, 9, 10, 11, 12, 13
-s <directory> 指定放置生成的源文件的位置
--source <release>, -source <release>
提供与指定的 Java SE 发行版的源兼容性。支持的发行版:7, 8, 9, 10, 11, 12, 13
--source-path <path>, -sourcepath <path>
指定查找输入源文件的位置
--system <jdk>|none 覆盖系统模块位置
--target <release>, -target <release>
生成适合指定的 Java SE 发行版的类文件。支持的发行版:7, 8, 9, 10, 11, 12, 13
--upgrade-module-path <path>
覆盖可升级模块位置
-verbose 输出有关编译器正在执行的操作的消息
--version, -version 版本信息
-Werror 出现警告时终止编译
在pom.xml中,可以为 compile 指定参数,如
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.0</version>
<configuration>
<source>8</source>
<target>8</target>
<encoding>UTF-8</encoding>
<compilerArgs>
<compilerArgument>-g:lines</compilerArgument>
<compilerArgument>-g:vars</compilerArgument>
<compilerArgument>-g:source</compilerArgument>
<compilerArgument>-verbose</compilerArgument>
</compilerArgs>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.0</version>
<configuration>
<source>8</source>
<target>8</target>
<encoding>UTF-8</encoding>
<compilerArgs>
<compilerArgument>-g:none</compilerArgument>
</compilerArgs>
</configuration>
</plugin>
查看生成的class文件内容:没有LineNumberTable/LocalVariableTable/SourceFile信息。
在网站 http://java-decompiler.github.io/ 可以下载。在 MacBook 上执行下载的程序,可能会被macOS的安全认证拦截掉,提示“macOS无法验证App不包含恶意软件”。这时候我们假定jd-gui是安全的,就可以按住 Control键的时候打开软件。
IDEA缺省集成了反编译工具,只需要在IDEA中直接打开一个class文件,就能看到反编译的结果。
// idea 反编译结果
package bytecode;
public class PageClass {
private static final int size = 10;
private int page;
public PageClass(int page) {
this.page = page;
}
public int calculateOffset() {
return this.page * 10;
}
public static void main(String[] args) {
PageClass page = new PageClass(3);
System.out.println(page.calculateOffset());
}
}
可以看到,区别只有常量10。calculateOffset()代码中的size,被替换成数字10。
// idea 反编译结果
package bytecode;
public class PageClass {
private static final int size = 10;
private int page;
public PageClass(int var1) {
this.page = var1;
}
public int calculateOffset() {
return this.page * 10;
}
public static void main(String[] var0) {
PageClass var1 = new PageClass(3);
System.out.println(var1.calculateOffset());
}
}
可以看到,区别在于函数内的变量名被替换掉:
这是因为字节码中缺少 LocalVariableTable 的原因。这时再看一下jad-gui的反编译结果,在变量命名上有一些区别:jad-gui使用了 paramInt。如果一个函数变量有多个相同类型的参数,会自动添加数字如 paramInt1 / paramInt2。
// jd-gui 反编译结果
package bytecode;
public class PageClass {
private static final int size = 10;
private int page;
public PageClass(int paramInt) { this.page = paramInt; }
public int calculateOffset() { return this.page * 10; }
public static void main(String[] paramArrayOfString) {
PageClass pageClass = new PageClass(3);
System.out.println(pageClass.calculateOffset());
}
}
前面分析Class结构的时候,在属性Code中,有字节码的具体值。当时是直接从javap分析出的数据直接拷贝的内容。这里只是大概了解一下。
Java虚拟机的指令由一个字节长度的、代表着某种特定操作含义的数字(称为操作码,Opcode)以及跟随其后的零至多个代表此操作所需参数(称为操作数,Operands)而构成。命令格式Opcode Operands
。比如我们前面PageClass中的main()方法生成的字节码,红框内是 Opcode,蓝框内是 Operands。
栈帧(StackFrame)是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈(VirtualMachineStack)的栈元素。栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。每一个方法从调用开始至执行完成的过程,都对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程。
每一个栈帧都包括了局部变量表、操作数栈、动态连接、方法返回地址和一些额外的附加信息。
一个线程中的方法调用链可能会很长,很多方法都同时处于执行状态。对于执行引擎来说,在活动线程中,只有位于栈顶的栈帧才是有效的,称为当前栈帧(CurrentStackFrame),与这个栈帧相关联的方法称为当前方法(CurrentMethod)。执行引擎运行的所有字节码指令都只针对当前栈帧进行操作,在概念模型上,典型的栈帧结构
是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。在Java程序编译为Class文件时,就在方法的Code属性的max_locals数据项中确定了该方法所需要分配的局部变量表的最大容量。
局部变量表的容量以变量槽(VariableSlot,下称Slot)为最小单位,虚拟机规范中并没有明确指明一个Slot应占用的内存空间大小,只是很有导向性地说到每个Slot都应该能存放一个boolean、byte、char、short、int、float、reference或returnAddress类型的数据,这8种数据类型。简单理解为每个Slot的大小为32位,如果需要保存long和double,就占用两个Slot。对于占了两个Slot的long和double,不允许直接访问其中Slot,否则在类加载的校验阶段就会抛异常。
这里还有很多高级的细节,比如:Slot的重用、变量逃逸分析等,这儿就不说了。
操作数栈(OperandStack)也常称为操作栈,它是一个后入先出(LastInFirstOut,LIFO)栈。当一个方法刚刚开始执行的时候,这个方法的操作数栈是空的,在方法的执行过程中,会有各种字节码指令往操作数栈中写入和提取内容,也就是出栈/入栈操作。例如,在做算术运算的时候是通过操作数栈来进行的,又或者在调用其他方法的时候是通过操作数栈来进行参数传递的。
举个例子,整数加法的字节码指令iadd在运行的时候操作数栈中最接近栈顶的两个元素已经存入了两个int型的数值,当执行这个指令时,会将这两个int值出栈并相加,然后将相加的结果入栈。
操作数栈中元素的数据类型必须与字节码指令的序列严格匹配,在编译程序代码的时候,编译器要严格保证这一点,在类校验阶段的数据流分析中还要再次验证这一点。再以上面的iadd指令为例,这个指令用于整型数加法,它在执行时,最接近栈顶的两个元素的数据类型必须为int型,不能出现一个long和一个float使用iadd命令相加的情况。
每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接(DynamicLinking)。我们知道Class文件的常量池中存有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用作为参数。这些符号引用一部分会在类加载阶段或者第一次使用的时候就转化为直接引用,这种转化称为静态解析。另外一部分将在每一次运行期间转化为直接引用,这部分称为动态连接。
当一个方法开始执行后,只有两种方式可以退出这个方法。第一种方式是执行引擎遇到任意一个方法返回的字节码指令,这时候可能会有返回值传递给上层的方法调用者;另外一种退出方式是,在方法执行过程中遇到了异常被抛出,就会导致方法退出,这时不会给它的上层调用者产生任何返回值。
无论采用何种退出方式,在方法退出之后,都需要返回到方法被调用的位置,程序才能继续执行,方法返回时可能需要在栈帧中保存一些信息,用来帮助恢复它的上层方法的执行状态。一般来说,方法正常退出时,调用者的PC计数器的值可以作为返回地址,栈帧中很可能会保存这个计数器值。而方法异常退出时,返回地址是要通过异常处理器表来确定的,栈帧中一般不会保存这部分信息。
方法退出的过程实际上就等同于把当前栈帧出栈,因此退出时可能执行的操作有:恢复上层方法的局部变量表和操作数栈,把返回值(如果有的话)压入调用者栈帧的操作数栈中,调整PC计数器的值以指向方法调用指令后面的一条指令等。
对于大部分与数据类型相关的字节码指令,它们的操作码助记符中都有特殊的字符来表明专门为哪种数据类型服务:i代表对int类型的数据操作,l代表long,s代表short,b代表byte,c代表char,f代表float,d代表double,a代表reference。也有一些指令的助记符中没有明确地指明操作类型的字母,如arraylength指令,它没有代表数据类型的特殊字符,但操作数永远只能是一个数组类型的对象。还有另外一些指令,如无条件跳转指令goto则是与数据类型无关的。
例如,iload指令用于从局部变量表中加载int型的数据到操作数栈中,而fload指令加载的则是float类型的数据。
加载和存储指令用于将数据在栈帧中的局部变量表和操作数栈之间来回传输,这类指令包括如下内容。<n>
代表的是一组指令: _0 _1 _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
运算或算术指令用于对两个操作数栈上的值进行某种特定运算,并把结果重新存入到操作栈顶。大体上算术指令可以分为两种:对整型数据进行运算的指令与对浮点型数据进行运算的指令,无论是哪种算术指令,都使用Java虚拟机的数据类型,由于没有直接支持byte、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。
位移指令:ishl、ishr、iushr、lshl、lshr、lushr。
按位或指令:ior、lor。
按位与指令:iand、land。
按位异或指令:ixor、lxor。
局部变量自增指令:iinc。
比较指令:dcmpg、dcmpl、fcmpg、fcmpl、lcmp。
类型转换指令可以将两种不同的数值类型进行相互转换,这些转换操作一般用于实现用户代码中的显式类型转换操作,或者用来处理字节码指令集中数据类型相关指令无法与数据类型一一对应的问题。
Java虚拟机直接支持以下数值类型的宽化类型转换(即小范围类型向大范围类型的安全转换)
int --> long、float double
long --> float、double
float --> double
相对的,处理窄化类型转换(NarrowingNumericConversions)时,必须显式地使用转换指令来完成,这些转换指令包括:
i2b、i2c、i2s、l2i、f2i、f2l、d2i、d2l和d2f。
窄化类型转换可能会导致转换结果产生不同的正负号、不同的数量级的情况,转换过程很可能会导致数值的精度丢失。
虽然类实例和数组都是对象,但Java虚拟机对类实例和数组的创建与操作使用了不同的字节码指令。对象创建后,就可以通过对象访问指令获取对象实例或者数组实例中的字段或者数组元素,这些指令如下。
创建类实例的指令:new
创建数组的指令:newarray、anewarray、multianewarray
访问类字段(static)的指令: getstatic、putstatic
访问实例字段(非static)的指令:getfield、putfield
把一个数组元素加载到操作数栈的指令:baload、caload、saload、iaload、laload、faload、daload、aaload
将一个操作数栈的值存储到数组元素中的指令:bastore、castore、sastore、iastore、fastore、dastore、aastore。
取数组长度的指令:arraylength。
检查类实例类型的指令:instanceof、checkcast。
如同操作一个普通数据结构中的堆栈那样,Java虚拟机提供了一些用于直接操作操作数栈的指令,包括:
将操作数栈的栈顶一个或两个元素出栈:pop、pop2。
复制栈顶一个或两个数值并将复制值或双份的复制值重新压入栈顶:dup、dup2、dup_x1、dup2_x1、dup_x2、dup2_x2。
将栈最顶端的两个数值互换:swap。
控制转移指令可以让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。
列举以下5条用于方法调用的指令。
用于调用对象的实例方法,根据对象的实际类型进行分派(虚方法分派),这也是Java语言中最常见的方法分派方式。
用于调用接口方法,它会在运行时搜索一个实现了这个接口方法的对象,找出适合的方法进行调用。
用于调用一些需要特殊处理的实例方法,包括实例初始化方法、私有方法和父类方法。
用于调用类方法(static方法)。
用于在运行时动态解析出调用点限定符所引用的方法,并执行该方法。
前面4条调用指令的分派逻辑都固化在Java虚拟机内部,而invokedynamic指令的分派逻辑是由用户所设定的引导方法决定的。
在Java程序中显式抛出异常的操作(throw语句)都由athrow指令来实现,除了用throw语句显式抛出异常情况之外,Java虚拟机规范还规定了许多运行时异常会在其他Java虚拟机指令检测到异常状况时自动抛出。例如,在整数运算中,当除数为零时,虚拟机会在idiv或ldiv指令中抛出ArithmeticException异常。
而在Java虚拟机中,处理异常(catch语句)不是由字节码指令来实现的,而是采用异常表来完成的。
Java虚拟机可以支持方法级的同步和方法内部一段指令序列的同步,这两种同步结构都是使用Monitor来支持的。
方法级的同步是隐式的,即无须通过字节码指令来控制,它实现在方法调用和返回操作之中。虚拟机可以从方法常量池的方法表结构中的ACC_SYNCHRONIZED访问标志得知一个方法是否声明为同步方法。当方法调用时,调用指令将会检查方法的ACC_SYNCHRONIZED访问标志是否被设置,如果设置了,执行线程就要求先成功持有Monitor,然后才能执行方法,最后当方法完成(无论是正常完成还是非正常完成)时释放Monitor。在方法执行期间,执行线程持有了Monitor,其他任何线程都无法再获取到同一个Monitor。如果一个同步方法执行期间抛出了异常,并且在方法内部无法处理此异常,那么这个同步方法所持有的Monitor将在异常抛到同步方法之外时自动释放。
同步一段指令集序列通常是由Java语言中的synchronized语句块来表示的,Java虚拟机的指令集中有monitorenter和monitorexit两条指令来支持synchronized关键字的语义。
以 PageClass.calculateOffset()为例来分析,足够简单
Code {
0 aload_0
1 getfield #7 <bytecode/PageClass.page>
4 bipush 10
6 imul
7 ireturn
}
local_variable_tables[
{
u2 : 0000; (start_pc : 0)
u2 : 0008; (length : 8)
u2 : 0025; (name_index,37-Utf8 : this)
u2 : 0026; (descriptor_index,38-Utf8 : Lbytecode/PageClass;)
u2 : 0000; (index : 0 , 第1个内部变量)
}
]
调用 calculateOffset() 的代码如下,进入到方法后, page 变量的值为3。
PageClass page = new PageClass(3);
System.out.println(page.calculateOffset());
执行过程及操作数栈的变化
字节码 | 初始 | aload_0 | getfield #7 | bipush 10 | imul | ireturn |
---|---|---|---|---|---|---|
解释 | 加载变量0 | 读变量#7(page) | 入栈10 | 乘法 | 返回 | |
操作数栈 | this | this | this | This | ||
3 | 3 | 30 | ||||
10 |
imul:取出栈顶的两个数相乘,并写回栈。
为什么代码是 page*size
,但压栈的时候直接bipush 10
?
将size的final删掉并重新编译(修改前后的代码一起展示)
/**
* 定义常量
*/
private static final int size = 10;
/**
* 定义常量
*/
private static int size = 10;
再看calculateOffset()的字节码(修改前后的字节码一起展示)
0 aload_0
1 getfield #7 <bytecode/PageClass.page>
4 bipush 10
6 imul
7 ireturn
0 aload_0
1 getfield #7 <bytecode/PageClass.page>
4 getstatic #13 <bytecode/PageClass.size>
7 imul
8 ireturn