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

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

时间:2023-08-21 10:32:30浏览次数:62  
标签:ACC Java 字节 常量 虚拟机 局部变量 字段 原理 底层

前言介绍

了解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字节码文件技术基底和实现原理(底层结构剖析—基本变量概念)_字段_02

局部变量数组中的每个槽位都被用于存储一个变量,其中所有类型都占用一个槽位,除了 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字节码文件技术基底和实现原理(底层结构剖析—基本变量概念)_字段_03

类文件中的每个方法都包含一个局部变量表。如果在某个方法中加入这段代码,那么该方法的局部变量表将包含以下条目。

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

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

bipush

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

putfield
putfield #2

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

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

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

通过putfield指令可以将操作数堆栈中的值赋给常量池中引用的字段,从而实现字段的更新。这种优化方式将较大的字段引用存储在常量池中,减小了字节码的体积,并在运行时通过引用访问实际的字段数据。

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

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

标签:ACC,Java,字节,常量,虚拟机,局部变量,字段,原理,底层
From: https://blog.51cto.com/alex4dream/7171669

相关文章

  • 高速信号处理处理卡设计原理图:501-基于TMS320C6670的软件无线电核心板
    基于TMS320C6670的软件无线电核心板一、板卡概述     北京太速科技自主研发的TMS320C6670核心板,采用TI KeyStone系列的四核定点/浮点DSP TMS320C6670作主处理器。板卡引出处理器的全部信号引脚,便于客户二次开发,降低了硬件的开发难度和时间成本。板卡满足工......
  • day01-运维介绍与虚拟机安装-20230820
     1.解释我们正在使用哪些互联网行业的软件,移动端?PC端? (1)平台不一样视觉范围更广,可设计的地方更多,设计性更强,相对来说容错度更高一些。操作局限性大,在设计上可用空间显得尤为珍贵,避免原件过小过近。(2)操作系统不一样对于会员系统、视频和音乐、购物支付等功能都进行了精简,使......
  • VMware vSphere虚拟机挂起迁移保持进程状态
    开发/测试环境:172.16.3.133,上面跑了很多个组件和43个tomcat应用。Esxi主机:172.16.12.115(上面虚拟机:172.16.3.133)Esxi主机:172.16.12.111(把172.16.3.133迁移到这台机上)测试目的:在虚拟机挂起的时候,把虚拟机迁移到其他esxi主机上,保存好服务的状态,无需重新所有的应用和组件,提高效率。 1......
  • Rocky虚拟机(Three Days)用户与组管理与目录/文件权限
    ThreeDays一、用户管理1、概述Linux系统是一个多用户多任务的分时操作系统,任何一个要使用系统资源的用户,都必须首先向系统管理员申请一个账号,然后以这个账号的身份进入系统用户的账号一方面可以帮助系统管理员对使用系统的用户进行跟踪,并控制他们对系统资源的访问;另一方......
  • 深入研究高性能数据库连接池的实现原理与优化策略
    在现代的后端应用开发中,数据库连接池是提高性能和可伸缩性的关键组件之一。本文将深入探讨数据库连接池的实现原理,涵盖Java和Python示例,并介绍一些常见的连接池优化策略。数据库连接池的作用数据库连接池是一种维护和管理数据库连接的技术,它通过预先创建一组数据库连接,并将这些连接......
  • 深入理解数据库索引优化策略与原理
    在后端开发领域,数据库索引是优化查询性能的关键因素之一。本文将深入探讨数据库索引的优化策略和原理,重点关注Java与Python开发环境中的实际应用,同时结合Nginx与Elasticsearch等技术,为读者提供深奥的干货内容。1.索引概述与原理数据库索引是一种用于加速数据检索操作的数据结构。......
  • 虚拟机linux无法实现与原机windows之间的复制和拖拽文件--已解决
    在虚拟机(我用的是Ubuntu)桌面右键打开终端,输入第一行sudoaptinstallopen-vm-tools中间全部yes,然后关闭终端然后再次在桌面打开终端,输入sudoaptinstallopen-vm-tools-desktop中间全部yes完成......
  • esxi虚拟机安装群晖,并直通核显给群晖,实现核显硬解以N5105为例。
    n5105一直又个遗憾就是不能虚拟机安装群晖并硬解,之前的硬解方案大多数都是套娃式的解决方案,没有一个是可以真正实现群晖下直接硬解的。当然遗憾的还不是N5105,可以说英特尔11-13代的cpu的核显都是不被支持的,包括N6005、J6412、J6413、N100、N200、N305等都是一个情况。本次也是一......
  • 【深度学习 | CNN】“深入解析卷积神经网络与反卷积:从生活案例到原理的全面指南” (从
    ......
  • POP3协议的历史及其工作原理
    POP3,全名为“PostOfficeProtocol-Version3”,即“邮局协议版本3”。是TCP/IP协议族中的一员,由RFC1939定义。POP3的具体历史可以追溯到1984年,由J.K.Reynolds带领的团队研发出了POP3协议的前身,POP1和POP2。到了1998年,POP3成为Internet标准,并持续发展和改进。虽然POP4曾被提出,但......