为什么每次使用new关键字创建字符串时,JVM都会创建一个新的字符串对象?

11

如果 jvm 为了内存优化创建了 字符串池,那么为什么每次我们使用 new 关键字创建字符串时,即使它已经存在于 字符串池 中,它也会创建一个新的对象呢?


7
因为这就是“new”的意思…… - njzk2
2
计算机为什么会按照我所说的去做?- o.O - The Paramagnetic Croissant
与问题“text和new String('text')之间的区别是什么?”密切相关。 - Raedwald
7个回答

30
为什么在使用new关键字创建字符串时,即使它已经存在于字符串池中,Java也会每次都创建一个新对象?
因为你明确地告译器告诉了它要这样做!new操作符总是会创建一个新的对象。根据JLS 15.9.4的规定:
“类实例创建表达式的值是指向指定类的新创建对象的引用。每次计算该表达式都会创建一个新的对象。
就记录而言,调用new String(String)几乎总是错误的... 但在某些特殊情况下可能有用。可以想象您可能希望获得一个字符串,其中equals返回true并且==返回false。调用new String(String)将为您提供此功能。
对于旧版Java,substringtrim和可能的其他String方法会提供共享原始存储的字符串。在某些情况下,这可能导致内存泄漏。例如,调用new String(str.trim())将防止出现内存泄漏,但代价是创建另一个已修剪的字符串的新副本。而String(String)构造函数会保证分配一个新的支持数组以及为您提供一个新的String对象。
注意:Java 7中substringtrim的行为发生了变化。

6
也适用于测试 - 能够发现那些讨人厌的使用==比较String类型时的错误。 - Boris the Spider
我想知道如果像subString这样的操作在使用不到原始字符串一半的情况下返回具有新后备存储器的字符串,但在其他情况下借用原始字符串,或者在需要复制超过原始字符串寿命的情况和不需要的情况下分别使用单独的subString方法,会对性能产生什么影响? - supercat
@supercat:我认为有几种选择。例如,您可以拥有一个廉价的substring实现,共享数组和智能垃圾收集器,知道如何压缩这些字符串,如果原始大字符串变得不可访问。今天的JVM 垃圾收集器,具有特殊的字符串和它们的字符数组知识,因为它将强制相等字符串的数组共享。尽管如此,拥有廉价的substring也意味着每个String都必须携带额外的offsetlength字段,这些字段必须被每个其他字符串操作所尊重... - Holger
@Holger:这些字段在实践中会产生多少成本?如果我正在设计Java,我个人可能会使string作为语言中的不同类型,通常会保存引用,并允许强制转换为ObjectStringObject,但将实现==作为值相等,并且不保证(StringObject)string1 == (StringObject)string1何时会为true或false [类似于例如(Short)x ==(Short)x的情况]。这将允许实现有效地为不同种类的对象使用不同的... - supercat
可以使用各种字符串类型(例如char[]byte[]或各种堆对象)并进行方便的替换。例如,ConcatenatedString 可以包含一个 String[],其第一个非空元素表示其内容,其余非空元素则标识组成部分。将 ConcatenatedString 用于需要线性字符串的任何目的都会导致系统物理上连接其片段并存储对线性字符串的引用(以防再次需要),但连接字符串然后将其连接到其他东西... - supercat
不需要重新生成整个线性字符串。在某些情况下,如果一个字符串的最后一段和另一个字符串的第一段总共不到256个字符左右,最好生成一个新的 char[]byte[] 来将它们合并而不是分别存储它们,但是没有理由一直重新生成大量的线性字符串。 - supercat

5
为了提供原始的声明风格并提高性能,设计师引入了字符串字面量。但是当您使用new关键字时,您会明确地在堆上创建对象而不是常量池中。当在堆上创建对象时,没有办法共享该内存,并且它们变得完全陌生,不像常量池中那样。为了打破堆和常量池之间的障碍,字符串驻留将帮助您解决问题。

字符串驻留是一种仅存储每个不同字符串值的一个副本的方法,该值必须是不可变的。

请记住,常量池也是堆的一小部分,具有一些额外的好处,其中可用共享内存。

3

When you write

String str = new String("mystring"); 

然后它在堆中创建一个字符串对象,就像你创建其他对象一样。字符串常量“mystring”存储在字符串常量池中。

根据Javadocs

String类私下维护着一个初始为空的字符串池。当调用intern方法时,如果池中已经包含与此String对象相等的字符串(由equals(Object)方法确定),则返回池中的字符串。否则,将此String对象添加到池中,并返回对此String对象的引用。因此,对于任何两个字符串s和t,只有当s.equals(t)为true时,s.intern() == t.intern()才为true。

1
为了利用字符串池,您需要使用String#intern而不是new。

1
可能值得一提的是,这仍然需要首先创建新的String对象,但如果相同的字符串存在于常量池中,则可以快速清理新创建的对象。 - Andy Turner
@AndyTurner - 但是如果常量池中存在相同的字符串,那么新创建的对象可以很快被清理掉。 为什么?如何实现? - TheLostMind
因为如果你说String foo = "foo"; String bar = new String("foo").intern();,那么bar == foo,所以由new String("foo")创建的实例不再被引用。 - Andy Turner
@AndyTurner - 即使它不存在,由 new String("foo") 创建的实例也不会引用它,并且将有资格进行垃圾回收。 - TheLostMind

0

以下对象将被存储在字符串池中:

String s = "hello";

以下对象将存储在堆中(而不是字符串池中):

String s  = new String ("hello")

1
不完全是这样...你正在使用传递给构造函数的字面量,而该字面量存储在常量池中。 - Gaskoin
@Gaskoin - 如果您使用new运算符创建字符串对象,它将始终创建一个新对象,并且它将存储在堆中。请提供您提供这个语句的参考。参考资料:http://java67.blogspot.in/2014/08/difference-between-string-literal-and-new-String-object-Java.html - Rahman
1
在同一包(§7)中的同一类(§8)中的文字字符串表示对同一String对象的引用。当您使用字符串文字编译源代码时,将创建一个常量String对象。第一行上的变量s指向此对象。在第二行,将构造一个新的String对象,并将常量String对象作为参数传递。https://docs.oracle.com/javase/specs/jls/se7/html/jls-3.html#jls-3.10.5 - Joe
1
@Joe:你不应该只看第一个列表项就停下来。比如,“不同包中不同类中的文字字符串也表示对同一字符串对象的引用”。换句话说,所有相等的文字字符串都会产生相同的对象,无论它们所在的包或类别。 - Holger

0

强制进行垃圾回收!如果您只需要使用某个字符串一次,那么将其保留在内存中(几乎永远)就没有意义了。这种情况适用于常量池中的字符串。不在常量池中的字符串可以像其他对象一样被垃圾回收。因此,您应该只将经常使用的字符串放入常量池中(通过使用文字或将它们放入池中)。


1
@Gaskoin - 你正在查看的是类常量池,而不是JVM字符串常量池。这两者之间有很大的区别。 - TheLostMind
运行时池中的String与任何其他未使用的字符串一样,可能会被垃圾回收。只有当它们恰好与类文件中现有的常量匹配时,该常量才不会被回收,这并不是问题,因为它已经存在了。不使用intern()在每个字符串上的主要原因是它不便宜,因为它将包含哈希和更糟糕的是,国际化字符串表的大小是固定的,这增加了冲突的可能性(在Java7u40之前,它的大小非常小)。 - Holger
@Holger - 所有进入字符串常量池的字符串常量都将永远存在于类常量(字节码)中。堆上的非内部化字符串可以像任何其他对象一样被垃圾回收。如果定义/使用它的类加载器被垃圾回收,常量池中的字符串可能会被垃圾回收。 - TheLostMind
请停止将字节码和调用intern()视为相同的处理方式。调用intern()不会突然修改类的字节码。被内部化但不匹配文字的字符串可以轻松进行垃圾回收。由new创建的字符串是我们讨论的问题,因此我们谈论文字。但是如果我们认为该字符串可能与文字匹配,则允许新字符串的垃圾回收是一个奇怪的论点,因为这意味着解决了一个本来不存在的问题,即当返回现有字符串并且没有创建新字符串时。 - Holger
@Holger - 请停止将字节码和调用intern()视为相同的东西。我从未说过它们是一样的 :) - TheLostMind
你的回答只包含一个理由,最后得出结论:“因此,你应该只将常用字符串放入常量池中(通过使用字面值或者intern()方法)。” 这意味着整个推理过程适用于使用字面值或者intern()方法。但是问题并不是关于字面值的,而是关于向池中添加非字面值的内容,比如intern()方法所做的。 - Holger

0

以字符串字面量形式创建的字符串(String s = "string";)存储在字符串池中,但通过使用 new 调用 String 构造函数创建的字符串(String s = new String("string");)不会存储在字符串池中。


1
不完全是这样... 你使用了传递给构造函数的文字,而这些文字被存储在常量池中。 - Gaskoin

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