Java String.substring方法可能存在内存泄漏问题?

16

我正在查看String类的API,发现substring方法可能会导致潜在的内存泄漏,因为它与原始字符串共享相同的字符数组。

如果原始字符串很大,那么由substring返回的小字符串可能会防止Java中支持大型数组的原始字符串被垃圾回收。

有什么想法,或者是我对API的理解有误吗。


2
这实际上并不算是内存泄漏,因为字符数组仍然被引用,并且当引用它的所有字符串被回收时可以在后期进行收集。字符数组的一部分可能已经不再使用,但这并不意味着它是一个泄漏。 - cdhowie
1
如果您有100个每个100MB的大字符串,并且您有一个substring(0,1),那么您在技术上持有String类中使用的value[],而不是在您的应用程序中。巨大的字符串可以进行垃圾回收。 - Srujan Kumar Gulla
1
最佳链接 http://javarevisited.blogspot.com/2012/03/why-character-array-is-better-than.html - Premraj
3个回答

20

如果你从一个相当大的字符串中提取子字符串时没有进行复制(通常通过String(String)构造函数),就存在内存泄漏的潜在可能性。

需要注意的是,这个问题自Java 7u6以来已经有所改变。 参见https://bugs.openjdk.java.net/browse/JDK-7197183

原来关于String对象实现享元模式的假设不再被视为有效。

请参见此答案获取更多信息。


两种策略都是有效的。原始实现很好;新实现也很好。但改变实现方式并非明智之举… 这是令人惊讶的不兼容性突破。他们不能因为显而易见的原因而这样做。肯定有其他问题存在。 - irreputable
@irreputable 这显然是一个实现细节,不属于规范的一部分 => 不应该依赖它。这个更改是出于性能原因进行的。 - assylias
2
@assylias 这更像是一个推卸责任的行为。人们已经确立了 substring()/trim() 是O(1)的信念。悄悄地将其更改为 O(n)并不好。那么你如何编写在不同的Java版本上行为一致的代码呢? - irreputable
他们出于性能原因这样做,而不是表面上的那些理由,这些理由非常薄弱。 - irreputable
@irreputable 他们这样做是为了提高字符串的性能,但缺点是子字符串的性能已经下降了... - assylias
他们为了某些更阴谋论的目的而这样做。 "bug"已经被提出了十多年(4513622等); 如果该漏洞由于明显原因是有效的,他们应该早就修复了它。 - irreputable

4
  1. It was the case until Java 7u6 - you would generally deal with the issue by doing:

    String sub = new String(s.substring(...)); // create a new string
    

    That effectively removes the dependency and the original string is now available for GC. This is by the way one of the only scenarios where using the string constructor makes sense.

  2. Since Java 7u6, a new String is created and there is no memory issue any longer.


是的,但这创造了一个新问题。如果您去掉一个空格(trim()),这是非常常见的情况,您最终会复制 N-1 个字符。 - irreputable
1
@irreputable 你总能找到一些边缘情况,它的性能会变差。通用库的目标是平均表现良好,并且在进行更改之前已经考虑了许多不同的用例。 - assylias
同时,复制字符非常快(并不是说它没有成本)。 - assylias
1
trim() 不是一个特殊情况。实际上,str.substring().trim() 是一个非常常见的情况,它涉及到两次复制。O(1)->O(n) 是一件大事。而且,Oracle并不知道这将如何影响现有的应用程序。 - irreputable
@irreputable 我理解你的观点。在考虑到许多变量时(特别是在这种情况下更好地使用CPU缓存),即使经常使用子字符串,也可以平衡这一点。您可以深入研究讨论以获取更多信息。 - assylias

1
在Java 7中,String的subString被修改为:
/**
     * Returns a new string that is a substring of this string. The
     * substring begins with the character at the specified index and
     * extends to the end of this string. <p>
     * Examples:
     * <blockquote><pre>
     * "unhappy".substring(2) returns "happy"
     * "Harbison".substring(3) returns "bison"
     * "emptiness".substring(9) returns "" (an empty string)
     * </pre></blockquote>
     *
     * @param      beginIndex   the beginning index, inclusive.
     * @return     the specified substring.
     * @exception  IndexOutOfBoundsException  if
     *             <code>beginIndex</code> is negative or larger than the
     *             length of this <code>String</code> object.
     */
    public String substring(int beginIndex) {
        if (beginIndex < 0) {
            throw new StringIndexOutOfBoundsException(beginIndex);
        }
        int subLen = value.length - beginIndex;
        if (subLen < 0) {
            throw new StringIndexOutOfBoundsException(subLen);
        }
        return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
    }

因此,每当您使用beginIndex不等于0进行subString操作时,我们都会得到一个新的String对象。

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