【JVM系列】深入理解Java虚拟机(JVM)的核心技术 :从静态到运行时的秘密(三、Java常量池揭秘)

文章目录

  • 【JVM系列】深入理解Java虚拟机(JVM)的核心技术 :从静态到运行时的秘密(二、Java常量池揭秘)
    • 1. 基本概念
    • 2. 常量池分类
      • 2.1 运行常量池
      • 2.2 静态常量池
      • 2.3 字符串常量池
    • 3. String 类
      • 3.1 创建String对象的两种方式和区别
      • 3.2 字符串的特性
      • 3.3 StringBuffer 类
      • 3.4 String intern方法
      • 3.5 String延迟加载
      • 3.6 如何证明字符串常量池是存放在堆中
        • 3.6.1 JDK1.8字符串常量池存放在堆中
        • 3.6.2 JDK1.6字符串常量池存放在方法区
        • 3.6.3 为什么jdk7 String常量池放入到堆中

【JVM系列】深入理解Java虚拟机(JVM)的核心技术 :从静态到运行时的秘密(二、Java常量池揭秘)

JVM内存结构
image-20240914215007240

1. 基本概念

常量池(Class文件常量池):.java经过编译后生成的.class文件,是class文件的资源仓库。

常量池中主要存放:字面量(文本字符串,final常量)和符号引用(类和接口的全局定名,字段的名称和描述,方法的名称和描述)

常量池:通过一张表,虚拟机根据该常量表扎到执行的类名、方法名、参数类型、字面量。

2. 常量池分类

  1. 运行时常量池:类加载器读取class文件到内存中,该常量池就是运行时常量池;
  2. 静态常量池java 编译为 class 还没有被类加载器加载该class文件;
  3. 字符串常量池JDK7 之前是存放在方法区、JDK7 存放在堆中 、JDK 8方法区改为元空间,字符串常量池还是存放在我们堆中。

2.1 运行常量池

运行时常量池是类加载器将描述类的数据读入到内存中,并且进行相应的解析之后,形成的数据结构。它是类或接口的直接组成部分,当一个类被加载到JVM中时,它的运行时常量池也就随之建立。运行时常量池包含了编译期间生成的各种字面量(literal)和符号引用(symbolic references),并且这些常量池中的元素在类被加载到内存之后,可以被JVM动态解析为直接引用。

特点:

  1. 动态性:运行时常量池中的内容并非固定不变,随着程序运行,可能会有新的常量加入到池中。例如,字符串字面量的intern方法就是一个典型的例子,它可以将新的字符串添加到常量池中。
  2. 共享性:运行时常量池中的内容可以被类或接口的所有实例以及该类或接口中的任何方法共享。
  3. 位置:在JDK 7及以后的版本中,字符串常量池被移到了堆中,而其他常量(如类和接口的符号引用)则位于元空间(Metaspace)。

用途

  • 运行时常量池主要用于存储类的各种常量信息,如字段名、方法名、字面量等。
  • 字符串常量池(String Intern Pool)是运行时常量池的一部分,它允许开发者通过String.intern()方法获取字符串字面量的唯一引用。

示例

public class Test01 {public static void main(String[] args) {String str1 = "HelloWorld";String str2 = "HelloWorld";System.out.println(str1 == str2); // 输出 true,因为两个字符串引用相同的常量池条目}
}

在这个例子中,str1str2都引用了字符串常量池中的同一个条目,因此它们的引用是相等的。

2.2 静态常量池

静态常量池指的是.class文件中的常量池部分,它是编译器生成的信息集合,包含了类的所有常量信息。在编译阶段,编译器会生成一个包含所有常量信息的常量池,并将其嵌入到.class文件中。这个常量池的内容是固定的,不会随程序的运行而改变。

特点:

  1. 静态性:静态常量池的内容是在编译时确定的,并且在类加载之前就已经存在于.class文件中。
  2. 持久性:静态常量池的内容不会随着程序的运行而发生变化,它是类的一部分,存储在磁盘上的.class文件中。
  3. 位置:静态常量池存在于.class文件中,当类被加载到JVM时,这部分内容被解析并转化为运行时常量池。

用途

  • 静态常量池主要用于存储类的各种常量信息,如字段名、方法名、字面量等。
  • 在编译阶段,编译器会检查所有的字面量和符号引用是否存在于常量池中,如果不存在,则会产生编译错误。

示例

public class Test02 {public static final String CONSTANT_STRING = "HelloWorld";public static void main(String[] args) {System.out.println(CONSTANT_STRING);}
}

在这个例子中,CONSTANT_STRING是一个静态常量,它被存储在.class文件的常量池中。当编译器遇到这个常量时,它会确保这个字符串字面量被正确地记录在常量池中。

2.3 字符串常量池

JDK1.7之前,运行时常量池(字符串常量池也在里边)是存放在方法区,此时方法区的实现是永久带;

image-20240917120322474

JDK1.7字符串常量池被单独从方法区移到堆中,运行时常量池剩下的还在永久带(方法区);

image-20240917120530528

JDK1.8,永久带更名为元空间(方法区的新的实现),但字符串常量池池还在堆中,运行时常量池在元空间(方法区)。

image-20240917120615329

3. String 类

3.1 创建String对象的两种方式和区别

方式1:直接赋值 String s1 ="zhaoli";

从常量池査看是否有"zhaoli"数据空间,如果有直接指向;如果没有则重新创建之后指向它;s1最终指向的是常量池的空间地址。

方式2:调用构造器 String s2 = new String(“zhaoli”);

先从堆中创建空间,里面维护了value属性数组,指向常量池的"zhaoli"空间,如果常量池没有"zhaoli",重新创建,如果有直接通过value指向,最终指向的是堆中的空间地址。

image-20240917220127675 image-20240917220601530

3.2 字符串的特性

  1. String是一个final类,代表不可变的字符序列

  2. 字符串是不可变的,一个字符串对象一旦被分配,其内容是不可变的。

public class StringTest {public static void main(String[] args) {String str = "zhaoli";str = "hello world";}
}

在字符常量池中会存在2个字符串值

Constant pool:                                                               #1 = Methodref          #5.#21         // java/lang/Object."<init>":()V   #2 = String             #22            // zhaoli                          #3 = String             #23            // hello world                     //...此处省略#22 = Utf8               zhaoli#23 = Utf8               hello world#24 = Utf8               com/zhaoli/jvm/test03/StringTest#25 = Utf8               java/lang/Object
{public static void main(java.lang.String[]);descriptor: ([Ljava/lang/String;)Vflags: ACC_PUBLIC, ACC_STATICCode:stack=1, locals=2, args_size=10: ldc           #2                  // String zhaoli2: astore_13: ldc           #3                  // String hello world5: astore_16: returnLineNumberTable:line 8: 0line 9: 3line 10: 6LocalVariableTable:Start  Length  Slot  Name   Signature0       7     0  args   [Ljava/lang/String;3       4     1   str   Ljava/lang/String;
}

注意看#2#3#22#23,其实在堆空间是存在了两个字符串值的。

3.3 StringBuffer 类

java.lang.StringBuilder代表可变的字符序列,可以对字符串内容进行增删,很多方法与String相同,StringBuilder是一个容器。StringBuilder字串追加效率比String要高

在Java中,当你使用 + 操作符来连接字符串时,实际上会触发一个字符串连接操作。

String a = "zhaoli";
String b = "22";
String str = a + b;

这段代码首先定义了两个字符串变量 ab,然后将它们连接起来并赋值给 str。底层的实现过程如下:

  1. 创建StringBuilder或StringBuffer对象
    Java虚拟机(JVM)内部会自动创建一个 StringBuilder 对象(如果是多线程环境可能会使用 StringBuffer),这是因为字符串连接操作会通过 StringBuilderStringBuffer 类来高效地处理字符串构建过程。

  2. 追加操作
    初始情况下,这个 StringBuilder 对象是空的。然后,它会调用 append 方法依次追加 ab 的内容。当 b 是一个 String 类型时,可以直接追加;如果 b 是其他类型(如上面例子中的 Integer 字符串表示"22"),则需要先转换为字符串形式。

  3. 创建最终的String对象
    在所有需要追加的部分都被添加到 StringBuilder 对象之后,会调用 toString() 方法来创建一个新的 String 对象。这个新创建的字符串就是 ab 的连接结果,并且会被赋予给 str 变量。

下面是一个更详细的伪代码描述:

StringBuilder sb = new StringBuilder();// 创建StringBuilder对象
sb.append(a);// 追加a的内容
sb.append(b);// 追加b的内容(这里假设b是String类型)
str = sb.toString();// 创建String对象,并赋值给str

请注意,如果你知道字符串连接操作会被频繁执行,并且连接操作涉及多个字符串或非字符串数据类型,那么最好显式地使用 StringBuilderStringBuffer 类,以避免不必要的性能开销。例如:

StringBuilder sb = new StringBuilder();
sb.append(a);
sb.append(b);
String str = sb.toString();

这种方式可以更有效地管理内存资源,并提高程序的执行效率。

汇编代码如下

descriptor: ()V
flags: ACC_PUBLIC
Code:stack=1, locals=1, args_size=10: aload_01: invokespecial #1                  // Method java/lang/Object."<init>":()V4: returnLineNumberTable:line 6: 0LocalVariableTable:Start  Length  Slot  Name   Signature0       5     0  this   Lcom/zhaoli/jvm/test03/Test01;13: aload_114: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;17: aload_218: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;21: invokevirtual #7                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;24: astore_325: returnLineNumberTable:line 8: 0line 9: 3line 10: 6line 11: 25LocalVariableTable:Start  Length  Slot  Name   Signature0      26     0  args   [Ljava/lang/String;3      23     1     a   Ljava/lang/String;6      20     2     b   Ljava/lang/String;25       1     3   str   Ljava/lang/String;

3.4 String intern方法

在Java中,String 类的 intern 方法是一个用来优化字符串常量池使用的工具方法。它的主要作用是检查字符串是否已经存在于字符串常量池中。如果该字符串已经在池中,则返回池中已存在的字符串引用;否则,将该字符串添加到池中,并返回此字符串的引用。

使用场景:

  • 当你需要确保字符串在内存中只有一个副本时,可以使用 intern 方法。
  • 常用于确保某些字符串字面量在整个应用程序中保持唯一性,比如用于键的字符串。

实现原理:

当调用 String 对象的 intern 方法时,它会执行以下步骤:

  1. 检查字符串常量池中是否存在一个与当前字符串对象内容相等的对象。
  2. 如果存在这样的对象,就返回池中已有字符串的引用。
  3. 如果不存在,就将当前字符串对象添加到常量池中,并返回当前字符串对象的引用。

示例代码:

String str1 = new String("hello");
String str2 = new String("hello");
System.out.println(str1 == str2); // 输出 false,因为str1和str2指向不同的对象
str1 = str1.intern();
str2 = str2.intern();
System.out.println(str1 == str2); // 输出 true,因为现在str1和str2都指向常量池中的同一个对象

注意事项:

  • intern 方法的性能取决于JVM的实现以及字符串常量池的大小和配置。在某些情况下,频繁调用 intern 方法可能会导致内存消耗增加。
  • 使用 intern 方法可以提高字符串比较的效率,因为在比较引用相等(==)时,可以直接比较对象引用而不需要进行逐字符的比较。

弱引用特性:

从Java 7开始,字符串常量池中的字符串是通过弱引用 (WeakReference) 来维护的。这意味着如果系统内存压力较大,垃圾回收器可能会回收这些字符串对象。因此,在内存紧张的情况下,调用 intern 方法的行为可能不如预期那样稳定。

总之,String 类的 intern 方法是一种优化手段,可以用来减少内存中重复字符串的存储次数,但是使用时需要考虑其潜在的性能影响和内存管理行为。

3.5 String延迟加载

字符串是懒加载的,当真正需要加载的字符串才会加载到内存中,如果已经在常量池中存在则直接复用该常量池内存地址。

将项目Debug启动后勾选Memory,每执行一行代码点击Load classes,观察java.lang.String对应的Count值的变化。

image-20240918230726639

image-20240918230827832

image-20240918230906897

3.6 如何证明字符串常量池是存放在堆中

JDK7开始字符串常量池存放在堆中。

3.6.1 JDK1.8字符串常量池存放在堆中
public class StringDemo01 {// JDK1.8 设置 堆内存大小 -Xmx1m -Xms1m  -XX:-UseGCOverheadLimitpublic static void main(String[] args) {ArrayList<String> arrayList = new ArrayList<>();int count = 0;try {for (int i = 0; i < 250000; i++) {arrayList.add((i + "").intern());count++;}} catch (Exception e) {e.printStackTrace();} finally {System.out.println("循环次数:" + count);}}
}

image-20240918231207492

报错:Exception in thread "main" java.lang.OutOfMemoryError: Java heap space

3.6.2 JDK1.6字符串常量池存放在方法区
public class StringDemo01 {// jdk1.6 设置 方法区大小参数:-XX:MaxPermSize=1mpublic static void main(String[] args) {ArrayList arrayList = new ArrayList();int count = 0;try {for (int i = 0; i < 2500000; i++) {arrayList.add((i + "").intern());count++;}} catch (Exception e) {e.printStackTrace();} finally {System.out.println("循环次数:" + count);}}
}

image-20240918231309912

报错:Exception in thread "main" java.lang.OutOfMemoryError: PermGen space

3.6.3 为什么jdk7 String常量池放入到堆中

在Java 7之前,字符串常量池是位于永久代(PermGen space)中的。然而,从Java 7开始,字符串常量池被移到了堆(Heap)中。这一改变有几个主要原因:

  1. PermGen 空间限制

    • 在Java 6及更早版本中,永久代空间是有限的,并且不容易调整大小。如果大量的类加载或字符串实习化(interning)发生,可能导致 PermGen 空间的溢出。
    • 移动字符串常量池到堆中,使得这部分内存的管理更加灵活,可以根据应用程序的需求动态调整大小。
  2. HotSpot VM 的改进

    • 从Java 7开始,Oracle对HotSpot VM进行了许多改进,包括引入了新的元空间(Metaspace)机制来替代永久代。虽然元空间主要用于存储类元数据,但它也体现了Oracle对于内存管理的新思路。
    • 字符串常量池移至堆中也是这一系列改进的一部分。
  3. 更好的内存管理

    • 将字符串常量池放置在堆中后,可以更好地利用垃圾收集机制来管理和回收不再使用的字符串对象。
    • 堆内存通常比永久代更大,这使得更多的字符串可以被实习化,从而减少内存中的字符串冗余。
  4. 一致性

    • 由于字符串常量池现在位于堆中,这使得其内存管理与其他对象更加一致。这样简化了开发者的理解负担,并且减少了不同内存区域之间复杂的交互。
  5. 性能优化

    • 字符串常量池的重新定位也有助于性能上的优化。由于字符串对象本身就在堆中,将常量池也放在同一区域可以减少不同内存区域之间的数据移动,从而提升性能。

综上所述,将字符串常量池移到堆中是为了提高系统的健壮性和灵活性,同时减少内存管理方面的复杂度,并提高性能。这一改变反映了Java平台随着硬件和软件的发展不断演进的过程。

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

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

【Unity】背景图片随着背景里面内容大小而变化

今天制作项目里面的设置界面和暂停界面时&#xff0c;发现两个界面有很多重复部分&#xff0c;所以直接做一个界面就行了&#xff0c;但是两个界面的背景大小会有变化&#xff0c;图片在下面 这个是游戏暂停界面的&#xff0c;设置界面和这个界面有很多重复地方&#xff0c;仅仅…

Spring事务的1道面试题

每次聊起Spring事务&#xff0c;好像很熟悉&#xff0c;又好像很陌生。本篇通过一道面试题和一些实践&#xff0c;来拆解几个Spring事务的常见坑点。 原理 Spring事务的原理是&#xff1a;通过AOP切面的方式实现的&#xff0c;也就是通过代理模式去实现事务增强。 具体过程是…

【网页设计】CSS Part2

目标 能使用 emmet 语法能够使用 CSS 复合选择器能够写出伪类选择器的使用规范能够说出元素有几种显示模式能够写出元素显示模式的相互转换代码能够写出背景图片的设置方式能够计算 CSS 的权重 1. Emmet 语法 Emmet语法的前身是Zen coding,它使用缩写,来提高html/css的编写速…

java继承-cnblog

类的继承 继承本身 一个类要继承另一个类需要使用关键字extends 关键字extends的使用 class a extend b{}由于java只支持单继承&#xff0c;所以同时继承多个父类是错误的 在创造子类对象时&#xff0c;会先执行父类的构造方法&#xff0c;再执行子类的构造方法 举例说明 …

用 logfire 提高应用的可观测性

Logfire是由 Pydantic 团队打造的平台, 还有供 app 使用的 library, 我们经常提到对应用要做 LMT(Log, Metrics, Trace), Logfire 可以用来收集、存储、分析和可视化日志数据和应用性能指标。通过集成日志和度量&#xff0c;Logfire 提供了一个统一的界面来管理应用程序和系统的…

Windows环境mysql 9安装mysqld install报错:Install/Remove of the Service Denied!

Windows环境mysql 9安装mysqld install报错&#xff1a;Install/Remove of the Service Denied! 解决方案&#xff1a; 控制台/批处理命令窗口需要以系统管理员身份运行。 mysql数据库环境配置和安装启动&#xff0c;Windows-CSDN博客文章浏览阅读920次。先下载mysql的zip压缩…

ChatTTS 本地安装和测试

Ubuntu 22服务器&#xff0c;3.9/3.10都可以&#xff0c;但是 3.11不可以 sudo apt install python3.10 apt install python3.10 python3.10-dev #ubuntu 22 安装python3.10对应的pip3.10 # 下载 get-pip.py curl -sS https://bootstrap.pypa.io/get-pip.py -o get-pip.py # 使…

干货分享:Air780E选型的注意事项

Air780E已经是个明星模组了&#xff0c;累计出货数量2000万&#xff0c;广泛应用于物联网各行业。 今天计划讲一讲选择Air780E的注意事项&#xff01;从用户的角度&#xff0c;解答大家对Air780E这款模组最关心的问题&#xff0c;更多从选型、应用等非技术维度展开。 选择Air…

Spring Boot洗衣店订单系统:业务流程优化

2相关技术 2.1 MYSQL数据库 MySQL是一个真正的多用户、多线程SQL数据库服务器。 是基于SQL的客户/服务器模式的关系数据库管理系统&#xff0c;它的有点有有功能强大、使用简单、管理方便、安全可靠性高、运行速度快、多线程、跨平台性、完全网络化、稳定性等&#xff0c;非常适…

算法-依据先序遍历和中序遍历构建二叉树

简单的二叉树遍历算法&#xff0c; 为了通过给定的先序遍历&#xff08;preorder&#xff09;和中序遍历&#xff08;inorder&#xff09;数组构造二叉树&#xff0c;我们需要理解这两种遍历方式的特点&#xff1a; 先序遍历&#xff08;Preorder&#xff09;&#xff1a;首先…

如何高效部署SD-WAN及是否需要路由器?

随着SD-WAN&#xff08;软件定义广域网&#xff09;的快速普及&#xff0c;企业在构建网络架构时迎来了更多灵活和高效的管理方式。但在决定是否仍需部署物理路由器时&#xff0c;关键在于企业的具体网络需求与架构特点。 SD-WAN的最大特点是其通过虚拟化技术来实现网络管理。通…

<<迷雾>> 第10章 用机器做一连串的加法(6)--循环移位寄存器改进的控制器 示例电路

使用循环移位寄存器来简化装载和相加过程. info::操作说明 鼠标单击开关切换开合状态 开始之前, 应当设置循环移位寄存器 RR 的初始状态, t01, t10.(如果不是该状态, 可单击一次开关 K 即可) 在 GA 传输门左边的开关置入一个数, 比如 10. 闭合 K装载, 断开 K相加, 此时 IGAIR…

使用 three.js和 shader 实现一个五星红旗 飘扬得着色器

使用 three.js和 shader 实现一个五星红旗 飘扬得着色器 源链接&#xff1a;https://threehub.cn/#/codeMirror?navigationThreeJS&classifyshader&idchinaFlag 国内站点预览&#xff1a;http://threehub.cn github地址: https://github.com/z2586300277/three-ce…

TY1801 内置GaN电源芯片(18w-65w)

TY1801 是一款针对离线式反激变换器的多模式 PWM GaN 功率开关。TY1801内置 GaN 功率管,具备超宽 的 VCC 工作范围&#xff0c;非常适用于 PD 快充等要求宽输出电压的应用场合,TY1801不需要使用额外的绕组或外围降压电路&#xff0c;节省系统 BOM 成本。TY1801 支持 Burst&…

【最新华为OD机试E卷-支持在线评测】智能成绩表(100分)多语言题解-(Python/C/JavaScript/Java/Cpp)

🍭 大家好这里是春秋招笔试突围 ,一枚热爱算法的程序员 💻 ACM金牌🏅️团队 | 大厂实习经历 | 多年算法竞赛经历 ✨ 本系列打算持续跟新华为OD-E/D卷的多语言AC题解 🧩 大部分包含 Python / C / Javascript / Java / Cpp 多语言代码 👏 感谢大家的订阅➕ 和 喜欢�…

Unity网络开发基础 —— 实践小项目

概述 接Unity网络开发基础 导入基础知识中的代码 需求分析 手动写Handler类 手动书写消息池 using GamePlayer; using System; using System.Collections; using System.Collections.Generic; using UnityEngine;/// <summary> /// 消息池中 主要是用于 注册 ID和消息类…

视频怎么去除杂音保留人声?让人声更动听!视频噪音处理攻略

在视频制作过程中&#xff0c;音质是至关重要的一环。然而&#xff0c;很多时候我们录制的视频会伴随着各种不想要的杂音&#xff0c;比如风声、交通噪音或是其他环境音&#xff0c;这些杂音严重影响了观众的观看体验。那么&#xff0c;如何在保留人声的同时&#xff0c;有效地…

Linux与科学计算

1、引言 Linux作为一种开源操作系统&#xff0c;在科学计算领域得到了广泛的应用。科学计算通常涉及处理大量的数据和复杂的数学模型&#xff0c;要求计算机系统具备强大的计算能力、灵活性和高效性。Linux凭借其高可扩展性、稳定性和开源生态系统的优势&#xff0c;成为科学计…

Scala面试题大全~基础题(15题)

1&#xff1a;Scala是什么? Scala是一种多范式的编程语言&#xff0c;它结合了面向对象编程和函数式编程的特性&#xff0c;它支持面向对象、函数式和命令式编程方法。Scala运行在Java虚拟机&#xff08;JVM&#xff09;上&#xff0c;这意味着它可以与Java代码无缝集成。它还…

情绪识别数据集(包含25w张图片) yolo格式类别:八种训练数据已划分, 识别精度:90%

情绪识别数据集(包含25w张图片) yolo格式 类别&#xff1a;Anger、Contempt、Disgust、Fear、Happy、Neutral、Sad、Surprise 八种 训练数据已划分&#xff0c;配置文件稍做路径改动即可训练。 训练集&#xff1a;171010 验证集&#xff1a;54060 测试集&#xff1a;27550 共计…