前言介绍
了解Java代码如何编译成字节码并在JVM上执行是非常重要的。这种理解可以帮助我们理解程序执行时发生的情况,确保语言特性符合逻辑,并在进行讨论时能够全面考虑各种因素和副作用。
本文将深入探讨Java代码编译成字节码并在JVM上执行的过程。如果您对JVM的内部结构和字节码执行过程中使用的不同内存区域有兴趣,建议阅读我之前的JVM专栏《深入浅出JVM原理及调优》。
接下来,我们将介绍不同的Java代码结构,并解释如何将这些结构编译成字节码并在JVM上执行。
总体技术知识脉络
- 基本编程概念(第一篇文章)
- 变量
- 局部变量
- 字段 (类变量)
- 常量字段 (类常量)
- 静态变量
- 条件语句(第二篇文章)
- if-else
- switch
- 循环
- while循环
- for循环
- do-while循环
- 面向对象和安全性 (第三篇文章)
- 异常处理
- 同步
- 方法调用
- 创建新对象和数组
- 元编程(第四篇文章)
- 泛型
- 注解
- 反射
代码案例提示
本文提供了许多代码示例,并展示了生成的典型字节码。每个字节码指令(或操作码)都标有一个数字,表示它在字节码中的位置。
- 例如,指令:iconst_1 只需要一个字节,因此它的字节码将位于位置2。
- 例如,指令:bipush 5 需要两个字节,其中一个字节用于操作码 bipush,另一个字节用于操作数5。由于操作数占用了位置2的字节,所以下一个字节码将位于位置3。
变量
在Java字节码中,变量是一种用于存储数据的容器,包括局部变量、字段、常量字段和静态变量,这些变量都需通过特定的指令进行声明、初始化和访问,并在字节码中有相应的表示形式。理解Java字节码中的变量对深入了解Java程序至关重要,有助于更好地理解代码的执行过程和内部结构。
- 局部变量是在方法或代码块内部声明的变量,用于临时存储数据,作用域仅限于其声明的方法或代码块。
- 字段是在类中声明的变量,用于存储对象的状态。字段可以是实例字段(每个对象具有自己的一组字段值)或静态字段(所有对象共享相同的字段值)。
- 常量字段是在类中声明的不可更改的字段,通常用作常量值,运行时不允许修改。
- 静态变量是与类本身关联而不是与类的实例相关联的变量,在整个类的生命周期中保持相同的值,可以通过类名直接访问。
局部变量
Java虚拟机(JVM)采用基于堆栈的架构,在执行每个方法时会创建一个包含局部变量的框架。局部变量存储在一个数组中,包括对本方法的引用、方法参数和其他本地定义的变量。对于类方法,方法参数从零开始计数,而对于实例方法,零槽将被保留给予this对象。
局部变量的类型
局部变量可以是任何类型,它们在局部变量数组中占用一个槽。但是,long和double类型占用两个连续的槽,因为它们是双倍宽度的(64位而不是32位)。其他所有类型都占用一个槽。
局部变量数组中的每个槽位都被用于存储一个变量,其中所有类型都占用一个槽位,除了 long 和 double 类型。由于它们是双倍宽度的(64位而不是32位),所以它们需要连续占用两个槽位。
在创建新变量时,其值会被存储到操作数栈上。然后,该值将被移动到本地变量数组中的相应槽位上。对于非基本类型的变量,局部变量槽位中只存储一个引用,该引用指向堆中存储的对象。
局部变量案例
java源码
int i = 4;
class字节码
0: bipush 4
2: istore_0
bipush
:将一个字节作为整数添加到操作数堆栈中,本例中是将4添加到操作数堆栈中。- istore_:是一组操作码之一,用于将一个整数存储到局部变量中。其中, 表示要存储的值在局部变量数组中的位置,只能是 0、1、2 或 3。而 istore 则用于存储大于 3 的值,它的操作数表示要存储的值在局部变量数组中的位置。
在内存中执行此操作
类文件中的每个方法都包含一个局部变量表。如果在某个方法中加入这段代码,那么该方法的局部变量表将包含以下条目。
LocalVariableTable:
Start Length Slot Name Signature
0 1 1 i I
字段(类变量)
字段是存储在堆上的类实例或对象的一部分。有关字段的信息会被添加到类文件的 field_info 数组中。
在Java的类或接口中,每个字段(包括类变量和实例变量)都会在class文件中通过一个名为field_info的可变长度表进行描述。在同一个class文件中,不会有两个具有相同名字和描述的字段存在。需要注意的是,虽然在Java中不允许在同一个类或接口中存在两个具有相同名字的字段,但是在一个class文件中,可以存在两个具有相同名字但描述符不同的字段。
换句话说,虽然不允许在Java中定义同名但类别不同的字段,但是这种情况在一个Java class文件中是合法的。下面是field_info表的详细格式:
field_info表的格式
类型 | 名称 | 数量 |
u2 | access_flags | 1 |
u2 | name index | 1 |
u2 | descriptor_index | 1 |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes _count |
field_info表中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_PUBLIC、ACC_PRIVATE、或ACC_PROTECTED这三个标志之一。ACC_FINAL和ACC_VOLATILE不能同时设置在同一个字段上。而在接口中声明的字段则必须且只能使用ACC_PUBLIC、ACC_STATIC和ACC_FINAL这三种标志。
field_info表中name_index项的标志
name_index项提供了一个索引,用于访问CONSTANT_Uf8_info表中的入口,该入口包含了字段的简单名称(而不是全限定名)。在class文件中,每个字段的名称都必须符合Java程序设计语言的有效命名规范。
field_info表中descriptor_index项的标志
descriptor_index提供了给l字段描述符的CONSTANT_Utf8_info人口的索引。
field_info表中attributes_count和attributes
attributes项是一个由多个attribute_info表组成的列表,而attributes_count表示列表中attribute_info表的数量。每个字段可以拥有任意数量的属性。在这个项中,可能会出现三种由Java虚拟机规范定义的属性:Constant Value、Deprecated和Synthetic。后文将详细介绍Constant Value属性。对于Java虚拟机来说,唯一需要识别的属性是Constant Value属性。虚拟机实现必须忽略无法识别的任何属性。
字段信息的示例
ClassFile {
u4 magic;
u2 minor_version;
u2 major_version;
u2 constant_pool_count;
cp_info contant_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];
}
此外,如果变量有初始化值,其初始化的字节码会被添加到构造函数中。对于以下 Java 代码的编译:
public class SimpleFieldClass {
public int fieldNumber = 100;
}
使用javap命令运行时,会出现一个额外的部分,显示添加到field_info数组中的字段信息:
public int fieldNumber ;
Signature: I
flags: ACC_PUBLIC
初始化的字节码会被添加到构造函数中,以下是示例:
public SimpleFieldClass ();
Signature: ()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 100
7: putfield #2 // Field simpleField:I
10: return
字节码指令分析介绍
- aload_0:初始化的字节码会被添加到构造函数中,用于将局部变量数组槽中的对象引用加载到操作数栈顶。在没有显式构造函数的情况下,默认构造函数会执行类变量(字段)的初始化代码。因此,第一个局部变量指向该变量,通过aload_0操作码将该引用加载到操作数栈中。aload_0是将对象引用加载到操作数栈的一种操作码,其中表示被访问的局部变量数组中的位置,只能是0、1、2或3。类似的操作码还有iload_、lload_、fload_和dload_,分别用于加载int、long、float和double类型的非对象引用。对于索引大于3的局部变量,可以使用iload、lload、fload、dload和aload指令进行加载,这些指令使用一个操作数来指定要加载的局部变量的索引。
- invokespecial:用于调用实例初始化方法和当前类的超类的私有方法和方法。它是方法调用指令集中的一部分,包括invokedynamic,invokeinterface,invokespecial,invokestatic和invokevirtual。其中,invokespecial指令主要用于调用超类的构造函数方法,也就是调用java.lang.Object类的构造函数。
- bipush:将一个字节作为整数添加到操作数堆栈中,具体是将数值100添加到操作数堆栈中。
- putfield:使用指令将操作数堆栈中的值赋给一个特定的字段,该字段在运行时常量池中被引用。代码首先从操作数堆栈中弹出包含该字段的对象,然后弹出一个数值。接下来,通过putfield指令将这两个值应用到字段上,从而更新字段的值。最终,被更新的字段simpleField被设置为数值100。
字节码变量案例
public class SimpleFieldClass {
public int fieldNumber = 100;
}
在内存中执行此操作时,会发生以下情况:
aload_0
bipush
putfield
putfield #2
在Java字节码中,putfield指令有一个操作数,即对运行时常量池中的字段引用。JVM会根据字段的类型来维护常量池,它是一种运行时数据结构,类似于符号表,但包含更多的信息。Java字节码中的字段引用通常较大,无法直接存储在字节码中,因此将其存储在常量池中,并在字节码中通过引用来访问。在类文件创建时,常量池部分包含了以下信息:
- 常量池计数器:记录常量池中的常量个数
- 常量池项:存储了各种类型的常量,包括类名、方法名、字段名、字符串值等信息
通过putfield指令可以将操作数堆栈中的值赋给常量池中引用的字段,从而实现字段的更新。这种优化方式将较大的字段引用存储在常量池中,减小了字节码的体积,并在运行时通过引用访问实际的字段数据。
常量池字节码案例
Constant pool:
#1 = Methodref #4.#16 // java/lang/Object."<init>":()V
#2 = Fieldref #3.#17 // SimpleFieldClass.fieldNumber:I
#3 = Class #13 // SimpleFieldClass
#4 = Class #19 // java/lang/Object
#5 = Utf8 simpleField
#6 = Utf8 I
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 SimpleFieldClass
#14 = Utf8 SourceFile
#15 = Utf8 SimpleFieldClass.java
#16 = NameAndType #7:#8 // "<init>":()V
#17 = NameAndType #5:#6 // fieldNumber:I
#18 = Utf8 LSimpleFieldClass;
#19 = Utf8 java/lang/Object
常量字段(类常量)
在Java类文件中,带有最终修饰符(final)的常量字段被标记为 ACC_FINAL。这个修饰符表示该字段是一个常量,其值在初始化后不能被改变。
常量案例代码
public class SimpleFieldClass {
public final int fieldNumber = 100;
}
字段描述用 ACC_FINAL 增强:
public static final int fieldNumber = 100;
签名: I
标志: acc_public, acc_final
常量值:int 100
但构造函数中的初始化不受影响:
4: aload_0
5: bipush 100
7: putfield #2 // Field fieldNumber :I
总结来说,带有最终修饰符的常量字段在类文件中标记为 ACC_FINAL,它们的值在初始化后不会再被修改。这样的字段可以提高代码的可读性、可维护性和性能,并帮助我们避免一些潜在的错误。因此,在设计和编写代码时,我们应该合理地使用最终修饰符来标记常量字段。
静态变量
带 static 修饰符的静态类变量在类文件中标记为 ACC_STATIC,如下所示:
public static int fieldNumber ;
签名: I
标志: ACC_PUBLIC, ACC_STATIC
在实例构造函数 <init>
中找不到用于初始化静态变量的字节码。相反,静态字段的初始化是类构造函数 的一部分,使用 putstatic 操作数而不是 putfield 操作数。
static {};
签名:()V
标志 ACC_STATIC
代码
stack=1, locals=0, args_size=0
0: bipush 100
2: putstatic #2 // Field fieldNumber :I
5: 返回
总结来说,带有static
修饰符的静态类变量在类文件中被标记为ACC_STATIC
,表示它们是属于类本身的,可以通过类名直接访问,并且在内存中只有一份副本。