什么是Java字符串池,"s"和new String("s")有什么不同?

127

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

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

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


3
与之密切相关:Java中的String ==.equals在Java中,使用==运算符比较两个字符串时,会检查它们是否指向同一个对象。而使用.equals方法比较字符串时,则会检查它们所包含的字符序列是否相同。因为字符串是对象,使用==运算符比较字符串时,实际上是比较它们在内存中的引用地址。而使用.equals则是比较它们的值。例如,在以下情况下:String str1 = "Hello"; String str2 = new String("Hello");str1 == str2 的结果将会是 false,但 str1.equals(str2) 的结果将会是 true。因此,通常应该使用.equals方法来比较字符串的值,而不是使用==运算符。 - Ciro Santilli OurBigBook.com
2
相关主题:[JEP 192: G1中的字符串去重](http://openjdk.java.net/jeps/192):“通过增强G1垃圾收集器自动持续去重字符串的重复实例,降低Java堆活动数据集。” - Basil Bourque
5个回答

165

字符串池是JVM对字符串驻留概念的特定实现:

在计算机科学中,字符串驻留是一种存储每个不同字符串值的唯一副本(必须是不可变的)的方法。驻留字符串使得某些字符串处理任务更加时间或空间有效,但需要在创建或驻留字符串时花费更多时间。这些不同的值存储在字符串驻留池中。

基本上,字符串驻留池允许运行时通过保留池中的不可变字符串来节省内存,以便应用程序的某些区域可以重用常见字符串的实例,而不是创建它的多个实例。

有趣的是,字符串驻留是享元设计模式的一个例子:

享元是一种软件设计模式。享元是一种通过与其他类似对象共享尽可能多的数据来最小化内存使用的对象;当简单的重复表示将使用不可接受数量的内存时,它是在大量使用对象的一种方式。


17
好的回答,但它并没有直接回答问题。根据你的描述,代码示例似乎会引用相同的内存,是吗?也许您可以在回答中添加一个简单的总结语句。 - James Oravec
不正确。代码示例在两种情况下都使用相同的内部化字符串字面量,但第二行创建了一个新对象。如果需要概念化它,可以将第一行视为:String s = GlobalStringObjectCache.get("hello"); - Charles Goodwin
8
从谷歌复制黏贴一个与问题无关的答案不应该获得太多的点赞。 - ineedahero

61

字符串常量池允许重用字符串常量,这是由于Java中的字符串是不可变的。如果在Java代码中随处重复相同的字符串常量,则实际上可以只有一个该字符串的拷贝存在于您的系统中,这是该机制的优点之一。

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


你的意思是这样的话内存中会有两个“字符串常量”的副本?我知道String s =“string constant”会将其分配到字符串池中。String s = new String(“string constant”)会将字符串分配到哪里? - liam xu
4
第二段代码片段为现有字符串字面量分配一个新的引用,而不是复制。在内存中只有一个字面量的副本。 - Software Engineer
当你使用 String s = new String("string constant"); 时,你强制分配一个副本,能否更详细地解释一下?什么是“副本”? - frank_liu

28

JLS

根据Andrew所述,该概念被JLS称为“interning”。

JLS 7 3.10.5中相关的段落:

  

此外,字符串字面值始终引用String类的相同实例。这是因为字符串字面值 - 或者更一般地说,是常量表达式(§15.28)的值的字符串 - 被“内部化”,以便共享唯一实例,使用方法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 7 5.1指出:

字符串字面值是对 String 类的一个实例的引用,并且是从类或接口的二进制表示中的 CONSTANT_String_info 结构(§4.4.3)派生的。CONSTANT_String_info 结构给出了组成字符串字面值的 Unicode 代码点序列。

Java 编程语言要求相同的字符串字面值(即包含相同代码点序列的字面值)必须引用 String 类的相同实例 (JLS §3.10.5)。此外,如果在任何字符串上调用 String.intern 方法,则结果是对与该字符串作为字面值出现时返回的相同类实例的引用。因此,以下表达式必须具有 true 的值:

("a" + "b" + "c").intern() == "abc"
为了得到一个字符串字面量,Java虚拟机会检查由CONSTANT_String_info结构给出的码点序列。
如果方法String.intern之前已经被调用并且包含与CONSTANT_String_info结构给出的相同的Unicode码点序列的String类实例,则字符串字面量派生的结果是对该String类实例的引用。
否则,将创建一个新的String类实例,其中包含由CONSTANT_String_info结构给出的Unicode码点序列;该类实例的引用是字符串字面量派生的结果。最后,调用新String实例的intern方法。
字节码
查看OpenJDK 7中的字节码实现也很有启发性。
如果我们反编译:
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
注意以下几点:
  • 03:都加载了相同的 ldc #2 常量(即字面量)
  • 12:创建了一个新的字符串实例(使用 #2 作为参数)
  • 35:将 ac 视为常规对象进行比较,使用 if_acmpne

常量字符串在字节码中的表示方式有些神奇:

上述JVMS引用似乎表明,每当指向的Utf8相同时,就会由ldc加载相同的实例。

我已经对字段进行了类似的测试:

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

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

奖励部分:与整数池相比较,其没有直接的字节码支持(即没有CONSTANT_String_info类似物)。


有两个不同的对象,一个在字符串池中包含“abc”,有两个引用即“a”和“b”。另一个在堆中包含“abc”,有一个引用即“c”。 - Ajay Takur

17

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

当您使用文字表达式时,例如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
========

String s1 = new String("abc"), String s2 = new String("abc"). s1 != s2,这是因为这两个对象是不同的。但在内存中是否有一个或两个'abc'的副本?当通过构造函数创建时,JVM会将'abc'分配到哪里? - liam xu
在大多数情况下(即字符串的大小和底层的char数组大小相等),新的String对象将具有与传递的String对象相同的底层char数组。因此,内存中只有一个'abc'的副本(表示为char数组),但使用它的两个字符串。 - Chris Dennett
1
这个答案是错误的,所以应该取消点赞。如果池中没有与相同值的字符串字面量,则构造new String("word")将仅在池中创建一个新字符串。但它将创建一个新的String对象,引用池中任何现有的字面量,因此检查对象引用相等性的结果。 - Software Engineer
我澄清了答案。在此之前它是正确的,你误读了它。 - Chris Dennett

8
很奇怪没有人直接回答这个问题,但大多数答案都有很多赞。
简而言之,第一个创建了一个String Pool中的条目,可以被重复使用(由于不变性上的链接,基本上是interning),第二个创建了一个新的String对象(代价更高)。
两个对象都在堆中。对两者的引用将在线程的堆栈中。 http://www.journaldev.com/797/what-is-java-string-pool清楚地阐明了如何实现这一点。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接