字符串文字池是对字符串对象的引用的集合,还是对对象的集合

我在 javaranch 网站上读到《 SCJP 提示热线》的作者 Corey McGlone 的文章后感到非常困惑。命名为字符串,字面上和 SCJP Java6程序员指南作者: Kathy Sierra (javaranch 的联合创始人)和 Bert Bates。

我将尝试引用 Corey 先生和 Kathy Sierra 女士引用的关于 String Literal Pool 的话。

据科里 · 麦克格隆先生说:

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

  • String s = "Hello";(假设堆中没有名为“ Hello”的对象) , 将在堆上创建一个 String 对象 "Hello",并在 String Literal Pool (Constant Table)

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

现在,关于 "new"操作符创建 String 及其引用的解释在本文中有些混乱,因此我将代码和 来自文章本身的解释如下。

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

凯西 · 塞拉和伯特 · 贝茨在 SCJP 的书中写道:

  • 为了提高 Java 的内存使用效率,JVM 在编译器执行任务时留出了一个特殊的内存区域,称为“字符串常量池” 遇到一个字符串文字,它检查池,看看是否有一个相同的字符串已经存在或不存在。如果不是,那么它将创建一个 新的字符串文字对象。

  • 创建一个 String 对象和一个引用变量... 。

    没关系,但我被这句话搞糊涂了:

  • 创建两个对象和一个引用变量。

    书上说..。在普通(非池)内存中创建一个新的 String 对象,并且“ s”将引用它... 而 将在池中放置一个额外的文字“ abc”。

    书中的以上几行与 Corey McGlone 文章中的一行相冲突。

    • 如果 String Literal Pool 是 Corey McGlone 提到的对 String 对象的引用的集合,那么为什么要将文本对象“ abc”放入池中(正如书中提到的) ?

    • 这个字符串文字池驻留在哪里?

请清除这个疑问,虽然在编写代码时它不会太重要,但是从内存管理的角度来看是非常重要的,并且 这就是为什么我想清理这个基金。

25860 次浏览

我认为这里需要理解的主要问题是 String Java 对象和它的内容之间的区别—— 专用 value字段下的 char[]String基本上是围绕 char[]阵列的一个包装器,封装了它,使它不可能被修改,这样 String就可以保持不变。此外,String类还记得实际使用了这个数组的哪些部分(见下文)。这意味着您可以有两个不同的 String对象(相当轻量级)指向同一个 char[]

我将向您展示几个例子,连同每个 StringhashCode()和内部 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引用都使用指向 "abc"字符串的相同 #2常量分配:

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对象创建新的 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()是如何工作的,这种方法的优点是什么,以及什么时候可以 会带来大麻烦