了解Java代码如何编译成字节码并在JVM上执行是非常重要的。这种理解可以帮助我们理解程序执行时发生的情况,确保语言特性符合逻辑,并在进行讨论时能够全面考虑各种因素和副作用。
本文将深入探讨Java代码编译成字节码并在JVM上执行的过程。如果您对JVM的内部结构和字节码执行过程中使用的不同内存区域有兴趣,建议阅读我之前的JVM专栏《深入浅出JVM原理及调优》。
接下来,我们将介绍不同的Java代码结构,并解释如何将这些结构编译成字节码并在JVM上执行。
本文提供了许多代码示例,并展示了生成的典型字节码。每个字节码指令(或操作码)都标有一个数字,表示它在字节码中的位置。
在Java字节码中,变量是一种用于存储数据的容器,包括局部变量、字段、常量字段和静态变量,这些变量都需通过特定的指令进行声明、初始化和访问,并在字节码中有相应的表示形式。理解Java字节码中的变量对深入了解Java程序至关重要,有助于更好地理解代码的执行过程和内部结构。
局部变量是在方法或代码块内部声明的变量,用于临时存储数据,作用域仅限于其声明的方法或代码块。
字段是在类中声明的变量,用于存储对象的状态。字段可以是实例字段(每个对象具有自己的一组字段值)或静态字段(所有对象共享相同的字段值)。
常量字段是在类中声明的不可更改的字段,通常用作常量值,运行时不允许修改。
静态变量是与类本身关联而不是与类的实例相关联的变量,在整个类的生命周期中保持相同的值,可以通过类名直接访问。
Java虚拟机(JVM)采用基于堆栈的架构,在执行每个方法时会创建一个包含局部变量的框架。局部变量存储在一个数组中,包括对本方法的引用、方法参数和其他本地定义的变量。对于类方法,方法参数从零开始计数,而对于实例方法,零槽将被保留给予this对象。
局部变量可以是任何类型,它们在局部变量数组中占用一个槽。但是,long和double类型占用两个连续的槽,因为它们是双倍宽度的(64位而不是32位)。其他所有类型都占用一个槽。
局部变量数组中的每个槽位都被用于存储一个变量,其中所有类型都占用一个槽位,除了 long 和 double 类型。由于它们是双倍宽度的(64位而不是32位),所以它们需要连续占用两个槽位。
在创建新变量时,其值会被存储到操作数栈上。然后,该值将被移动到本地变量数组中的相应槽位上。对于非基本类型的变量,局部变量槽位中只存储一个引用,该引用指向堆中存储的对象。
int i = 4;
0: bipush 4
2: istore_0
bipush
:将一个字节作为整数添加到操作数堆栈中,本例中是将4添加到操作数堆栈中。
类文件中的每个方法都包含一个局部变量表。如果在某个方法中加入这段代码,那么该方法的局部变量表将包含以下条目。
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表的详细格式:
类型 | 名称 | 数量 |
---|---|---|
u2 | access_flags | 1 |
u2 | name index | 1 |
u2 | descriptor_index | 1 |
u2 | attributes_count | 1 |
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 | 字段设为transient | 只有类 |
类(不包括接口)中声明的字段必须使用ACC_PUBLIC、ACC_PRIVATE、或ACC_PROTECTED这三个标志之一。ACC_FINAL和ACC_VOLATILE不能同时设置在同一个字段上。而在接口中声明的字段则必须且只能使用ACC_PUBLIC、ACC_STATIC和ACC_FINAL这三种标志。
name_index项提供了一个索引,用于访问CONSTANT_Uf8_info表中的入口,该入口包含了字段的简单名称(而不是全限定名)。在class文件中,每个字段的名称都必须符合Java程序设计语言的有效命名规范。
descriptor_index提供了给l字段描述符的CONSTANT_Utf8_info人口的索引。
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."":()V
4: aload_0
5: bipush 100
7: putfield #2 // Field simpleField:I
10: return
public class SimpleFieldClass {
public int fieldNumber = 100;
}
在内存中执行此操作时,会发生以下情况:
putfield #2
在Java字节码中,putfield指令有一个操作数,即对运行时常量池中的字段引用。JVM会根据字段的类型来维护常量池,它是一种运行时数据结构,类似于符号表,但包含更多的信息。Java字节码中的字段引用通常较大,无法直接存储在字节码中,因此将其存储在常量池中,并在字节码中通过引用来访问。在类文件创建时,常量池部分包含了以下信息:
通过putfield指令可以将操作数堆栈中的值赋给常量池中引用的字段,从而实现字段的更新。这种优化方式将较大的字段引用存储在常量池中,减小了字节码的体积,并在运行时通过引用访问实际的字段数据。
Constant pool:
#1 = Methodref #4.#16 // java/lang/Object."":()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
#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 // "":()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
在实例构造函数
中找不到用于初始化静态变量的字节码。相反,静态字段的初始化是类构造函数 的一部分,使用 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
,表示它们是属于类本身的,可以通过类名直接访问,并且在内存中只有一份副本。