字符串字面量池是一个对字符串对象的引用集合,还是一个对象集合?

74

我在阅读Corey McGlone在javaranch网站上的文章后感到很困惑,他是SCJP Tip Line的作者。这篇文章名为Strings, Literally,还有Kathy Sierra(javaranch的联合创始人)和Bert Bates所著的SCJP Java 6程序员指南。

我将尝试引用Corey先生和Kathy Sierra女士关于字符串文字池的引述。

1. 根据Corey McGlone先生的说法:

  • 字符串文字池是一个指向字符串对象的引用集合。

  • String s = "Hello"; (假设堆上没有名为“Hello”的对象),将在堆上创建一个字符串对象"Hello",并将对该对象的引用放置在字符串文字池(常量表)中。

  • String a = new String("Bye"); (假设堆上没有名为“Bye”的对象,new操作符将强制JVM在堆上创建一个对象。

现在,这篇文章中有关使用"new"运算符创建字符串及其引用的解释有些令人困惑,因此我将原文中的代码和解释原封不动地放在下面。
public class ImmutableStrings
{
    public static void main(String[] args)
    {
        String one = "someString";
        String two = new String("someString");

        System.out.println(one.equals(two));
        System.out.println(one == two);
    }
}

在这种情况下,由于关键字"new",我们实际上得到了略有不同的行为。在这种情况下,对两个字符串字面量的引用仍然被放入常量表(字符串字面量池)中,但是,当你遇到关键字 "new" 时,JVM有义务在运行时创建一个新的String对象,而不是使用来自常量表的那个。

这里是解释它的图示。

enter image description here

那么,这是否意味着字符串文字池也引用了这个对象?

这是 Corey McGlone 的文章链接。

http://www.javaranch.com/journal/200409/Journal200409.jsp#a1

2. 根据Kathy Sierra和Bert Bates在SCJP书中的说法:

为了使Java更加内存高效,JVM设置了一个特殊的内存区域,称为“字符串常量池”。当编译器遇到一个字符串字面量时,它会检查池中是否已经存在相同的字符串。如果没有,则创建一个新的字符串字面量对象。 String s = "abc"; // 创建一个字符串对象和一个引用变量...
但是,我对以下陈述感到困惑: String s = new String("abc") // 创建两个对象和一个引用变量。
书中说...在普通(非池)内存中创建一个新的字符串对象,并且“s”将引用它...而另一个字面量“abc”将被放置在池中。
这些书中的行与Corey McGlone的文章中的行相冲突。
如果字符串字面量池是Corey McGlone所提到的字符串对象的引用集合,那么为什么字面量对象“abc”会被放置在池中(如书中所述)?
这个字符串字面量池位于哪里?
请澄清这个疑问,虽然在编写代码时它并不太重要,但从内存管理的角度来看非常重要,这就是我想澄清这个基本原理的原因。

1
连接池的管理在某种程度上可能取决于JVM实现。只要某些东西没有被语言规范固定,开发人员可以自由地进行实验。因此,我相信连接池是保存引用还是对象可能会有所不同。 - MvG
我最近遇到了与你提到的相同的问题和资源。我想作者在书中所说的“对象'abc'将被放置在池中”的陈述,意味着对对象“abc”的引用将被存储在池中。是这样吗?被接受的答案非常详细,但我认为这就是被问到的内容。 - mustafa1993
1个回答

112
我认为要理解的主要点在于区分Java对象String和它的内容-私有value字段下的char[]之间的区别。 String基本上是围绕char[]数组的包装器,封装它并使其不可修改,以便String可以保持不变。此外,String类记住了实际使用的这个数组的哪些部分(见下文)。所有这些意味着您可以拥有两个不同的String对象(相当轻量级),指向相同的char[]
我会给你展示一些例子,每个字符串的hashCode()和内部char[] value字段的hashCode()(我将其称为文本以区分于字符串)。最后我将展示javap -c -verbose输出,以及我的测试类的常量池。请不要将类常量池与字符串字面池混淆。它们并不完全相同。另请参见理解 javap 对常量池的输出

先决条件

为了进行测试,我创建了这样一个实用方法,可以打破String的封装:

private int showInternalCharArrayHashCode(String s) {
    final Field value = String.class.getDeclaredField("value");
    value.setAccessible(true);
    return value.get(s).hashCode();
}

它将打印char[] valuehashCode(),有效地帮助我们理解这个特定的String是否指向相同的char[]文本。

类中的两个字符串字面量

让我们从最简单的例子开始。

Java代码

String one = "abc";
String two = "abc";

顺便说一句,如果你只是简单地写 "ab" + "c",Java 编译器会在编译时执行连接操作,并生成完全相同的代码。这仅在所有字符串在编译时已知的情况下才有效。

类常量池

每个类都有自己的常量池 - 一个常量值列表,如果它们在源代码中多次出现,则可以重复使用。它包括常见的字符串、数字、方法名等。

以下是我们上面示例中常量池的内容。

const #2 = String   #38;    //  abc
//...
const #38 = Asciz   abc;

重要的是要注意String常量对象(#2)和Unicode编码文本"abc"#38),即字符串指向的区别。

字节码

下面是生成的字节码。请注意,onetwo引用都被赋予相同的#2常量,指向"abc"字符串:
ldc #2; //String abc
astore_1    //one
ldc #2; //String abc
astore_2    //two

输出

我将打印每个示例的以下值:

System.out.println(showInternalCharArrayHashCode(one));
System.out.println(showInternalCharArrayHashCode(two));
System.out.println(System.identityHashCode(one));
System.out.println(System.identityHashCode(two));

没有什么奇怪的,两个配对是相等的。
23583040
23583040
8918249
8918249

这意味着不仅两个对象都指向相同的char[](下面相同的文本),因此equals()测试将通过。更重要的是,onetwo是完全相同的引用!因此,one == two也为真。显然,如果onetwo指向同一对象,则one.valuetwo.value必须相等。

字面量和new String()

Java代码

现在我们等待的示例 - 一个字符串字面量和一个使用相同字面量的新String。这将如何工作?

String one = "abc";
String two = new String("abc");

“abc”常量在源代码中被使用了两次,这一事实应该给你一些提示……
类常量池
同上。
字节码
ldc #2; //String abc
astore_1    //one

new #3; //class java/lang/String
dup
ldc #2; //String abc
invokespecial   #4; //Method java/lang/String."<init>":(Ljava/lang/String;)V
astore_2    //two

仔细看!第一个对象的创建方式与上面相同,没有什么意外。它只需要从常量池中获取对已创建的String(#2)的常量引用。然而,第二个对象是通过普通的构造函数调用创建的。但是!第一个String被作为参数传递。这可以反编译为:
String two = new String(one);

输出

输出结果有些出乎意料。第二对表示对String对象的引用是可以理解的 - 我们创建了两个String对象 - 其中一个在常量池中为我们创建,另一个则是手动为two创建的。但是,为什么第一对建议两个String对象都指向相同的char[] value数组?!

41771
41771
8388097
16585653

当你看一下String(String)构造函数的工作原理(这里大大简化)时,它变得清晰明了:
public String(String original) {
    this.offset = original.offset;
    this.count = original.count;
    this.value = original.value;
}

看到了吗?当您基于现有的String对象创建新的对象时,它会重用char[] valueString是不可变的,因此无需复制已知永远不会被修改的数据结构。
我认为这是您问题的线索:即使您有两个String对象,它们仍可能指向相同的内容。而且,正如您所看到的,String对象本身非常小。
运行时修改和intern() Java代码
假设您最初使用了两个不同的字符串,但在一些修改后它们都相同:
String one = "abc";
String two = "?abc".substring(1);  //also two = "abc"

Java编译器(至少我的)不够聪明,在编译时不能执行这样的操作,请看:
类常量池
突然间,我们有了两个指向不同常量文本的常量字符串:
const #2 = String   #44;    //  abc
const #3 = String   #45;    //  ?abc
const #44 = Asciz   abc;
const #45 = Asciz   ?abc;

字节码

ldc #2; //String abc
astore_1    //one

ldc #3; //String ?abc
iconst_1
invokevirtual   #4; //Method String.substring:(I)Ljava/lang/String;
astore_2    //two

第一个字符串按照通常的方式构建。第二个字符串是通过首先加载常量字符串"?abc",然后调用substring(1)来创建的。
输出:
毫不意外 - 我们有两个不同的字符串,指向内存中的两个不同的char[]文本。
27379847
7615385
8388097
16585653

“好的,这些文本并不是真正的不同,equals() 方法仍将返回 true。我们有两份相同文本的不必要副本。”
“现在我们应该运行两个练习。首先,尝试运行:”
two = two.intern();

在打印哈希码之前。不仅onetwo都指向相同的文本,而且它们是相同的引用!
11108810
11108810
15184449
15184449

这意味着 one.equals(two)one == two 测试都将通过。此外,我们节省了一些内存,因为 "abc" 文本只出现一次(第二个副本将被垃圾回收)。
第二个练习略有不同,请看:
String one = "abc";
String two = "abc".substring(1);

显然,onetwo是指向两个不同文本的两个不同对象。但为什么输出表明它们都指向相同的char[]数组?!?
23583040
23583040
11108810
8918249

我会把答案留给你。这将教你如何使用substring()函数,这种方法的优点以及何时可能会导致大麻烦

3
谢谢您提供了关于字符串的深入工作知识。但是,上述信息我已经有了一个大概的想法。我的问题仍然存在……字符串文字池是对象集合还是引用?如果字符串文字池持有对象,那么使用new运算符会创建两个字符串对象,一个在内存池内部,一个在外部,并且引用指向内存池外部的对象。 - Kumar Vivek Mitra
1
+1 个绝妙的答案。但我认为在最后一个示例的输出中,您获得的是 String 对象的相似 hashCode 而不是值 char 数组的。 - Eng.Fouad
6
字符串常量池是字符串对象的引用集合,而不是对象本身的集合。至于确切答案是什么,这要取决于具体的上下文和实现。 - smileVann
3
自 Java 7 起,substring 方法会创建一个所需部分数组的新副本,而不是指向同一数组。 - Heisenberg
5
这个答案没有回答最初的问题。String Pool 在内部是什么样子?它是一组引用还是一组字符串? - Eugene Maysyuk
显示剩余4条评论

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