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.rhkb.cn/news/475705.html

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

相关文章

一文读懂Redis6的--bigkeys选项源码以及redis-bigkey-online项目介绍

一文读懂Redis6的--bigkeys选项源码以及redis-bigkey-online项目介绍 本文分为两个部分,第一是详细讲解Redis6的--bigkeys选项相关源码是怎样实现的,第二部分为自己对--bigkeys源码的优化项目redis-bigkey-online的介绍。redis-bigkey-online是自己开发的…

Go语言跨平台桌面应用开发新纪元:LCL、CEF与Webview全解析

开篇寄语 在Go语言的广阔生态中,桌面应用开发一直是一个备受关注的领域。今天,我将为大家介绍三款基于Go语言的跨平台桌面应用开发框架——LCL、CEF与Webview,它们分别拥有独特的魅力和广泛的应用场景。通过这三款框架,你将能够轻…

音视频入门基础:MPEG2-TS专题(5)——FFmpeg源码中,判断某文件是否为TS文件的实现

一、引言 通过FFmpeg命令: ./ffmpeg -i XXX.ts 可以判断出某个文件是否为TS文件: 所以FFmpeg是怎样判断出某个文件是否为TS文件呢?它内部其实是通过mpegts_probe函数来判断的。从《FFmpeg源码:av_probe_input_format3函数和AVI…

C++初阶学习第十一弹——list的用法和模拟实现

目录 一、list的使用 二.list的模拟实现 三.总结 一、list的使用 list的底层是双向链表结构&#xff0c;双向链表中每个元素存储在互不相关的独立节点中&#xff0c;在节点中通过指针指向 其前一个元素和后一个元素。 常见的list的函数的使用 std::list<int> It {1,…

Qlik Sense QVD 文件

QVD 文件 QVD (QlikView Data) 文件是包含从 Qlik Sense 或 QlikView 中所导出数据的表格的文件。QVD 是本地 Qlik 格式&#xff0c;只能由 Qlik Sense 或 QlikView 写入和读取。当从 Qlik Sense 脚本中读取数据时&#xff0c;该文件格式可提升速度&#xff0c;同时又非常紧凑…

攻防世界 Web新手练习区

GFSJ0475 get_post 获取在线场景后&#xff0c;点开网址 依据提示在搜索框输入信息 给出第二条提示信息 打开hackbar&#xff0c;将网址Load下来&#xff0c;勾选Post data&#xff0c;在下方输入框输入b2 点击Execute 出现flag值 GFSJ0476 robots 打开御剑扫描域名&#…

MySQL —— explain 查看执行计划与 MySQL 优化

文章目录 explain 查看执行计划explain 的作用——查看执行计划explain 查看执行计划返回信息详解表的读取顺序&#xff08;id&#xff09;查询类型&#xff08;select_type&#xff09;数据库表名&#xff08;table&#xff09;联接类型&#xff08;type&#xff09;可用的索引…

前端研发高德地图,如何根据经纬度获取地点名称和两点之间的距离?

地理编码与逆地理编码 引入插件&#xff0c;此示例采用异步引入&#xff0c;更多引入方式 https://lbs.amap.com/api/javascript-api-v2/guide/abc/plugins AMap.plugin("AMap.Geocoder", function () {var geocoder new AMap.Geocoder({city: "010", /…

React(二)

文章目录 项目地址七、数据流7.1 子组件传递数据给父组件7.1.1 方式一:給父设置回调函数,传递给子7.1.2 方式二:直接将父的setState传递给子7.2 给props传递jsx7.2.1 方式一:直接传递组件给子类7.2.2 方式二:传递函数给子组件7.3 props类型验证7.4 props的多层传递7.5 cla…

SpringBootTest常见错误解决

1.启动类所在包错误 问题 由于启动类所在包与需要自动注入的类的包不在一个包下&#xff1a; 启动类所在包&#xff1a; com.exmaple.test_02 但是对于需要注入的类却不在com.exmaple.test_02下或者其子包下&#xff0c;就会导致启动类无法扫描到该类&#xff0c;从而无法对…

Redis面试篇笔记(持续更新)

一、redis主从集群 单节点redis的并发能力是由上限的&#xff0c;要进一步提高redis的并发能力可以搭建主从集群&#xff0c;实现读写分离&#xff0c;一主多从&#xff0c;主节点写数据&#xff0c;从节点读数据 部署redis主从节点的docker-compose文件命令解析 version: &q…

ISUP协议视频平台EasyCVR私有化视频平台新能源汽车充电停车管理方案的创新与实践

在环保意识提升和能源转型的大背景下&#xff0c;新能源汽车作为低碳出行的选择&#xff0c;正在全球迅速推广。但这种快速增长也引发了充电基础设施短缺和停车秩序混乱等挑战&#xff0c;特别是在城市中心和人口密集的居住区&#xff0c;这些问题更加明显。因此&#xff0c;开…

goland单元测试

一、单元测试的概念 1.1 什么是单元测试&#xff0c;有什么用&#xff1f; 单元测试是针对于函数的测试&#xff0c;用来保证该函数的逻辑正确性。 1.2 单元测试的要求&#xff1f; 1. 单元测试在正式上线之前应该全部自动执行&#xff0c;并且需要保证全部通过 2. 单元测试需…

连接数据库:通过链和代理查询鲜花信息

目录 新的数据库查询范式 实战案例背景信息 创建数据库表 用 Chain 查询数据库 用 Agent 查询数据库 一直以来&#xff0c;在计算机编程和数据库管理领域&#xff0c;所有的操作都需要通过严格、专业且结构化的语法来完成。这就是结构化查询语言&#xff08;SQL&#xff0…

【c++丨STL】stack和queue的使用及模拟实现

&#x1f31f;&#x1f31f;作者主页&#xff1a;ephemerals__ &#x1f31f;&#x1f31f;所属专栏&#xff1a;C、STL 目录 前言 一、什么是容器适配器 二、stack的使用及模拟实现 1. stack的使用 empty size top push和pop swap 2. stack的模拟实现 三、queue的…

aws上安装ssm-agent

aws-cloudwatch 连接机器 下载ssm-agent aws-ec2 安装ssm-agent aws-linux安装ssm-agent 使用 SSM 代理查找 AMI 预装 先运行&#xff1a;systemctl status amazon-ssm-agent 查看sshm-agent的状态。 然后安装提示&#xff0c;执行 systemctl start amazon-ssm-agent 启动即…

百度世界2024:智能体引领AI应用新纪元

在近日盛大举行的百度世界2024大会上&#xff0c;百度创始人李彦宏以一场题为“文心一言”的精彩演讲&#xff0c;再次将全球科技界的目光聚焦于人工智能&#xff08;AI&#xff09;的无限可能。作为一名科技自媒体&#xff0c;我深感这场演讲不仅是对百度AI技术实力的一次全面…

纯血鸿蒙NEXT-组件导航 (Navigation)

Navigation组件是路由导航的根视图容器&#xff0c;一般作为Page页面的根容器使用&#xff0c;其内部默认包含了标题栏、内容区和工具栏&#xff0c;其中内容区默认首页显示导航内容&#xff08;Navigation的子组件&#xff09;或非首页显示&#xff08;NavDestination的子组件…

C语言 | Leetcode C语言题解之第564题寻找最近的回文数

题目&#xff1a; 题解&#xff1a; #define MAX_STR_LEN 32 typedef unsigned long long ULL;void reverseStr(char * str) {int n strlen(str);for (int l 0, r n-1; l < r; l, r--) {char c str[l];str[l] str[r];str[r] c;} }ULL * getCandidates(const char * n…

docker学习笔记跟常用命令总结

Docker简介 Docker是一个用于构建运行传送应用程序的平台 镜像 将应用所需的函数库、依赖、配置等与应用一起打包得到的就是镜 镜像结构 镜像管理命令 命令说明docker pull拉取镜像docker push推送镜像docker images查看本地镜像docker rmi删除本地镜像docker image prune…