为什么在字符串后附加“”会节省内存?


193

我说了一个变量,里面有很多数据String data。我想通过以下方式使用此字符串的一小部分:

this.smallpart = data.substring(12,18);

经过数小时的调试(使用内存可视化器),我发现objects字段smallpart记住了的所有数据data,尽管它仅包含子字符串。

当我将代码更改为:

this.smallpart = data.substring(12,18)+""; 

..问题解决了!现在我的应用程序现在只占用很少的内存!

那怎么可能?谁能解释一下?我认为this.smallpart一直在引用数据,但是为什么呢?

更新:那 我怎么清除大字符串?data = new String(data.substring(0,100))会做这件事吗?


在下面阅读有关您最终目的的更多信息:大字符串最初来自何处?如果从文件或数据库CLOB或其他内容中读取内容,则仅在解析时读取所需内容将是最佳选择。
PSpeed 2010年

4
太神奇了...我在Java中工作了4至5年,但这对我来说仍然是新的:)。感谢您的信息兄弟。
Parth

1
使用有一个微妙的地方new String(String); 参见stackoverflow.com/a/390854/8946
劳伦斯·多尔

Answers:


159

执行以下操作:

data.substring(x, y) + ""

创建一个新的(较小的)String对象,并丢弃对substring()创建的String的引用,从而实现对此的垃圾回收。

重要的是要实现substring()现有 String 的窗口,或者说是原始String下方的字符数组的窗口。因此,它将消耗与原始String相同的内存。这在某些情况下可能是有利的,但是如果您想获得一个子字符串并处置原始String(如您所知),则会遇到问题。

查看JDK String源中的substring()方法以获取更多信息。

编辑:要回答您的补充问题,从子字符串构造一个新的String将减少您的内存消耗,只要您将对原始String的任何引用都进行装箱。

注意(2013年1月)。在Java 7u6中,以上行为已更改。flyweight模式不再使用,substring()将按您期望的那样工作。


89
这是String(String)构造函数(即以String作为输入的String构造函数)有用的极少数情况之一:new String(data.substring(x, y))有效地执行与append相同的操作"",但是使意图更加清晰。
Joachim Sauer 2010年

3
确切地说,子字符串使用value原始字符串的属性。我认为这就是为什么保留参考文献的原因。
Valentin Rocher 2010年

@Bishiboosh-是的,没错。我不想透露实现的特殊性,但这就是正在发生的事情。
Brian Agnew

5
从技术上讲,这是一个实现细节。但这仍然令人沮丧,并吸引了很多人。
Brian Agnew

1
我想知道是否有可能使用弱引用等在JDK中对此进行优化。如果我是需要此char []的最后一个人,而我只需要一点,请创建一个新数组供我内部使用。
WW。

28

如果查看的来源substring(int, int),您会看到它返回:

new String(offset + beginIndex, endIndex - beginIndex, value);

value原件在哪儿char[]?因此,您将获得一个新的String,但具有相同的基础char[]

完成后data.substring() + "",您将获得一个带有底层代码的新String char[]

实际上,用例是唯一应使用String(String)构造函数的情况:

String tiny = new String(huge.substring(12,18));

1
使用有一个微妙的地方new String(String); 参见stackoverflow.com/a/390854/8946
劳伦斯·多尔

17

使用时substring,它实际上不会创建新的字符串。它仍然引用您的原始字符串,但具有偏移量和大小限制。

因此,要收集原始字符串,您需要创建一个新字符串(使用new String或已有的东西)。


5

我认为this.smallpart一直在引用数据,但是为什么呢?

因为Java字符串由一个char数组,一个起始偏移量和一个长度(以及一个缓存的hashCode)组成。一些String操作(如substring()创建一个新的String对象)将共享原始的char数组,并且具有不同的offset和/或length字段。之所以可行,是因为String的char数组一旦创建就永远不会被修改。

当许多子字符串引用同一基本字符串而不复制重叠部分时,可以节省内存。正如您所注意到的,在某些情况下,它可以防止不再需要的数据被垃圾回收。

解决此问题的“正确”方法是new String(String)构造函数,即

this.smallpart = new String(data.substring(12,18));

顺便说一句,最好的整体解决方案是首先避免具有非常大的String,并以较小的块(一次仅几个KB)处理任何输入。


使用有一个微妙的地方new String(String); 参见stackoverflow.com/a/390854/8946
劳伦斯·多尔

5

在Java中,字符串是不可变的对象,一旦创建了字符串,它就会保留在内存中,直到被垃圾收集器清除为止(这种清除不是您可以理所当然的事情)。

当您调用substring方法时,Java不会创建一个全新的字符串,而只是在原始字符串中存储一系列字符。

因此,当您使用以下代码创建新字符串时:

this.smallpart = data.substring(12, 18) + ""; 

当您将结果与空字符串连接在一起时,您实际上创建了一个新字符串。这就是为什么。


3

正如jwz在1997年所记录的:

如果您有一个巨大的字符串,请拉出它的一个substring(),保留该子字符串,并允许较长的字符串成为垃圾(换句话说,该子字符串的寿命更长),该巨大字符串的基础字节永远不会消失远。


2

总结一下,如果您从少量大字符串中创建许多子字符串,请使用

   String subtring = string.substring(5,23)

由于您仅使用空间来存储大字符串,但是如果要从丢失的大字符串中提取少量的小字符串,则

   String substring = new String(string.substring(5,23));

由于不再需要使用大字符串,因此可以减少内存使用量。

调用new String该字符串有助于提醒您确实在获取新字符串,而不是对原始字符串的引用。


使用有一个微妙的地方new String(String); 参见stackoverflow.com/a/390854/8946
劳伦斯·多尔

2

首先,调用java.lang.String.substringString使用偏移量和长度在原始文件上创建新窗口,而不是复制基础数组的重要部分。

如果我们仔细研究一下该substring方法,我们将注意到一个字符串构造函数调用String(int, int, char[]),并将其完整地传递给char[]字符串,该字符串表示字符串。这意味着子字符串将占用与原始字符串一样多的内存。

好的,但是为什么+ ""需要的内存比没有内存的要少?

做一个+strings通过实施StringBuilder.append方法调用。在AbstractStringBuilder类中查看该方法的实现将告诉我们,它最终arraycopy与我们真正需要的部分(substring)有关。

还有其他解决方法吗?

this.smallpart = new String(data.substring(12,18));
this.smallpart = data.substring(12,18).intern();

0

在字符串后添加“” 有时会节省内存。

假设我有一个巨大的字符串,其中包含一整本书,一百万个字符。

然后,我创建20个字符串,其中包含书中的章节作为子字符串。

然后,我创建包含所有段落的1000个字符串。

然后,我创建包含所有句子的10,000个字符串。

然后,我创建了包含所有单词的100,000个字符串。

我仍然只使用1,000,000个字符。如果在每个章节,段落,句子和单词中添加“”,则使用5,000,000个字符。

当然,如果您仅从整本书中提取一个单词,那就完全不同了,整本书可以被垃圾回收,但这不是因为那个单词拥有对该单词的引用。

如果您有一百万个字符串,并且删除了两端的制表符和空格,说了十次调用来创建一个子字符串,这又是不同的。Java的工作方式或工作方式避免了每次复制一百万个字符。有妥协,如果您知道有什么妥协,那就很好。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.