什么是 Java 字符串池? “ s”与新字符串(“ s”)有什么不同?

字符串池是什么意思? 以下声明之间的区别是什么:

String s = "hello";
String s = new String("hello");

JVM 存储这两个字符串有什么区别吗?

69926 次浏览

字符串池是 JVM 对 字符串实习概念的特殊实现:

In computer science, string interning is a method of storing only one copy 每个不同的字符串值的 必须是不变的。实际字符串 做一些字符串处理任务 更节省时间或空间 时需要更多时间的成本 字符串被创建或实现 不同的值存储在字符串中 实习生。

Basically, a string intern pool allows a runtime to save memory by preserving immutable strings in a pool so that areas of the application can reuse instances of common strings instead of creating multiple instances of it.

有趣的是,字符串实习是 轻量级设计型式轻量级设计型式的一个例子:

轻量级是一种软件设计 轻量级是一个物体 最大限度地减少内存使用 much data as possible with other 类似的对象; 它是一种使用 当一个简单的 重复表示将使用 unacceptable amount of memory.

字符串对象基本上是字符串文字的包装器。惟一的字符串对象被池化以防止不必要的对象创建,JVM 可能决定在内部池化字符串文字。如果编译器支持这一点,还可以直接对多次引用的 String 常量提供字节码支持。

如果使用文字,比如 String str = "abc";,则使用池中的对象。如果使用 String str = new String("abc");,将创建一个新对象,但是现有的字符串文字可以在 JVM 级或字节码级(在编译时)重用。

通过在 for 循环中创建大量字符串并使用 ==运算符检查对象相等性,您可以自己检查这一点。在下面的示例中,string.value对于 String是私有的,并且保存所使用的字符串文本。因为它是私有的,所以必须通过反射访问它。

public class InternTest {
public static void main(String[] args) {
String rehi = "rehi";
String rehi2 = "rehi";
String rehi2a = "not rehi";
String rehi3 = new String("rehi");
String rehi3a = new String("not rehi");
String rehi4 = new String(rehi);
String rehi5 = new String(rehi2);
String rehi6 = new String(rehi2a);


String[] arr  = new String[] { rehi, rehi2, rehi2a, rehi3, rehi3a, rehi4, rehi5, rehi6 };
String[] arr2 = new String[] { "rehi", "rehi (2)", "not rehi", "new String(\"rehi\")", "new String(\"not rehi\")", "new String(rehi)", "new String(rehi (2))", "new String(not rehi)" };


Field f;
try {
f = String.class.getDeclaredField("value");
f.setAccessible(true);
} catch (NoSuchFieldException | SecurityException e) {
throw new IllegalStateException(e);
}


for (int i = 0; i < arr.length; i++) {
for (int j = 0; j < arr.length; j++) {
System.out.println("i: " +arr2[i]+", j: " +arr2[j]);
System.out.println("i==j: " + (arr[i] == arr[j]));
System.out.println("i equals j: " + (arr[i].equals(arr[j])));
try {
System.out.println("i.value==j.value: " + (f.get(arr[i]) == f.get(arr[j])));
} catch (IllegalArgumentException | IllegalAccessException e) {
throw new IllegalStateException(e);
}
System.out.println("========");
}
}
}
}

产出:

i: rehi, j: rehi
i==j: true
i equals j: true
i.value==j.value: true
========
i: rehi, j: rehi (2)
i==j: true
i equals j: true
i.value==j.value: true
========
i: rehi, j: not rehi
i==j: false
i equals j: false
i.value==j.value: false
========
i: rehi, j: new String("rehi")
i==j: false
i equals j: true
i.value==j.value: true
========
i: rehi, j: new String("not rehi")
i==j: false
i equals j: false
i.value==j.value: false
========
i: rehi, j: new String(rehi)
i==j: false
i equals j: true
i.value==j.value: true
========
i: rehi, j: new String(rehi (2))
i==j: false
i equals j: true
i.value==j.value: true
========
i: rehi, j: new String(not rehi)
i==j: false
i equals j: false
i.value==j.value: false
========
i: rehi (2), j: rehi
i==j: true
i equals j: true
i.value==j.value: true
========
i: rehi (2), j: rehi (2)
i==j: true
i equals j: true
i.value==j.value: true
========
i: rehi (2), j: not rehi
i==j: false
i equals j: false
i.value==j.value: false
========
i: rehi (2), j: new String("rehi")
i==j: false
i equals j: true
i.value==j.value: true
========
i: rehi (2), j: new String("not rehi")
i==j: false
i equals j: false
i.value==j.value: false
========
i: rehi (2), j: new String(rehi)
i==j: false
i equals j: true
i.value==j.value: true
========
i: rehi (2), j: new String(rehi (2))
i==j: false
i equals j: true
i.value==j.value: true
========
i: rehi (2), j: new String(not rehi)
i==j: false
i equals j: false
i.value==j.value: false
========
i: not rehi, j: rehi
i==j: false
i equals j: false
i.value==j.value: false
========
i: not rehi, j: rehi (2)
i==j: false
i equals j: false
i.value==j.value: false
========
i: not rehi, j: not rehi
i==j: true
i equals j: true
i.value==j.value: true
========
i: not rehi, j: new String("rehi")
i==j: false
i equals j: false
i.value==j.value: false
========
i: not rehi, j: new String("not rehi")
i==j: false
i equals j: true
i.value==j.value: true
========
i: not rehi, j: new String(rehi)
i==j: false
i equals j: false
i.value==j.value: false
========
i: not rehi, j: new String(rehi (2))
i==j: false
i equals j: false
i.value==j.value: false
========
i: not rehi, j: new String(not rehi)
i==j: false
i equals j: true
i.value==j.value: true
========
i: new String("rehi"), j: rehi
i==j: false
i equals j: true
i.value==j.value: true
========
i: new String("rehi"), j: rehi (2)
i==j: false
i equals j: true
i.value==j.value: true
========
i: new String("rehi"), j: not rehi
i==j: false
i equals j: false
i.value==j.value: false
========
i: new String("rehi"), j: new String("rehi")
i==j: true
i equals j: true
i.value==j.value: true
========
i: new String("rehi"), j: new String("not rehi")
i==j: false
i equals j: false
i.value==j.value: false
========
i: new String("rehi"), j: new String(rehi)
i==j: false
i equals j: true
i.value==j.value: true
========
i: new String("rehi"), j: new String(rehi (2))
i==j: false
i equals j: true
i.value==j.value: true
========
i: new String("rehi"), j: new String(not rehi)
i==j: false
i equals j: false
i.value==j.value: false
========
i: new String("not rehi"), j: rehi
i==j: false
i equals j: false
i.value==j.value: false
========
i: new String("not rehi"), j: rehi (2)
i==j: false
i equals j: false
i.value==j.value: false
========
i: new String("not rehi"), j: not rehi
i==j: false
i equals j: true
i.value==j.value: true
========
i: new String("not rehi"), j: new String("rehi")
i==j: false
i equals j: false
i.value==j.value: false
========
i: new String("not rehi"), j: new String("not rehi")
i==j: true
i equals j: true
i.value==j.value: true
========
i: new String("not rehi"), j: new String(rehi)
i==j: false
i equals j: false
i.value==j.value: false
========
i: new String("not rehi"), j: new String(rehi (2))
i==j: false
i equals j: false
i.value==j.value: false
========
i: new String("not rehi"), j: new String(not rehi)
i==j: false
i equals j: true
i.value==j.value: true
========
i: new String(rehi), j: rehi
i==j: false
i equals j: true
i.value==j.value: true
========
i: new String(rehi), j: rehi (2)
i==j: false
i equals j: true
i.value==j.value: true
========
i: new String(rehi), j: not rehi
i==j: false
i equals j: false
i.value==j.value: false
========
i: new String(rehi), j: new String("rehi")
i==j: false
i equals j: true
i.value==j.value: true
========
i: new String(rehi), j: new String("not rehi")
i==j: false
i equals j: false
i.value==j.value: false
========
i: new String(rehi), j: new String(rehi)
i==j: true
i equals j: true
i.value==j.value: true
========
i: new String(rehi), j: new String(rehi (2))
i==j: false
i equals j: true
i.value==j.value: true
========
i: new String(rehi), j: new String(not rehi)
i==j: false
i equals j: false
i.value==j.value: false
========
i: new String(rehi (2)), j: rehi
i==j: false
i equals j: true
i.value==j.value: true
========
i: new String(rehi (2)), j: rehi (2)
i==j: false
i equals j: true
i.value==j.value: true
========
i: new String(rehi (2)), j: not rehi
i==j: false
i equals j: false
i.value==j.value: false
========
i: new String(rehi (2)), j: new String("rehi")
i==j: false
i equals j: true
i.value==j.value: true
========
i: new String(rehi (2)), j: new String("not rehi")
i==j: false
i equals j: false
i.value==j.value: false
========
i: new String(rehi (2)), j: new String(rehi)
i==j: false
i equals j: true
i.value==j.value: true
========
i: new String(rehi (2)), j: new String(rehi (2))
i==j: true
i equals j: true
i.value==j.value: true
========
i: new String(rehi (2)), j: new String(not rehi)
i==j: false
i equals j: false
i.value==j.value: false
========
i: new String(not rehi), j: rehi
i==j: false
i equals j: false
i.value==j.value: false
========
i: new String(not rehi), j: rehi (2)
i==j: false
i equals j: false
i.value==j.value: false
========
i: new String(not rehi), j: not rehi
i==j: false
i equals j: true
i.value==j.value: true
========
i: new String(not rehi), j: new String("rehi")
i==j: false
i equals j: false
i.value==j.value: false
========
i: new String(not rehi), j: new String("not rehi")
i==j: false
i equals j: true
i.value==j.value: true
========
i: new String(not rehi), j: new String(rehi)
i==j: false
i equals j: false
i.value==j.value: false
========
i: new String(not rehi), j: new String(rehi (2))
i==j: false
i equals j: false
i.value==j.value: false
========
i: new String(not rehi), j: new String(not rehi)
i==j: true
i equals j: true
i.value==j.value: true
========

字符串池允许重用字符串常量,这是可能的,因为 Java 中的字符串是不可变的。如果在 Java 代码中到处重复相同的字符串常量,实际上系统中只能有该字符串的一个副本,这是该机制的优点之一。

当您使用 String s = "string constant";时,您将获得字符串池中的副本。但是,当您执行 String s = new String("string constant");时,您将强制分配一个副本。

令人费解的是,没有人直接回答这个问题,但大多数答案都有很多赞成票。

简而言之,第一种方法在 String Pool 中创建一个条目,这个条目可以被重用(由于上面关于不可变性的链接,基本上就是实习,所以更有效率) ,第二种方法创建一个新的 String 对象(成本更高)。

两个对象都存在于堆中。 对两者的引用都将位于线程的堆栈中。

Http://www.journaldev.com/797/what-is-java-string-pool 清楚地说明了如何做到这一点

JLS

正如前面提到的 作者: 安德鲁,这个概念被 JLS 称为“实习”。

来自 JLS 73.10.5的相关段落:

Moreover, a string literal always refers to the same instance of class String. This is because string literals - or, more generally, strings that are the values of constant expressions (§15.28) - are "interned" so as to share unique instances, using the method String.intern.

示例3.10.5-1. 字符串文字

由编译单元(7.3)组成的程序:

package testPackage;
class Test {
public static void main(String[] args) {
String hello = "Hello", lo = "lo";
System.out.print((hello == "Hello") + " ");
System.out.print((Other.hello == hello) + " ");
System.out.print((other.Other.hello == hello) + " ");
System.out.print((hello == ("Hel"+"lo")) + " ");
System.out.print((hello == ("Hel"+lo)) + " ");
System.out.println(hello == ("Hel"+lo).intern());
}
}
class Other { static String hello = "Hello"; }

编制单位:

package other;
public class Other { public static String hello = "Hello"; }

产生的输出:

true true true true false true

JVMS

JVMS 75.1说:

字符串文字是对类 String 实例的引用,它派生自类或接口的二进制表示形式中的 CONSTANT _ String _ info 结构(4.4.3)。CONSTANT _ String _ info 结构给出了构成字符串文字的 Unicode 代码点序列。

Java 编程语言要求相同的字符串文字(即包含相同代码点序列的文字)必须引用类 String 的相同实例(JLS 3.10.5)。此外,如果对任何字符串调用 String.international 方法,则结果是对同一个类实例的引用,如果该字符串显示为文本,则返回该类实例。因此,下列表达式的值必须为 true:

("a" + "b" + "c").intern() == "abc"

为了派生字符串文字,Java 虚拟机检查由 CONSTANT _ String _ info 结构给出的代码点序列。

  • 如果之前已经对包含与 CONSTANT _ String _ info 结构所给定的 Unicode 代码点序列相同的类 String 的实例调用过 String.international 方法,那么字符串文本派生的结果就是对同一个类 String 实例的引用。

  • 否则,将创建一个新的类 String 实例,其中包含 CONSTANT _ String _ info 结构给出的 Unicode 代码点序列; 对该类实例的引用是字符串文本派生的结果。最后,调用新 String 实例的 internmethod。

字节码

研究 OpenJDK7上的字节码实现也很有启发性。

如果我们反编译:

public class StringPool {
public static void main(String[] args) {
String a = "abc";
String b = "abc";
String c = new String("abc");
System.out.println(a);
System.out.println(b);
System.out.println(a == c);
}
}

我们有一个常量池:

#2 = String             #32   // abc
[...]
#32 = Utf8               abc

main:

 0: ldc           #2          // String abc
2: astore_1
3: ldc           #2          // String abc
5: astore_2
6: new           #3          // class java/lang/String
9: dup
10: ldc           #2          // String abc
12: invokespecial #4          // Method java/lang/String."<init>":(Ljava/lang/String;)V
15: astore_3
16: getstatic     #5          // Field java/lang/System.out:Ljava/io/PrintStream;
19: aload_1
20: invokevirtual #6          // Method java/io/PrintStream.println:(Ljava/lang/String;)V
23: getstatic     #5          // Field java/lang/System.out:Ljava/io/PrintStream;
26: aload_2
27: invokevirtual #6          // Method java/io/PrintStream.println:(Ljava/lang/String;)V
30: getstatic     #5          // Field java/lang/System.out:Ljava/io/PrintStream;
33: aload_1
34: aload_3
35: if_acmpne     42
38: iconst_1
39: goto          43
42: iconst_0
43: invokevirtual #7          // Method java/io/PrintStream.println:(Z)V

注意:

  • 0 and 3: the same ldc #2 constant is loaded (the literals)
  • 12: 创建一个新的字符串实例(使用 #2作为参数)
  • 35: 将 ac作为常规对象与 if_acmpne进行比较

常量字符串的表示在字节码中非常神奇:

而上面的 JVMS 引用似乎说,只要 Utf8指向的是相同的,那么相同的实例就由 ldc加载。

我也做过类似的实地测试,而且:

  • static final String s = "abc"通过 ConstantValue 属性指向常量表
  • 非 final 字段没有该属性,但仍然可以用 ldc初始化

结论 : 字符串池有直接的字节码支持,内存表示是有效的。

额外的好处: 比较一下 Integer pool,它没有直接的字节码支持(即没有 CONSTANT_String_info模拟)。