字符串常量池

2020/1/1

点击勘误issues (opens new window),哪吒感谢大家的阅读

# 字符串常量池

字符串常量池 直接在堆中创建

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

在 Java 中,栈上存储的是基本数据类型的变量和对象的引用,而对象本身则存储在堆上。

它创建了两个对象

同时引用变量 s 存储在栈上,它指向堆内存中的字符串对象

为什么要先在字符串常量池中创建对象,然后再在堆上创建呢?

由于字符串的使用频率实在是太高了,所以 Java 虚拟机为了提高性能和减少内存开销,在创建字符串对象的时候进行了一些优化,特意为字符串开辟了一块空间——也就是字符串常量池。

# 字符串常量池的作用

通常情况下,我们会采用双引号的方式来创建字符串对象,而不是通过 new 关键字的方式

当执行 String s = "謝謝" 时,Java 虚拟机会先在字符串常量池中查找有没有“謝謝”这个字符串对象,如果有,则不创建任何对象,直接将字符串常量池中这个“謝謝”的对象地址返回,赋给变量 s;如果没有,在字符串常量池中创建“謝謝”这个对象,然后将其地址返回,赋给变量 s。

new 的方式始终会创建一个对象,不管字符串的内容是否已经存在,而双引号的方式会重复利用字符串常量池中已经存在的对象。

# 字符串常量池在内存中的什么位置呢?

在 Java 7 之前,字符串常量池位于永久代(Permanent Generation)的内存区域中,主要用来存储一些字符串常量(静态数据的一种)。永久代是 Java 堆(Java Heap)的一部分,用于存储类信息、方法信息、常量池信息等静态数据。

而 Java 堆是 JVM 中存储对象实例和数组的内存区域,也就是说,永久代是 Java 堆的一个子区域。

换句话说,永久代中存储的静态数据与堆中存储的对象实例和数组是分开的,它们有不同的生命周期和分配方式。

但是,永久代和堆的大小是相互影响的,因为它们都使用了 JVM 堆内存,因此它们的大小都受到 JVM 堆大小的限制。

于是,当我们创建一个字符串常量时,它会被储存在永久代的字符串常量池中。如果我们创建一个普通字符串对象,则它将被储存在堆中。如果字符串对象的内容是一个已经存在于字符串常量池中的字符串常量,那么这个对象会指向已经存在的字符串常量,而不是重新创建一个新的字符串对象。

需要注意的是,永久代的大小是有限的,并且很难准确地确定一个应用程序需要多少永久代空间。如果我们在应用程序中使用了大量的类、方法、常量等静态数据,就有可能导致永久代空间不足。这种情况下,JVM 就会抛出 OutOfMemoryError 错误。

因此,从 Java 7 开始,为了解决永久代空间不足的问题,将字符串常量池从永久代中移动到堆中。这个改变也是为了更好地支持动态语言的运行时特性。

到了 Java 8,永久代(PermGen)被取消,并由元空间(Metaspace)取代。元空间是一块本机内存区域,和 JVM 内存区域是分开的。不过,元空间的作用依然和之前的永久代一样,用于存储类信息、方法信息、常量池信息等静态数据。

与永久代不同,元空间具有一些优点,例如:

  • 它不会导致 OutOfMemoryError 错误,因为元空间的大小可以动态调整。
  • 元空间使用本机内存,而不是 JVM 堆内存,这可以避免堆内存的碎片化问题。
  • 元空间中的垃圾收集与堆中的垃圾收集是分离的,这可以避免应用程序在运行过程中因为进行类加载和卸载而频繁地触发 Full GC。

# 永久代、方法区、元空间

  • 方法区是 Java 虚拟机规范中的一个概念,就像是一个接口吧;
  • 永久代是 HotSpot 虚拟机中对方法区的一个实现,就像是接口的实现类;
  • Java 8 的时候,移除了永久代,取而代之的是元空间,是方法区的另外一种实现,更灵活了。

永久代是放在运行时数据区中的,所以它的大小受到 Java 虚拟机本身大小的限制,所以 Java 8 之前,会经常遇到 java.lang.OutOfMemoryError: PremGen Space 的异常,PremGen Space 就是方法区的意思;而元空间是直接放在内存中的,所以只受本机可用内存的限制。

# 详解 String.intern() 方法

第一,使用双引号声明的字符串对象会保存在字符串常量池中。

第二,使用 new 关键字创建的字符串对象会先从字符串常量池中找,如果没找到就创建一个,然后再在堆中创建字符串对象;如果找到了,就直接在堆中创建字符串对象。

第三,针对没有使用双引号声明的字符串对象来说

String s1 = new String(

如果想把 s1 的内容也放入字符串常量池的话,可以调用 intern() 方法来完成。

不过,需要注意的是,Java 7 的时候,字符串常量池从永久代中移动到了堆中,虽然此时永久代还没有完全被移除。Java 8 的时候,永久代被彻底移除。

这个变化也直接影响了 String.intern() 方法在执行时的策略,Java 7 之前,执行 String.intern() 方法的时候,不管对象在堆中是否已经创建,字符串常量池中仍然会创建一个内容完全相同的新对象; Java 7 之后呢,由于字符串常量池放在了堆中,执行 String.intern() 方法的时候,如果对象在堆中已经创建了,字符串常量池中就不需要再创建新的对象了,而是直接保存堆中对象的引用,也就节省了一部分的内存空间。

String s1 = new String("嘻嘻嘻");
String s2 = s1.intern();
System.out.println(s1 == s2);

s1 和 s2 的引用地址是不同的,一个来自堆,一个来自字符串常量池,所以输出的结果为 false。

不过需要注意的是,尽管 intern 可以确保所有具有相同内容的字符串共享相同的内存空间,但也不要烂用 intern,因为任何的缓存池都是有大小限制的,不能无缘无故就占用了相对稀缺的缓存空间,导致其他字符串没有坑位可占。

另外,字符串常量池本质上是一个固定大小的 StringTable,如果放进去的字符串过多,就会造成严重的哈希冲突,从而导致链表变长,链表变长也就意味着字符串常量池的性能会大幅下降,因为要一个一个找是需要花费时间的。

上次更新: 2024/12/9 01:46:36