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

我使用了一个包含大量数据的变量,比如 String data。 我想以下面的方式使用这个字符串的一小部分:

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

经过几个小时的调试(使用内存可视化工具) ,我发现对象字段 smallpart记住了来自 data的所有数据,尽管它只包含子字符串。

当我把代码改成:

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

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

这怎么可能? 有人能解释一下吗? 我想这个.smallpart 一直在引用数据,但是为什么呢?

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

9084 次浏览

做以下事情:

data.substring(x, y) + ""

创建一个新的(较小的) String 对象,并抛弃对 substring ()创建的 String 的引用,从而启用对。

重要的是要意识到,substring()提供了一个窗口到 存在字符串-或者更确切地说,字符数组底层的原始字符串。因此,它将消耗与原来的 String 相同的内存。这在某些情况下可能是有利的,但是如果您希望获得一个子字符串并释放原始 String (正如您已经发现的那样) ,那么就有问题了。

有关更多信息,请查看 JDK String 源代码中的 Substring ()方法

编辑: 为了回答你的补充问题,从子字符串构造一个新的字符串可以减少内存消耗,提供可以将任何对原来字符串的引用存入。

注(2013年1月)。上述行为改变了 用 Java 7u6写的。该享元模式已不再使用,而且 abc0将按照您的预期工作。

当您使用 substring时,它实际上并不创建新的字符串。它仍然引用原始字符串,带有偏移量和大小约束。

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

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

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

因此,当您使用这段代码创建一个新字符串时:

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

当您将结果与空字符串连接时,实际上创建了一个新字符串。 这就是原因。

如果你查看 substring(int, int)的源代码,你会看到它返回:

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

其中 value是原来的 char[]。所以你得到一个新的字符串,但是 一样底层是 char[]

当您这样做时,data.substring() + "",您将得到一个新的字符串,其中 新的底层为 char[]

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

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

我想是这个,保留了一小部分 引用数据,但是为什么呢?

因为 Java 字符串由一个字符数组、一个起始偏移量和一个长度(以及一个缓存的 hashCode)组成。一些字符串运算如 substring()创建一个新的 String 对象,它共享原始的 char 数组,只是有不同的偏移量和/或长度字段。这是因为 String 的 char 数组一旦创建就不会被修改。

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

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

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

顺便说一句,总体上最好的解决方案是首先避免使用非常大的 String,并且以较小的块(a a few KB at a a time)处理任何输入。

根据 JWZ 在1997年的记录:

如果你有一个巨大的字符串,取出它的子字符串() ,保留子字符串并允许较长的字符串变成垃圾(换句话说,子字符串有更长的生命周期)巨大字符串的底层字节永远不会消失。

总而言之,如果您从少量的大字符串中创建了大量的子字符串,那么使用

   String subtring = string.substring(5,23)

因为您只使用空间来存储大字符串,但是如果从丢失的大字符串中提取少量小字符串,那么

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

将保持您的内存使用下降,因为大字符串可以回收时,不再需要。

您调用 new String是一个有用的提醒,它提醒您确实获得了一个新字符串,而不是对原始字符串的引用。

首先,调用 ABC0会在原来的 String上创建新窗口使用偏移量和长度 < 强 > 代替复制底层数组的重要部分。

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

好吧,但是为什么 + ""导致内存需求比没有它的时候要少呢

strings上执行 +是通过 StringBuilder.append方法调用实现的。看看这个方法在 AbstractStringBuilder类中的实现,它将告诉我们它最终用我们真正需要的部分(substring)完成了 arraycopy

还有别的办法吗?

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

在字符串后面加上“”将节省内存。

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

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

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

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

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

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

当然,如果您只从整本书中提取一个单词,并且整本书可以被垃圾收集,那么情况就完全不同了,但这并不是因为这个单词包含了对它的引用。

如果你有一个100万字符的字符串,并且在两端移除制表符和空格,比如说调用10次来创建一个子字符串,那么情况又是不同的。Java 的工作方式避免了每次复制一百万个字符。有妥协,如果你知道什么是妥协是好的。