3

我对 Java 和其他语言中的字符串对象等不可变对象的内存管理存在概念上的疑问。例如,如果我有一个 String 对象 "str" 持有值 "Hello",我执行以下操作:

String str = "Hello";
str = str.concatenate("World");

在这种情况下,据我了解,创建了一个状态为“Hello World”的新 String 对象并将其引用回 str。现在,在 Java(以及大多数其他面向对象的语言中)中,任何对象的生命周期只要它的引用仍然存在。那么持有“Hello”的对象去哪里了。它是否驻留在内存堆中,直到垃圾收集器自行处理它?另外,不支持垃圾收集器并且必须依赖类析构函数的语言呢?

此外,如果可变对象(例如StringBufferStringBuilder)更加灵活且性能友好,为什么在设计语言时首先要使对象不可变?(我的意思是为什么字符串对象从一开始就不是可变的,而是必须在后续的 JDK 版本中引入新的结构,例如字符串缓冲区?)。

如果有人可以指导我,那就太好了。我对此很陌生,因此我们将非常感谢您提供清晰,基本的解释。谢谢。

4

4 回答 4

4

这实际上是一个关于 java String 类的问题——不是一般的不变性。当 Java 第一次被引入时,设计者决定让 String 变得特别——在某些方面它介于引用类型和原始类型之间。

我们使用 String 获得的优势是,虚拟机保留了一个公共的字符串字面量池,以阻止堆被填满 - 请参阅此处以获取描述。这背后的原因是程序的大部分内存都可以用来存储常用的字符串。另请参阅String.intern

对于任何其他类型的不可变对象,情况并非如此(可悲)。您关于 str 去向的问题已由其他人回答 - 它遵循正常的垃圾收集规则,我相信您知道(或可以找到)。

也许您问题中最有趣的部分是

另外,如果像 StringBuffer/StringBuilder 这样的可变对象更加灵活和性能友好,为什么在设计语言时首先要使对象可变?(我的意思是为什么字符串对象从一开始就不是可变的,而不是在后续的 jdk 版本中引入新的结构,例如字符串缓冲区?)。

我的回答是,常见的情况是我们有很多相同的字符串,我们想针对常见的情况进行优化。另请注意,Java 编译器在连接字符串时使用 StringBuilder。例如采取这个代码

public class StringBuilderTest {

  public static void main(String [] args){

    String hello = "hello ";
    String world = "world";
    System.out.println(hello+world);
   }
}

并使用拆卸它

javap -c StringBuilderTest

并获取以下主要方法的字节码

  public static void main(java.lang.String[]);
    Code:
       0: ldc           #2                  // String hello 
       2: astore_1      
       3: ldc           #3                  // String world
       5: astore_2      
       6: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
       9: new           #5                  // class java/lang/StringBuilder
      12: dup           
      13: invokespecial #6                  // Method java/lang/StringBuilder."<init>":()V
      16: aload_1       
      17: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      20: aload_2       
      21: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      24: invokevirtual #8                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
      27: invokevirtual #9                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      30: return        
}

它使用 StringBuilder 来执行追加。

于 2013-06-06T08:55:59.517 回答
2

遵循 Least Surprise 原则,字符串是不可变的。

基元类型如int,floatchar是按值复制的——如果你将它的值复制到另一个地方并编辑其中一个副本,它实际上是一个全新的基元,已经被编辑过了,在另一个地方看不到任何变化。

字符串不是原语,但从概念上讲,它们在很多方面都被“视为原语”。由于我们已经习惯了原语的按值复制特征,如果字符串被制成可变但我们忘记并将它们视为具有按值复制的语义,会发生什么?

事情可能会变得一团糟。例如:

-任何时候你返回一个字符串,你都必须返回一个字符串的副本,否则字符串的使用者可以编辑它,突然你的字符串也被编辑了!例如,用户名、密码、消息等数据可能会被“令人惊讶地”编辑。

- 安全是一个问题。如果您调用未知代码并且它改变了您正在使用的字符串,那么您必须记住并复制所有字符串(还有性能问题!)或者当它们从您的脚下更改时遭受随机的有害行为。

-String interning 是不可能的(这是一种机制,可以重复使用具有相同值的字符串,因此该字符串值的对象只存在一个而不是两个)。字符串实习生表依赖于字符串是不可变的。

这是一个折衷——在某些方面的性能,与在其他方面的性能(现在需要在你想确保传递字符串的东西没有编辑它时复制字符串是性能损失!),更难以推理你的代码(字符串无处不在,如果任何字符串可能因任何原因随时更改,如果它曾经被暴露并且获得了另一个引用......)等等。

于 2013-06-06T08:56:03.857 回答
2
So where does the object holding "Hello" go

对 "Hello" 的引用 str 被分配了新值,因此对值 "Hello" 的引用丢失了,但它仍然在池中,可用,垃圾收集器可能会收集它并将其从堆中删除,具体不知道,假设在未来的代码中你仍然使用“Hello”字符串

String againhello= "Hello" ;

然后在这种情况下,垃圾收集器不应该收集它,因为“Hello”字符串被创建并再次被使用,只分配了新的引用。

对象的可变性和不变性背后的概念是,任何两个对象如果具有相同的值,就应该具有相同的哈希码,并且对于equals方法应该返回true,这对于String对象也是如此,但是为了提高性能。

他们将 String 设置为不可变的,因为他们不希望堆中填充相同的值和数量的不同对象,例如,假设

String sre="Hello";

String str="Hello"; 

如果没有 String 的不变性,那么 heap 中就会有两个对象,但是这里只有一个对象,只有两个引用变量。

what is difference between String and StringBuilder class. 

Java 5 中添加了 StringBuilder 类,并提供了与 StringBuffer 类似的功能(即.. mutable string ),其中在每次修改 string 时,现在都不会创建新对象 使用 StringBuilder 的好处是它比 StringBuffer 相对快,因为 StringBuffer 是同步的类而 StringBuilder 不是,因此如果您想在不关心线程安全的环境中使用 StringBuffer,请考虑使用 StringBuilder 以获得更好的性能。

默认情况下,所有Java 类都是可变的,即可以修改它们的实例的内容。但是不可变性提供的优势很少(http://download.oracle.com/javase/tutorial/essential/concurrency/immutable.html),这就是为什么通过将某些类标记为最终类来使其不可变的原因。有问题的类是 String 和 Wrapper 类,如果您从逻辑上考虑它们(任何不可变类),那么提供的链接中的描述将开始有意义。让我们分别解决这两个问题:

String class: 

正如 Kathy Siera 和 Bert Bates 在 SCJP 第 433 页中提到的,随着应用程序的增长,程序的字符串文字中存在大量冗余是很常见的。因此,为了解决这个问题,Java 的设计者提出了字符串池的概念,它通过有效利用可用内存来提高性能。但是现在,正如您可能想象的那样,如果多个引用变量在不知情的情况下引用同一个字符串,那么如果它们中的任何一个都可以更改字符串的值,那就太糟糕了。因此,需要使这个 String 类不可变。

Wrapper classes:

制作包装类的目标之一是提供一种机制来处理具有为对象保留的活动的原语,例如添加到集合中,或从具有对象返回值的方法返回。如果您考虑一个集合,它通常会被多个线程访问。如果包装类不是可变的,它将面临并发修改的风险,从而导致不一致的状态。因此,为了避免冲突,包装类是不可变的。

因此,一般来说,每当您遇到不可变类时,考虑其实例以并发方式使用是合乎逻辑的。此外,如果您不希望修改对象内容(原因之一是并发访问),则使该类不可变。

于 2013-06-06T08:38:29.197 回答
0

你可能在外缘领域,达斯编码器和谷歌不可用,所以这里是参考对象和垃圾收集的入门级解释。

关于这在 Oracle Java VM 中是如何工作的技术解释见此处

理解任何语言的垃圾收集的关键思想是可达性。每个对象都需要通过根引用的路径访问。什么是根引用?示例是方法调用堆栈帧链、类、线程、JNI 引用等等。从这些根中无法到达的所有内容都被视为未使用,并使用文章中描述的方法回收其空间。垃圾收集绝不是微不足道和生动的研究领域,所以请耐心等待:-)。

于 2013-06-06T08:35:41.413 回答