likes
comments
collection
share

《深入浅出Java虚拟机 — JVM原理与实战》带你攻克技术盲区,夯实底层基础 —— 吃透class字节码文件技术基底和实现原理(底层结构剖析—基本变量概念)

作者站长头像
站长
· 阅读数 9

@[TOC](带你攻克技术盲区,夯实底层基础 —— 吃透class字节码文件技术基底和实现原理(执行原理剖析))

前言介绍

了解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原理与实战》带你攻克技术盲区,夯实底层基础 —— 吃透class字节码文件技术基底和实现原理(底层结构剖析—基本变量概念)

  • 局部变量是在方法或代码块内部声明的变量,用于临时存储数据,作用域仅限于其声明的方法或代码块。

  • 字段是在类中声明的变量,用于存储对象的状态。字段可以是实例字段(每个对象具有自己的一组字段值)或静态字段(所有对象共享相同的字段值)。

  • 常量字段是在类中声明的不可更改的字段,通常用作常量值,运行时不允许修改。

  • 静态变量是与类本身关联而不是与类的实例相关联的变量,在整个类的生命周期中保持相同的值,可以通过类名直接访问。

局部变量

Java虚拟机(JVM)采用基于堆栈的架构,在执行每个方法时会创建一个包含局部变量的框架。局部变量存储在一个数组中,包括对本方法的引用、方法参数和其他本地定义的变量。对于类方法,方法参数从零开始计数,而对于实例方法,零槽将被保留给予this对象。

局部变量的类型

局部变量可以是任何类型,它们在局部变量数组中占用一个槽。但是,long和double类型占用两个连续的槽,因为它们是双倍宽度的(64位而不是32位)。其他所有类型都占用一个槽。 《深入浅出Java虚拟机 — JVM原理与实战》带你攻克技术盲区,夯实底层基础 —— 吃透class字节码文件技术基底和实现原理(底层结构剖析—基本变量概念) 局部变量数组中的每个槽位都被用于存储一个变量,其中所有类型都占用一个槽位,除了 long 和 double 类型。由于它们是双倍宽度的(64位而不是32位),所以它们需要连续占用两个槽位。

在创建新变量时,其值会被存储到操作数栈上。然后,该值将被移动到本地变量数组中的相应槽位上。对于非基本类型的变量,局部变量槽位中只存储一个引用,该引用指向堆中存储的对象。

局部变量案例

java源码

int i = 4;

class字节码

0: bipush  4
2: istore_0
  • bipush:将一个字节作为整数添加到操作数堆栈中,本例中是将4添加到操作数堆栈中。
  • istore_:是一组操作码之一,用于将一个整数存储到局部变量中。其中, 表示要存储的值在局部变量数组中的位置,只能是 0、1、2 或 3。而 istore 则用于存储大于 3 的值,它的操作数表示要存储的值在局部变量数组中的位置。

在内存中执行此操作

《深入浅出Java虚拟机 — JVM原理与实战》带你攻克技术盲区,夯实底层基础 —— 吃透class字节码文件技术基底和实现原理(底层结构剖析—基本变量概念) 类文件中的每个方法都包含一个局部变量表。如果在某个方法中加入这段代码,那么该方法的局部变量表将包含以下条目。

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表的格式

类型名称数量
u2access_flags1
u2name index1
u2descriptor_index1
u2attributes_count1
attribute_infoattributesattributes _count

field_info表中access_flags项的标志

标志名称设定含义设定者
ACC_PUBLIC0x0001字段设为public类和接口
ACC_PRIVATE0x0002字段设为private只有类
ACC_PROTECTED0x0004字段设为protected只有类
ACC_STATIC0x0008字段设为static类和接口
ACC_FINAL0x0010字段设为final类和接口
ACC_VOLATILE0x0040字段设为volatile只有类
ACC_TRANSIENT0x0080字段设为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

《深入浅出Java虚拟机 — JVM原理与实战》带你攻克技术盲区,夯实底层基础 —— 吃透class字节码文件技术基底和实现原理(底层结构剖析—基本变量概念)

bipush

《深入浅出Java虚拟机 — JVM原理与实战》带你攻克技术盲区,夯实底层基础 —— 吃透class字节码文件技术基底和实现原理(底层结构剖析—基本变量概念)

putfield

putfield #2

在Java字节码中,putfield指令有一个操作数,即对运行时常量池中的字段引用。JVM会根据字段的类型来维护常量池,它是一种运行时数据结构,类似于符号表,但包含更多的信息。Java字节码中的字段引用通常较大,无法直接存储在字节码中,因此将其存储在常量池中,并在字节码中通过引用来访问。在类文件创建时,常量池部分包含了以下信息:

  • 常量池计数器:记录常量池中的常量个数
  • 常量池项:存储了各种类型的常量,包括类名、方法名、字段名、字符串值等信息

《深入浅出Java虚拟机 — JVM原理与实战》带你攻克技术盲区,夯实底层基础 —— 吃透class字节码文件技术基底和实现原理(底层结构剖析—基本变量概念) 通过putfield指令可以将操作数堆栈中的值赋给常量池中引用的字段,从而实现字段的更新。这种优化方式将较大的字段引用存储在常量池中,减小了字节码的体积,并在运行时通过引用访问实际的字段数据。

《深入浅出Java虚拟机 — JVM原理与实战》带你攻克技术盲区,夯实底层基础 —— 吃透class字节码文件技术基底和实现原理(底层结构剖析—基本变量概念)

常量池字节码案例

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,表示它们是属于类本身的,可以通过类名直接访问,并且在内存中只有一份副本。