JVM--内存结构

目录

1. PC Register(程序计数器)

1.1 定义

1.2 工作原理

1.3 特点

1.4 应用

2.虚拟机栈

2.1定义与特性

2.2内存模型

2.3工作原理

2.4异常处理

2.5应用场景

2.6 Slot 复用

2.7 动态链接详解

1. 栈帧与动态链接

动态链接的作用:

2. 为什么需要动态链接?

符号引用的特性:

动态链接 VS 静态链接:

3. 动态链接的过程

(1)从栈帧中获取常量池引用:

(2)解析符号引用:

(3)执行方法调用:

4. 动态链接的优化

具体优化策略:

3 Native Method Stack(本地方法栈)

3.1 native 关键字

3.2 定义与特性

3.3 结构与工作原理

3.4 与Java虚拟机栈的区别

3.5 优化与注意事项

4.堆

Java 7及之前

Java 8

Java 9及之后

总结

5.方法区

扩展知识点

1.每个方法所在类都有自己的运行时常量池


注:本文参考多位博主作品,供大家一起学习进步。

JVM内存结构由五部分组成如下:

  • Method Area(方法区)
  • Heap(堆)
  • JVM Stack(虚拟机栈)
  • PC Register(程序计数器)
  • Native Method Stacks(本地方法栈)

1. PC Register(程序计数器)

1.1 定义

PC Register,即程序计数器(Program Counter Register),是计算机处理器中的一个关键寄存器,也被称为指令计数器。它主要用于存放下一条指令所在单元的地址,是计算机能够连续执行指令的重要机制之一。以下是关于PC Register的详细概念介绍:

基本概念

  • 定义程序计数器是用于存放下一条指令所在单元的地址的地方。当执行一条指令时,CPU会根据PC中存放的指令地址,将指令由内存取到指令寄存器中,此过程称为“取指令”。
  • 作用:程序计数器是程序控制流的指示器,它保证了程序能够按照预定的顺序执行。通过不断更新PC中的地址,CPU能够连续地取出并执行指令,从而实现程序的连续运行。

1.2 工作原理

  • 取指令:当CPU需要执行下一条指令时,它会首先查看PC中的地址,然后根据这个地址从内存中取出相应的指令。
  • 执行指令:指令被取出后,CPU会对其进行译码和执行。在执行指令的过程中,PC中的地址可能会根据指令的类型和需要进行更新。
  • 更新PC:对于大多数顺序执行的指令,PC中的地址会自动加1(或加上指令的字节数),以指向下一条指令的地址。如果遇到跳转或分支指令,PC中的地址会根据指令的要求进行更新,以指向新的指令地址。

1.3 特点

  • 线程私有:在JVM等环境中,每个线程都有自己的程序计数器,它是线程私有的。这保证了在多线程环境下,每个线程都能够独立地执行自己的程序,而不会相互干扰。
  • 生命周期:程序计数器的生命周期与线程的生命周期保持一致。当线程创建时,程序计数器被初始化;当线程结束时,程序计数器也随之销毁。
  • 存储区域:程序计数器是一块很小的内存空间,几乎可以忽略不计。同时,它也是运算速度最快的存储区域之一。

1.4 应用

  • 程序控制:程序计数器是实现程序控制流(如分支、循环、跳转等)的关键机制之一。通过不断更新PC中的地址,CPU能够按照预定的程序流程执行指令。
  • 异常处理:在程序执行过程中,如果遇到异常情况(如除数为零、数组越界等),程序计数器会记录出错时的指令地址,以便系统能够定位并处理错误。
  • 线程恢复:在多线程环境中,当线程被中断或挂起后,程序计数器会记录线程被中断时的指令地址。当线程恢复执行时,CPU会根据程序计数器中的地址继续执行线程的程序。

综上所述,PC Register(程序计数器)是计算机处理器中的一个重要寄存器,它通过存放下一条指令的地址来保证程序的连续执行。在程序控制、异常处理和线程恢复等方面都发挥着重要作用。

2.虚拟机栈

虚拟机栈,特别是Java虚拟机栈(Java Virtual Machine Stack),是Java虚拟机中用于描述Java方法执行时内存模型的一个重要组成部分。以下是关于虚拟机栈的详细解释:

2.1定义与特性

  • 定义:虚拟机栈是线程私有的,它的生命周期与线程相同。每个线程在创建时都会创建一个虚拟机栈,用于存储该线程执行方法时的各种信息
  • 特性
    • 线程私有:每个线程都有自己独立的虚拟机栈,互不干扰。
    • 生命周期:与线程的生命周期一致,线程创建时创建,线程结束时销毁。
    • 存储内容:主要存储局部变量表、操作数栈、动态链接、方法出口等信息。

2.2内存模型

  • 栈帧(Stack Frame)虚拟机栈由多个栈帧组成,每个栈帧对应着一次方法调用。当一个方法被调用时,就会创建一个新的栈帧,并将其压入虚拟机栈的栈顶。当方法执行完毕后,对应的栈帧就会从虚拟机栈中弹出,并销毁。
  • 局部变量表:是栈帧中用于存储方法参数和局部变量的一块内存区域局部变量表中的变量只在当前方法调用中有效,方法执行完毕后,随着栈帧的销毁而销毁。
  • 操作数栈:主要用于保存计算过程的中间结果,以及作为计算过程中变量临时的存储空间。它是一个后进先出(LIFO)的栈,通过标准的入栈和出栈操作来访问数据。操作数栈的元素可以是任意的Java数据类型。方法刚开始执行时,操作数栈是空的,在方法执行过程中,通过字节码指令对操作数栈进行压栈和出栈的操作。通常进行算数运算的时候是通过操作数栈来进行的,又或者是在调用其他方法的时候通过操作数栈进行参数传递。操作数栈可以理解为栈帧中用于计算的临时数据存储区。
  • 动态链接:每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,用于支持方法调用过程中的动态连接。
  • 方法出口:包括方法正常退出时的返回地址,以及异常退出时的异常处理器信息。

2.3工作原理

  • 方法调用:当一个方法被调用时,会创建一个新的栈帧,并将其压入虚拟机栈的栈顶。然后,根据方法的字节码指令,执行引擎会操作这个栈帧中的局部变量表和操作数栈,完成方法的执行。
  • 方法执行:在执行过程中,如果需要调用其他方法,会创建新的栈帧并压入栈顶,当前栈帧成为非活动栈帧。当被调用的方法执行完毕后,其对应的栈帧会从栈顶弹出,之前的栈帧重新成为活动栈帧。
  • 方法返回当一个方法执行完毕后,会将其返回值(如果有的话)压入调用者的操作数栈中,并弹出当前栈帧。然后,调用者的执行引擎会继续执行下一条指令。

2.4异常处理

  • 如果在方法执行过程中遇到异常,并且该异常在当前方法内没有得到处理,那么会导致当前方法退出,并弹出对应的栈帧。同时,会根据异常的类型和异常表中的信息,找到相应的异常处理器进行处理。
  • 程序运行中虚拟机栈可能会出现两种错误:

StackOverFlowError:若栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度的时候,就抛出 StackOverFlowError 错误。

OutOfMemoryError:如果栈的内存大小可以动态拓展(Classic 虚拟机),当虚拟机在动态拓展栈时无法申请到足够的内存空间,则抛出 OutOfMemoryError 异常。如果栈的内存大小不可以动态拓展(HotSpot 虚拟机),线程申请栈空间失败也会出现OutOfMemoryError 异常。

2.5应用场景

虚拟机栈是Java虚拟机中非常重要的一个组成部分,它支持着Java程序的运行。在多线程环境下,每个线程都有自己独立的虚拟机栈,这保证了线程之间的独立性和安全性。同时,虚拟机栈也是实现方法调用、参数传递、局部变量存储等功能的关键机制之一。

综上所述,虚拟机栈是Java虚拟机中用于描述Java方法执行时内存模型的一个重要组成部分,它支持着Java程序的运行和线程之间的独立执行。

局部变量的容量以变量槽(Variable Slot)为最小单位,每个变量槽最大存储32位的数据类型。对于64位的数据类型(long、double),JVM 会为其分配两个连续的变量槽来存储。以下简称 Slot 。

JVM 通过索引定位的方式使用局部变量表,索引的范围从0开始至局部变量表中最大的 Slot 数量。普通方法与 static 方法在第 0 个槽位的存储有所不同。非 static 方法的第 0 个槽位存储方法所属对象实例的引用。

2.6 Slot 复用

为了尽可能的节省栈帧空间,局部变量表中的 Slot 是可以复用的。方法中定义的局部变量,其作用域不一定会覆盖整个方法。当方法运行时,如果已经超出了某个变量的作用域,即变量失效了,那这个变量对应的 Slot 就可以交给其他变量使用,也就是所谓的 Slot 复用。通过一个例子来理解变量“失效”。

当虚拟机运行 test 方法,就会创建一个栈帧,并压入到当前线程的栈中。当运行到 int a = 66时,在当前栈帧的局部变量中创建一个 Slot 存储变量 a,当运行到 int b = 55时,此时已经超出变量 a 的作用域了(变量 a 的作用域在{}所包含的代码块中),此时 a 就失效了,变量a 占用的 Slot 就可以交给b来使用,这就是 Slot 复用。

凡事有利弊。Slot 复用虽然节省了栈帧空间,但是会伴随一些额外的副作用。比如,Slot 的复用会直接影响到系统的垃圾收集行为。

上段代码很简单,先向内存中填充了 64M 的数据,然后通知虚拟机进行垃圾回收。为了更清晰的查看垃圾回收的过程,我们再虚拟机的运行参数中加上“-verbose:gc”,这个参数的作用就是打印 GC 信息。

打印的GC信息如下:

可以看到虚拟机没有回收这 64M 内存。为什么没有被回收?其实很好理解,当执行 System.gc() 方法时,变量 placeholder 还在作用域范围之内,虚拟机是不会回收的,它还是“有效”的。

我们对上面的代码稍作修改,使其作用域“失效”。

当运行到 System.gc() 方法时,变量 placeholder 的作用域已经失效了。它已经“无用”了,虚拟机会回收它所占用的内存了吧?

运行结果:

发现虚拟机还是没有回收 placeholder 变量占用的 64M 内存。为什么所想非所见呢?在解释之前,我们再对代码稍作修改。在System.gc()方法执行之前,加入一个局部变量。

在 System.gc() 方法之前,加入 int a = 0,再执行方法,查看垃圾回收情况。

发现 placeholder 变量占用的64M内存空间被回收了,如果不理解局部变量表的Slot复用,很难理解这种现象的。

而 placeholder 变量能否被回收的关键就在于:局部变量表中的 Slot 是否还存有关于 placeholder 对象的引用。

第一次修改中,限定了 placeholder 的作用域,但之后并没有任何对局部变量表的读写操作,placeholder 变量在局部变量表中占用的Slot没有被其它变量所复用,所以作为 GC Roots 一部分的局部变量表仍然保持着对它的关联。所以 placeholder 变量没有被回收。

第二次修改后,运行到 int a = 0 时,已经超过了 placeholder 变量的作用域,此时 placeholder 在局部变量表中占用的Slot可以交给其他变量使用。而变量a正好复用了 placeholder 占用的 Slot,至此局部变量表中的 Slot 已经没有 placeholder 的引用了,虚拟机就回收了placeholder 占用的 64M 内存空间。

2.7 动态链接详解

1. 栈帧与动态链接

  • 每个 栈帧(Stack Frame) 都与一个方法调用相对应,用于保存方法执行的相关信息。
  • 栈帧中的一部分数据结构是 动态链接信息,它包含了一个指向 运行时常量池(Runtime Constant Pool) 的引用。
动态链接的作用:

动态链接用于在方法调用时解析常量池中的符号引用,从而获得具体的方法或字段的实际地址。


2. 为什么需要动态链接?

Java 中的方法调用是通过 符号引用(Symbolic Reference)来表示的,而实际执行需要将这些符号引用解析为具体的 内存地址(也称为直接引用)。这就是动态链接的主要任务。

符号引用的特性:
  • 符号引用 是一种间接的、抽象的标识,例如类名、方法名、描述符等。
  • 动态链接的过程是将符号引用解析为方法的直接引用,这通常涉及到运行时查找和验证。
动态链接 VS 静态链接:
  • 静态链接 在编译期确定引用关系(如 C/C++ 的链接器)。
  • 动态链接 在运行时根据上下文环境解析符号引用,使得程序具有更大的灵活性和动态特性。

3. 动态链接的过程

在 JVM 中,方法调用的指令(例如 invokevirtualinvokestatic 等)会触发动态链接。以下是动态链接的主要过程:

(1)从栈帧中获取常量池引用:

每个栈帧都持有对所属方法所在类的运行时常量池的引用,动态链接通过这个引用查找符号。

(2)解析符号引用:

动态链接会检查运行时常量池中的符号引用,并将其解析为:

  • 具体的类(字段或方法所属的类)。
  • 方法或字段的实际地址。
(3)执行方法调用:
  • 找到实际地址后,JVM 使用该地址执行方法调用。

4. 动态链接的优化

为了提高性能,动态链接过程中会利用 缓存机制直接引用,避免每次调用都重新解析符号。

具体优化策略:
  1. 静态方法和私有方法:

    • 因为它们在编译期就可以确定调用关系,因此使用 静态绑定
  2. 虚方法(Virtual Method):

    • 使用 虚方法表(vtable) 来加速方法的动态查找。
  3. 内联缓存(Inline Cache):

    • 在热点代码中缓存方法的直接引用,提高调用效率。

3 Native Method Stack(本地方法栈)

3.1 native 关键字

在 Java 中,native 关键字用于声明一个方法为本地方法,意味着该方法的实现将在本地代码完成,通常是 C 或 C++ 代码。使用 native关键字可以允许 Java 程序调用本地代码库中的函数,从而拓展 Java 的功能,并利用已有的本地代码资源。然而,使用 native 关键字需要谨慎,并注意安全性、性能、兼容性、维护性和资源限制等方面的问题。

3.2 定义与特性

  • 定义:本地方法栈是JVM为支持本地方法调用而设置的一个内存区域。本地方法是指使用其他编程语言(如C、C++等)编写的,通过JNI(Java Native Interface)技术与Java代码进行交互的方法。
  • 特性
    • 线程私有:与Java虚拟机栈类似,本地方法栈也是线程私有的,每个线程都拥有自己独立的本地方法栈,不与其他线程共享。
    • 内存管理:本地方法栈的大小通常可以通过JVM参数进行设置,但具体实现可能因JVM的不同而有所差异。
    • 异常处理:本地方法栈也可以捕获和处理异常,当本地方法抛出异常时,JVM会在本地方法栈上找到相应的异常处理器并进行处理。

3.3 结构与工作原理

  • 结构:本地方法栈的结构与Java虚拟机栈类似,每个栈帧包含了本地方法的相关信息,如局部变量表、操作数栈、返回地址等。局部变量表用于存储本地方法的局部变量和参数,操作数栈用于执行本地方法中的操作指令。
  • 工作原理:当Java程序调用本地方法时,JVM会在本地方法栈上为该方法创建一个新的栈帧,用于保存本地方法的局部变量、参数、返回值和临时数据。在方法的执行过程中,JVM会根据需要创建更多的栈帧来支持方法的执行。当方法执行完毕后,相应的栈帧会被弹出本地方法栈,并释放其所占用的内存资源。

3.4 与Java虚拟机栈的区别

  • 用途:Java虚拟机栈用于执行Java方法的调用和返回,而本地方法栈则用于执行本地方法的调用和返回。
  • 存储内容:Java虚拟机栈主要存储Java方法的参数、局部变量和返回值等,而本地方法栈则主要存储本地方法的参数、局部变量和返回值等。
  • 语言支持:Java虚拟机栈中的方法是Java语言编写的,而本地方法栈中的方法是使用非Java语言(如C、C++)编写的。

3.5 优化与注意事项

  • 优化本地方法栈:为了减少本地方法栈的开销,应尽量减少不必要的本地方法调用,并合理设置本地方法栈的大小。同时,可以使用JIT编译器优化频繁调用的本地方法,以提高程序的执行效率。
  • 注意事项:在编写Java程序时,如果涉及到本地方法的调用,需要特别注意本地方法栈的大小和异常处理机制,以避免栈溢出等问题。此外,由于本地方法可能导致Java程序失去平台独立性并增加代码调试和维护的难度,因此在选择是否使用本地方法时需要谨慎考虑。

总之,本地方法栈是JVM中用于支持本地方法执行的重要组件,它通过为本地方法提供独立的内存区域和栈帧结构来支持Java程序与本地代码的交互。了解本地方法栈的工作原理和特性对于编写高效、稳定的Java程序具有重要意义。

4.堆

堆是Java虚拟机所管理的内存中最大的一块存储区域。堆内存被所有线程共享。主要存放使用new关键字创建的对象。所有对象实例以及数组都要在堆上分配。垃圾收集器就是根据GC算法,收集堆上对象所占用的内存空间(收集的是对象占用的空间而不是对象本身)。

Java堆分为年轻代(Young Generation)和老年代(Old Generation);年轻代又分为伊甸园(Eden)和幸存区(Survivor区);幸存区又分为From Survivor空间和 To Survivor空间。

年轻代存储“新生对象”,我们新创建的对象存储在年轻代中。当年轻内存占满后,会触发Minor GC,清理年轻代内存空间。

老年代存储长期存活的对象和大对象。年轻代中存储的对象,经过多次GC后仍然存活的对象会移动到老年代中进行存储。老年代空间占满后,会触发Full GC

注:Full GC是清理整个堆空间,包括年轻代和老年代。如果Full GC之后,堆中仍然无法存储对象,就会抛出OutOfMemoryError异常。

Java堆设置常用参数

在Java中,不同版本对堆和方法区(在Java 8及之后称为“元空间”)的存储内容和结构有一些不同。以下是不同Java版本的主要区别:

Java 7及之前

  • 堆 (Heap): 存储所有的对象实例以及数组,包括类的实例变量。堆分为新生代(Young Generation)和老年代(Old Generation),用来管理对象的生命周期。
    • 新生代:新创建的对象会分配在新生代中,由Eden区和两个Survivor区组成。
    • 老年代:当对象在新生代中存活一段时间或超过新生代的容量时,会被转移到老年代。
  • 方法区 (Method Area): 存储类的元数据(Class Metadata)、常量池、静态变量和JIT编译后的代码。Java 7及之前使用了**永久代(Permanent Generation)**来实现方法区。
    • 永久代 (PermGen):存储类信息、静态变量和字符串常量池。
    • 字符串常量池在Java 7之前存放于永久代中。

Java 8

  • 堆 (Heap):仍然用于存储对象实例及其实例变量。
  • 元空间 (Metaspace): Java 8移除了永久代,将方法区改为“元空间”。
    • 元空间存储类元数据,并且不再占用堆空间,而是直接使用本地内存(Native Memory)。
    • 字符串常量池移动到了堆内存中,因此不再受限于永久代的大小。
    • 这种改进避免了因为永久代大小不足导致的内存错误(如OutOfMemoryError: PermGen space),并提高了元数据存储的灵活性。

Java 9及之后

  • 堆 (Heap)元空间 (Metaspace):Java 9及之后的版本仍然遵循Java 8的内存结构。
    • 类数据共享 (Class Data Sharing, CDS):Java 9引入CDS来优化类加载机制,允许类的元数据在不同的JVM实例之间共享,从而节省内存并加速启动。
    • 动态CDS (Dynamic CDS):Java 10进一步扩展了CDS,可以动态地生成CDS归档文件。

总结

  • Java 7及之前:使用堆(Heap)和永久代(PermGen)。
  • Java 8:移除永久代,引入元空间(Metaspace)。
  • Java 9及之后:优化CDS机制,进一步提升内存使用效率和启动性能。

5.方法区

方法区同 Java 堆一样是被所有线程共享的区间,用于存储已被虚拟机加载的类信息、常量、静态变量、即编译器编译后的代码。更具体的说,静态变量+常量+类信息(版本、方法、字段等)+运行时常量池存在方法区中。常量池是方法区的一部分

当类被加载时,类的定义(包括类的字节码、类的方法、字段等)会被存储在方法区。

注:JDK1.8之前方法区称为永恒代并位于堆内存中,JDK1.8及以后 使用元空间 MetaSpace 替代方法区,元空间并不在 JVM中,而是使用本地内存。元空间两个参数:

MetaSpaceSize:初始化元空间大小,控制发生GC阈值

MaxMetaspaceSize : 限制元空间大小上限,防止异常占用过多物理内存

常量池中存储编译器生成的各种字面量和符号引用。字面量就是Java中常量的意思。比如文本字符串,final修饰的常量等。符号引用则包括类和接口的全限定名,方法名和描述符,字段名和描述符等。

常量池有什么用 ?

优点:常量池避免了频繁的创建和销毁对象而影响系统性能,其实现了对象的共享。

举个栗子: Integer 常量池(缓存池),和字符串常量池

Integer常量池:

我们知道 == 基本数据类型比较的是数值,而引用数据类型比较的是内存地址

i1 和 i2 使用 new 关键字,每 new 一次都会在堆上创建一个对象,所以 i1 == i2 为 false。

i3 == i4 为什么是 true 呢?Integer i3 = 66 实际上有一步装箱的操作,即将 int 型的 66 装箱成 Integer,通过 Integer 的 valueOf 方法。

Integer 的 valueOf 方法很简单,它判断变量是否在 IntegerCache 的最小值(-128)和最大值(127)之间,如果在,则返回常量池中的内容,否则 new 一个 Integer 对象。

而 IntegerCache 是 Integer的静态内部类,作用就是将 [-128,127] 之间的数“缓存”在 IntegerCache 类的 cache 数组中,valueOf 方法就是调用常量池的 cache 数组,不过是将 i3、i4 变量引用指向常量池中,没有真正的创建对象。而new Integer(i)则是直接在堆中创建对象。

IntegerCache 类中,包含一个构造方法,三个静态变量:low最小值、high最大值、和Integer数组,还有一个静态代码块。静态代码块的作用就是在 IntegerCache 类加载的时候,对high最大值以及 Integer 数组初始化。也就是说当 IntegerCache 类加载的时候,最大最小值,和 Integer 数组就已经初始化好了。这个 Integer 数组其实就是包含了 -128到127之间的所有值。

IntegerCache 源码

而 i5 == i6 为 false,就是因为 150 不在 Integer 常量池的最大最小值之间【-128,127】,从而 new 了一个对象,所以为 false。

再看一段拆箱的代码。

由于 i1 和 i2 是 Integer 对象,是不能使用+运算符的。首先 i1 和 i2 进行自动拆箱操作,拆箱成int后再进行数值加法运算。i3 也是拆箱后再与之比较数值是否相等的。所以 i3 == i1+i2 其实是比较的 int 型数值是否相等,所以为true。


String 是由 final 修饰的类,是不可以被继承的。通常有两种方式来创建对象。

第一种使用 new 创建的对象,存放在堆中。每次调用都会创建一个新的对象。

第二种先在栈上创建一个 String 类的对象引用变量 str,然后通过符号引用去字符串常量池有没有 “abcd”,如果没有,则将“abcd”存放到字符串常量池中并将栈上的 str 变量引用指向常量池中的“abcd”。如果常量池中已经有“abcd”了,则不会再常量池中创建“abcd”,而是直接将 str 引用指向常量池中的“abcd”。

对于 String 类,equals 方法用于比较字符串内容是否相同; == 号用于比较内存地址是否相同,即是否指向同一个对象。通过代码验证上面理论。

首先在栈上存放变量引用 str1,然后通过符号引用去常量池中找是否有 abcd,没有,则将 abcd 存储在常量池中,然后将 str1 指向常量池的 abcd。当创建 str2 对象,去常量池中发现已经有 abcd 了,就将 str2 引用直接指向 abcd 。所以str1 == str2,指向同一个内存地址。

str1 和 str2 使用 new 创建对象,分别在堆上创建了不同的对象。两个引用指向堆中两个不同的对象,所以为 false。

关于字符串 + 号连接问题:

对于字符串常量的 + 号连接,在程序编译期,JVM就会将其优化为 + 号连接后的值。所以在编译期其字符串常量的值就确定了

关于字符串引用 + 号连接问题:

对于字符串引用的 + 号连接问题,由于字符串引用在编译期是无法确定下来的,在程序的运行期动态分配并创建新的地址存储对象

对于上边代码,str3 等于 str1 引用 + 字符串常量“b”,在编译期无法确定,在运行期动态的分配并将连接后的新地址赋给 str3,所以 str2 和 str3 引用的内存地址不同,所以 str2 == str3 结果为 false

通过 jad 反编译工具,分析上述代码到底做了什么。编译指令如下:

经过 jad 反编译工具反编译代码后,代码如下

发现 new 了一个 StringBuilder 对象,然后使用 append 方法优化了 + 操作符。new 在堆上创建对象,而 String s1=“ab”则是在常量池中创建对象,两个应用所指向的内存地址是不同的,所以 s1 == s2 结果为 false。

注:我们已经知道了字符串引用的 + 号连接问题,其实是在运行期间创建一个 StringBuilder 对象,使用其 append 方法将字符串连接起来。这个也是我们开发中需要注意的一个问题,就是尽量不要在 for 循环中使用 + 号来操作字符串。看下面一段代码:

在 for 循环中使用 + 连接字符串,每循环一次,就会新建 StringBuilder 对象,append 后就“抛弃”了它。如果我们在循环外创建StringBuilder 对象,然后在循环中使用 append 方法追加字符串,就可以节省 n-1 次创建和销毁对象的时间。所以在循环中连接字符串,一般使用 StringBuilder 或者 StringBuffer,而不是使用 + 号操作。

使用final修饰的字符串

final 修饰的变量是一个常量,编译期就能确定其值。所以 str1 + "b"就等同于 "a" + "b",所以结果是 true。

String对象的intern方法。

通过前面学习我们知道,s1+s2 实际上在堆上 new 了一个 StringBuilder 对象,而 s 在常量池中创建对象 “ab”,所以 s3 == s 为 false。但是 s3 调用 intern 方法,返回的是s3的内容(ab)在常量池中的地址值。所以 s3.intern() == s 结果为 true。

扩展知识点

1.每个方法所在类都有自己的运行时常量池

在 JVM 中,每个类(包括接口)都会有一个 运行时常量池(Runtime Constant Pool)。运行时常量池是 方法区 中的一部分,用来存储与类或接口相关的常量信息,包括编译时生成的各种 字面量符号引用。以下是详细说明:


1. 每个类的运行时常量池

  • 当一个类被 加载到 JVM 时,JVM 会从该类的 class 文件 中提取常量池(Constant Pool)并将其放入内存中。
  • 每个类或接口都有自己独立的运行时常量池,用来存储和它相关的信息。
常量池的内容:
  • 编译期常量(Compile-time Constants):
    • 字符串字面量、数字、布尔值等(例如 "hello", 3.14)。
  • 符号引用(Symbolic References):
    • 类的全限定名。
    • 字段的名称和描述符。
    • 方法的名称和描述符。

2. 运行时常量池的功能

运行时常量池的核心功能是支持动态链接和方法调用。

(1)符号引用解析
  • 符号引用是对类、字段或方法的一种逻辑描述(如名称和描述符)。
  • JVM 在运行时会从运行时常量池中查找符号引用并解析为实际的内存地址(直接引用)。
(2)动态生成常量
  • 运行时常量池可以在运行时存储新的常量值,例如通过 String.intern() 方法动态生成字符串常量。
(3)支持方法调用
  • 方法调用指令(如 invokevirtual, invokestatic)需要通过运行时常量池获取目标方法的符号引用,并在必要时解析为直接引用。

3. 为什么每个类有独立的运行时常量池?

  1. 类的独立性:

    • 每个类或接口都有独立的常量池,因为它们的常量信息是独立的,互不干扰。
  2. 动态链接需求:

    • 不同类加载器加载的类可以有不同的运行时常量池,用于支持动态链接和类隔离机制。
  3. 内存优化:

    • JVM 通过让每个类独立管理自己的常量池,避免不必要的全局资源共享,提高效率。

4. 运行时常量池与 Class 文件常量池的关系

  • Class 文件常量池
    • 编译后的 .class 文件中包含一个常量池表,存储的是符号引用和字面量。
  • 运行时常量池
    • 当类加载到 JVM 后,常量池会被载入运行时常量池,供 JVM 使用。
    • 在运行时常量池中,符号引用可能被解析为直接引用。

5. 常见的运行时常量池操作

  • 动态链接:

    • JVM 从运行时常量池中解析符号引用,找到类、字段或方法的具体内存地址。
  • 字符串常量池:

    • 字符串字面量(如 "hello")在运行时常量池中存储,并可能被转移到 JVM 的字符串池(String Pool)。
  • 异常处理:

    • 异常表中可能包含符号引用,用于定位异常处理的目标类或方法。

6. 示例

假设一个简单的类:

Example 类的运行时常量池中,可能包含:

  • 类名 "Example"
  • 方法名和描述符:"sayHello"()V(无参数,无返回值)。
  • Systemout 的符号引用。
  • 字符串字面量 "Hello, World!"

当 JVM 运行 sayHello 方法时,会从运行时常量池中查找这些信息并完成解析。


7. 总结

  • 每个类确实有独立的运行时常量池
  • 它主要用于符号引用解析、动态链接和常量存储。
  • JVM 的设计确保了常量池的独立性,便于类的动态加载和隔离运行。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/886479.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Python 第三方库 PyQt5 的安装

目录 前言 PyQt5安装 不同操作系统PyQt5安装 一、Windows 系统 二、macOS 系统 三、Linux 系统(以 Ubuntu 为例) 安装 PyQt5 可能会遇到的问题 一、环境相关问题 二、依赖问题 三、网络问题 四、安装工具问题 五、运行时问题 六、环境配置问…

手机直连卫星NTN通信初步研究

目录 1、手机直连卫星之序幕 2、卫星NTN及其网络架构 2.1 NTN 2.2 NTN网络架构 3、NTN的3GPP标准化进程 3.1 NTN需要适应的特性 3.2 NTN频段 3.3 NTN的3GPP标准化进程概况 3.4 NTN的3GPP标准化进程的详情 3.4.1 NR-NTN 3.4.1.1 NTN 的无线相关 SI/WI 3.4.1.2…

【SpringBoot】什么是Maven,以及如何配置国内源实现自动获取jar包

前言 🌟🌟本期讲解关于Maven的了解和如何进行国内源的配置~~~ 🌈感兴趣的小伙伴看一看小编主页:GGBondlctrl-CSDN博客 🔥 你的点赞就是小编不断更新的最大动力 &#x1f3…

阿里斑马智行 2025届秋招 NLP算法工程师

文章目录 个人情况一面/技术面 1h二面/技术面 1h三面/HR面 20min 个人情况 先说一下个人情况: 学校情况:211本中9硕,本硕学校都一般,本硕都是计算机科班,但研究方向并不是NLP,而是图表示学习论文情况&…

力扣第 55 题 跳跃游戏

力扣第 55 题 跳跃游戏(Jump Game)。题目要求判断一个非负整数数组中,是否能够从第一个位置跳跃到最后一个位置。每个元素表示从当前位置最多可以跳跃的步数。 解题思路 我们可以用 贪心算法 来解决这个问题。贪心的核心思想是始终维护当前…

富士施乐DocuContre S2520报打开盖子A,取出纸张。代码077-900故障检修

故障描述: 一台富士施乐DocuContre S2520复印机开机报错:打开盖子A,取出纸张。代码077-900故障,用户之前经常卡纸,卡着、卡着就一直提示打开盖子A,取出纸张了;复印机屏幕提示如下图: 故障检修: 富士施乐DocuContre S2520复印机报打开盖子A,取出纸张。077-900的错误代…

【jvm】方法区常用参数有哪些

目录 1. -XX:PermSize2. -XX:MaxPermSize3. -XX:MetaspaceSize(Java 8及以后)4. -XX:MaxMetaspaceSize(Java 8及以后)5. -Xnoclassgc6. -XX:TraceClassLoading7.-XX:TraceClassUnLoading 1. -XX:PermSize 1.设置JVM初始分配的永久…

哈佛商业评论 | 项目经济的到来:组织变革与管理革新的关键

在21世纪,项目经济(Project Economy)逐步取代传统运营,成为全球经济增长的核心动力。项目已不再是辅助工具,而是推动创新和变革的重要载体。然而,只有35%的项目能够成功,显示出项目管理领域存在巨大的改进空间。本文将详细探讨项目经济的背景、项目管理的挑战,以及适应…

ES6的Iterator 和 for...of 循环

写在前面 在JavaScript中,Iterator(遍历器)是一种接口,用于遍历数据结构(如数组、对象等)中的元素。它提供了一种统一的方式来访问集合中的每个项,包括值和位置。 默认 Iterator 接口 许多内…

大数据CDP集群中ImpalaHive常见使用语法

1. SQL中设置常量 set var:pi_sysdate 20241114; Variable PI_SYSDATE set to 202411142. CDP中impala 创建内外表 #hive3.0 默认不创建事务表的配置参数 set default_transactional_typenone; create external table stg.hd_aml_mac_ip_ext (machinedate string,vc_fundacc…

【Ubuntu24.04】VirtualBox安装ubuntu-live-server24.04

目录 0 背景1 下载镜像2 安装虚拟机3 安装UbuntuServer24.044 配置基本环境5 总结0 背景 有了远程连接工具之后,似乎作为服务器的Ubuntu24.04桌面版有点备受冷落了,桌面版的Ubuntu24.04的优势是图形化桌面,是作为一个日常工作的系统来用的,就像Windows,如果要作为服务器来…

01.防火墙概述

防火墙概述 防火墙概述1. 防火墙的分类2. Linux 防火墙的基本认识3. netfilter 中五个勾子函数和报文流向 防火墙概述 防火墙( FireWall ):隔离功能,工作在网络或主机边缘,对进出网络或主机的数据包基于一定的 规则检…

STM32设计井下瓦斯检测联网WIFI加Zigbee多路节点协调器传输

目录 目录 前言 一、本设计主要实现哪些很“开门”功能? 二、电路设计原理图 1.电路图采用Altium Designer进行设计: 2.实物展示图片 三、程序源代码设计 四、获取资料内容 前言 本系统基于STM32微控制器和Zigbee无线通信技术,设计了…

「Mac玩转仓颉内测版17」PTA刷题篇8 - L1-008 求整数段和

本篇将继续讲解PTA平台上的题目 L1-008 求整数段和,通过对整数区间的求和,进一步提升Cangjie编程语言的循环操作与数学计算能力。 关键词 PTA刷题数字区间循环求和数学运算Cangjie语言 一、L1-008 求整数段和 题目描述:给定两个整数 A 和 B…

Vue 中的透传,插槽,依赖注入

1. 透传attributes 在组件上使用透传attribute&#xff1a; 当你在父组件中使用子组件时&#xff0c;你可以添加一些attribute到子组件上&#xff0c;即使这些attribute没有在子组件的props中声明。 父组件&#xff1a; <!-- 父组件&#xff0c;例如 ParentComponent.vue…

Figma汉化:提升设计效率,降低沟通成本

在UI设计领域&#xff0c;Figma因其强大的功能而广受欢迎&#xff0c;但全英文界面对于国内设计师来说是一个不小的挑战。幸运的是&#xff0c;通过Figma汉化插件&#xff0c;我们可以克服语言障碍。以下是两种获取和安装Figma汉化插件的方法&#xff0c;旨在帮助国内的UI设计师…

SpringBoot项目实现登录——集成JWT令牌和验证码的登录业务

目录 前言 一、初步认识JWT令牌 二、利用JWT令牌实现登录功能 1.配置登录拦截器&#xff1a; 2.实现后端的登录接口 三、在登录中添加验证码功能 点此查看&#xff1a;完整的&#xff0c;附带验证码和JWT令牌验证功能的登录流程&#xff0c;完整代码 前言 在我们的项目…

网络常用特殊地址-127.0.0.1

借用Medium博客的一张图 经常在问题解答群里留意到如下关于127.0.0.1的消息 ”如果单机版&#xff0c;不需要配置IP&#xff0c;所有配置IP的地方都写死127.0.0.1就可以” “ip: 根据实际情况填写&#xff08;在 xxx-init.conf 里可以给一个默认值 127.0.0.1 &#xff0c;方便…

【模拟仿真】基于区间观测器的故障诊断与容错控制

摘要 本文提出了一种基于区间观测器的故障诊断与容错控制方法。该方法通过构建区间观测器&#xff0c;实现对系统状态的上下边界估计&#xff0c;从而在存在不确定性和外部噪声的情况下进行高效的故障诊断。进一步地&#xff0c;本文设计了一种容错控制策略&#xff0c;以保证…

全面解锁:重启服务的高效方法汇总

在运维和系统管理中&#xff0c;重启服务是一个常见的操作。本文将介绍几种重启服务的方法&#xff0c;包括通过系统命令、脚本或程序、外部监控进程以及服务自身的机制来实现。 一、通过系统命令重启服务 使用systemctl命令 适用于使用systemd作为系统初始化程序的Linux系统…