为什么 JVM 不是 "seeing" 字符串池内存中的重复字符串值?

Why JVM is not "seeing" duplicate String value in String Pool memory?

可能这是重复的,但找不到我需要的解释。谁能给我解释一下。

如果我是正确的:

String s1 = "a";
String s2 = "a";

s1 和 s2 都将指向字符串池中的同一个地址,并且只有一个值为“a”的对象。

现在,如果我这样做:

String s1 = "a"; //line A
s1 = s1.concat("b"); //line B; I don't understand what's happening here in terms of references
String s2 = "ab";//line C
System.out.println(s1 == s2); //false

为什么我得到 false

我的看法(可能是错误的)是这样的:

after line A -> 在字符串池中创建对象(值为 a),由 s1 引用; after line B -> 在字符串池中创建值为 b 的对象(无引用),然后在字符串池中创建值为 ab 的新对象(由现有 s1 引用) after line C -> (这可能是我错了)由于现有对象的值为 ab(由 s1 引用)是使用 concat()(或 +)创建的,JVM 将不重用该对象以通过引用 s2 指向,它只会在字符串池中创建新对象,其值 ab 通过引用指向 s2;

我哪里错了?

我想 String concat() 方法实例化了一个 new String("ab") 对象,它与 "ab" 对象不同。

s1.concat("b") returns 一个 new String 因此 s1 == s2 将是 false.

是这样的:

String a = "Hello";
String b = new String("Hello");

System.out.println(a == b); // false

Java11中String#concat源码仅供参考:

public String concat(String str) {
    int olen = str.length();
    if (olen == 0) {
        return this;
    }
    if (coder() == str.coder()) {
        byte[] val = this.value;
        byte[] oval = str.value;
        int len = val.length + oval.length;
        byte[] buf = Arrays.copyOf(val, len);
        System.arraycopy(oval, 0, buf, val.length, oval.length);
        return new String(buf, coder);
    }
    int len = length();
    byte[] buf = StringUTF16.newBytesFor(len + olen);
    getBytes(buf, 0, UTF16);
    str.getBytes(buf, len, UTF16);
    return new String(buf, UTF16);
}

来自API Documentation

public String concat(String str)
...
If the length of the argument string is 0, then this String object is returned. Otherwise, a new String object is created, representing a character sequence that is the concatenation of the character sequence represented by this String object and the character sequence represented by the argument string.

TL;DR - 你的困惑点是字符串的 Java 内存模型,即 Heap字符串常量池 内存区域。


深入探究字符串内存模型

设计动机

在Java中,String可能是使用最频繁的对象。因此,Java 使用特殊的内存设计策略维护 String 对象,将它们保存在 Heap 中,在称为 String 的堆的隔离子集中常量池,或两者兼有。

String Constant PoolHeap内存中的一个特殊的space,存放着唯一"literal value"秒。任何时候你用它的文字值创建一个字符串,JVM首先检查字符串池中是否有相同值的对象,如果是,对同一个对象的引用是returned,如果不是- 新对象分配在 字符串常量池 中,所有其他字符串文字创建也是如此。

之所以拥有 常量池 是个好主意,是因为这句话本身的语义——因为它存储了 常量immutable String 对象,如您所见,当您可能创建许多具有相同文字内容的 String 对象时,这是一个好主意 - 在所有这些情况下,只有一个对象对于一个 "literal value" 每次都会被引用并且 不会为现有的字符串文字对象创建更新的对象

Note, that this is only possible because, String is immutable by definition. Also, note, that a pool of strings, which initially is empty, is maintained privately by the class String.

Java在哪里放置String对象?

现在事情变得有趣了。需要牢记的重要一点是,每当您使用 new String() 指令创建 String 对象时,您都会强制 Java 将新对象分配到 Heap 中;但是,如果您使用 "string literal" 创建一个字符串对象,它会分配到 字符串常量池 中。正如我们所说,String Constant Pool的存在主要是为了减少内存占用,提高内存中现有String对象的re-use。

所以,如果你写:

String s1 = "a";
String s2 = "a";
String s3 = new String("a");
  1. 字符串对象将创建到 字符串常量池 中,对该对象的引用将存储到变量 s1;
  2. String Constant Pool会是looked-up,而因为存在一个字面值相同的对象("a") 在池中找到,对同一对象的引用将被 returned;
  3. String 对象将在 Heap 区域显式创建,引用将被 returned 并存储到变量 s3.

内部字符串

如果您希望将使用 new 运算符创建的字符串对象移动到 字符串常量池 中,您可以调用 "your_string_text".intern(); 方法,并且发生两个 will 之一:

  1. 如果由 equals(Object) 方法确定池中已经包含等于此 String 对象的字符串,则池中的字符串将被 returned;
  2. 否则,此 String 对象将被添加到池中,并且此 String 对象的引用将被 returned。

您的代码中发生了什么?

String s1 = "a";
String s2 = "a";

Both s1 and s2 will point to the same address in String pool and there will be only one object with value "a".

没错。最初,将创建 String 对象并将其放入 String Constant Pool。之后,由于已经存在值为 "a" 的字符串,因此不会为 s2 创建新对象,并且存储在 s1 中的引用将类似地存储到 s2.

现在,让我们最后看看你的问题:

String s1 = "a"; //allocated in the String Constant Pool
s1 = s1.concat("b"); //contact() returns a new String object, allocated in the Heap
String s2 = "ab";//"ab" still does NOT exist in the String Constant Pool, and it gets allocated there
System.out.println(s1 == s2); //returns false, because one object is in the Heap, and another is in the String Constant Pool, and as there already exists the object in the pool, with the same value, existing object will be returned by `intern()`.

但是,如果你愿意,执行

System.out.println(s1.intern() == s2);

这将 return true,我希望你现在明白 - 为什么。因为intern()会将通过s1引用的对象从Heap移动到String Constant Pool.