JVM常量池(class文件常量池,运行时常量池,字符串常量池)

发布于:2025-05-23 ⋅ 阅读:(14) ⋅ 点赞:(0)


超过1W字深度剖析JVM常量池(全网最详细最有深度) - 跟着Mic学架构 - 博客园

问题

jdk1.8之后

  • 元空间是独立存在的?还是位于方法区?
  • 字符串常量池在堆中还是独立存在?

在 JDK 1.8 及之后版本:

  1. 元空间与方法区关系元空间可以理解为是方法区的一种实现 。JDK 1.8 移除了永久代,引入元空间来替代。元空间使用本地内存(Native Memory ),不再像永久代那样在 JVM 堆内存中划分一块固定区域。此时方法区的功能由元空间来承担,运行时常量池等原本在方法区的部分也存在于元空间中,但元空间和传统意义上方法区概念并非完全等同,只是功能上有继承关系。 所以说元空间是方法区新的实现形式,它不是独立于方法区概念存在,而是改变了方法区的实现方式
  2. 字符串常量池位置字符串常量池在堆中。JDK 1.7 时,字符串常量池从方法区(永久代 )迁移到堆中,JDK 1.8 延续了这一做法。在堆中的字符串常量池,用于存储字符串字面量以及通过String.intern()方法添加进去的字符串 。其物理位置位于堆中,逻辑归属于方法区,从 Java 虚拟机规范角度,它是方法区相关概念的一部分,承担着存储字符串字面量等常量的功能,和方法区其他组成部分(如运行时常量池等)有紧密逻辑关联。

JVM运行时数据区

从类加载,到JVM运行时数据区的整体结构画出来,如下图所示(元空间位于本地内存)。Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。这些区域都有各自的用途。

JVM运行时数据区在物理内存上并不是单一的连续内存块,而是由多个不同物理来源的内存区域组合而成的逻辑概念

在这里插入图片描述

JVM中的常量池

JVM中的常量池可以分成以下几类:

  1. Class文件常量池/静态常量池
  2. (全局)字符串常量池
  3. 运行时常量池

Class文件常量池

程序编译后可以通过javap -c 类名.class查看该类的字节码文件。或者使用jclasslib插件。

在这里插入图片描述

  1. 位置:存在于编译后的.class文件中,是.class文件的一部分。

    • 在编译阶段,静态常量池在.class文件中,此时还未加载到JVM内存,纯属磁盘上的文件内容。
    • 当类被JVM加载时,静态常量池的内容会被解析并存入运行时常量池(Runtime Constant Pool)。
  2. 作用:存储编译期生成的各种字面量和符号引用。

    • 字面量:指的是在源代码中直接给出的数据值,在源代码中直接写出的数值,字符串,布尔等。比如文本字符串(String a = “Hello World”);声明为final的常量值(private int value = 1);基本数据类型的值。

    • 符号引用

      • 类和接口的全限定名。也就是Ljava/lang/String;,主要用于在运行时解析得到类的直接引用。

          #23 = Utf8               ([Ljava/lang/String;)V
          #25 = Utf8               [Ljava/lang/String;
          #27 = Utf8               Ljava/lang/String;
        
      • 字段的名称和描述符。字段也就是类或者接口中声明的变量,包括类级别变量(static)实例级的变量

      • 方法的名称和描述符。方法的描述类似于JNI动态注册时的“方法签名”,也就是参数类型+返回值类型,比如下面的这种字节码,表示main方法和String返回类型。

          #19 = Utf8               main
          #20 = Utf8               ([Ljava/lang/String;)V
        

运行时常量池

运行时常量池就是每一个类或接口的常量池(constant pool)在运行时的表现形式。

一个类在加载的过程,会经历加载连接(验证、准备、解析)初始化,而在类加载这个阶段,需要做以下几件事情:

  1. 通过一个类的全类限定名获取此类的二进制字节流。
  2. 在堆内存生成一个java.lang.Class对象,代表加载这个类,做为这个类的入口。
  3. class字节流的静态存储结构转化成方法区(元空间)的运行时数据结构。

而第三点就包含了class文件常量池进入运行时常量池的过程。

所以,运行时常量池的作用是存储class文件常量池中的符号信息,在类的解析阶段会把这些符号引用转换成直接引用(实例对象的内存地址),翻译出来的直接引用也是存储在运行时常量池中。class文件常量池的大部分数据会被加载到运行时常量池。

虽然方法区(或元空间)是全局共享的内存区域,但每个类都有自己独立的运行时常量池

运行时常量池是 动态的符号引用解析中心,其核心行为可归纳为:

  1. 存储:承载 Class 文件常量池的原始符号信息
  2. 转换:在解析阶段将符号引用替换为直接引用
  3. 扩展:支持运行时动态添加新常量
  4. 协同:与字符串常量池、方法区元数据紧密交互

字符串常量池

位置

虽然从物理位置上看,它在堆空间内,但它是一个逻辑上独立的区域。

存在意义

JVM之所以单独设计字符串常量池,是JVM为了提高性能以及减少内存开销的一些优化:

  1. String对象作为Java语言中重要的数据类型,是内存中占据空间最大的一个对象。高效地使用字符串,可以提升系统的整体性能。
  2. 创建字符串常量时,首先检查字符串常量池是否存在该字符串,如果有,则直接返回该引用实例,不存在,则实例化该字符串放入常量池中。

举例

String a="Hello";

String b=new String("Mic");
  1. a这个变量,是在编译期间就已经确定的,会进入到字符串常量池。
  2. b这个变量,是通过new关键字实例化,new是创建一个对象实例并初始化该实例,因此这个字符串对象是在运行时才能确定的,创建的实例在堆空间上。

在这里插入图片描述

创建了几个对象

深入理解Java字符串常量池 | 二哥的Java进阶之路

String s = new String("二哥");

使用 new 关键字创建一个字符串对象时,Java 虚拟机会先在字符串常量池中查找有没有‘二哥’这个字符串对象,如果有,就不会在字符串常量池中创建‘二哥’这个对象了,直接在堆中创建一个‘二哥’的字符串对象,然后将堆中这个‘二哥’的对象地址返回赋值给变量 s。

如果没有,先在字符串常量池中创建一个‘二哥’的字符串对象,然后再在堆中创建一个‘二哥’的字符串对象,然后将堆中这个‘二哥’的字符串对象地址返回赋值给变量 s。

对于这行代码 String s = new String("二哥");,它创建了两个对象:一个是字符串对象 “二哥”,它被添加到了字符串常量池中,另一个是通过 new String() 构造方法创建的字符串对象 “二哥”,它被分配在堆内存中,同时引用变量 s 存储在栈上,它指向堆内存中的字符串对象 “二哥”。

在这里插入图片描述

String的定义

public final class String

    implements java.io.Serializable, Comparable<String>, CharSequence {

    /** The value is used for character storage. */

    private final char value[];



    /** Cache the hash code for the string */

    private int hash; // Default to 0

}

从上述源码中可以发现。

  1. String这个类是被final修饰的,代表该类无法被继承。
  2. String这个类的成员属性value[]也是被final修饰,代表该成员属性不可被修改。

因此String具有不可变的特性,也就是说String一旦被创建,就无法更改。这么设计的好处有几个。

  1. 方便实现字符串常量池: 在Java中,由于会大量的使用String常量,如果每一次声明一个String都创建一个String对象,那将会造成极大的空间资源的浪费。Java提出了String pool的概念,在堆中开辟一块存储空间String pool,当初始化一个String变量时,如果该字符串已经存在了,就不会去创建一个新的字符串变量,而是会返回已经存在了的字符串的引用。如果字符串是可变的,某一个字符串变量改变了其值,那么其指向的变量的值也会改变,String pool将不能够实现!
  2. 线程安全性,在并发场景下,多个线程同时读一个资源,是安全的,不会引发竞争,但对资源进行写操作时是不安全的,不可变对象不能被写,所以保证了多线程的安全。
  3. 保证 hash 属性值不会频繁变更。确保了唯一性,使得类似HashMap容器才能实现相应的key-value缓存功能,于是在创建对象时其hashcode就可以放心的缓存了,不需要重新计算。这也就是Map喜欢将String作为Key的原因,处理速度要快过其它的键对象。所以HashMap中的键往往都使用String。

intern()

  1. JDK文档中关于intern()方法的说明:当调用intern方法时,如果常量池(内置在 JVM 中的)中已经包含相同的字符串,则返回池中的字符串。否则,将此String对象添加到池中,并返回对该String对象的引用。‘
  2. 注意,所有字符串字面量在初始化时,会默认调用intern()方法。
  3. intern() 方法的核心逻辑
    • 检查常量池:JVM 会通过内容匹配(而非引用相等)查找常量池中是否已有该字符串。
    • 处理结果
      • 若存在:返回常量池中的引用(可能指向堆中的对象)。
      • 若不存在:JDK 7 及以后直接在常量池中记录堆中对象的引用,而非复制内容(节省内存),并返回该引用。

问题

public static void main(String[] args) {

   String a = new String(new char[]{'a', 'b', 'c'});  // 堆中创建新对象,常量池未变化

   String b = a.intern();                              // 将a的内容放入常量池(如果不存在),并返回池中的引用

   System.out.println(a == b);                         // true:此时常量池中的引用就是a本身
}

1. 为什么 new String(char[]) 不会自动调用 intern()

当使用 new String(char[]) 构造函数时,JVM 会创建一个全新的String对象,但不会自动将其放入常量池。这是因为:

  • 常量池的设计初衷:常量池主要用于存储编译期已知的字符串字面量(如 "abc")或显式调用 intern() 的字符串。
  • 性能考量:如果每次通过字符数组创建字符串都自动入池,会导致常量池膨胀,影响性能和内存占用。
  • 语义一致性new 关键字的语义是 “强制创建新对象”,即使常量池中已有相同内容的字符串,new String(char[]) 也会创建独立的对象。

3. 总结:何时字符串会进入常量池?

  • 编译期已知的字面量(如 "abc")会在类加载时自动进入常量池。
  • 运行时动态创建的字符串(如通过字符数组、substring() 等方法)不会自动入池,除非显式调用 intern()

网站公告

今日签到

点亮在社区的每一天
去签到