String substring的内存泄漏分析

2023-02-23 15:35:28 浏览数 (1)

String类的substring方法,为我们截取子字符串提供了便捷,但同时,如果使用的JDK是1.6版本,则使用不当的话可能导致内存泄露~

一、String#substring源码分析

本示例查看的是JDK1.6版本的substring方法~

代码语言:javascript复制
public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence
{
    /** The value is used for character storage. */
    private final char value[];

/**
     * 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) {
  return substring(beginIndex, count);
    }

    /**
     * Returns a new string that is a substring of this string. The
     * substring begins at the specified <code>beginIndex</code> and
     * extends to the character at index <code>endIndex - 1</code>.
     * Thus the length of the substring is <code>endIndex-beginIndex</code>.
     * <p>
     * Examples:
     * <blockquote><pre>
     * "hamburger".substring(4, 8) returns "urge"
     * "smiles".substring(1, 5) returns "mile"
     * </pre></blockquote>
     *
     * @param      beginIndex   the beginning index, inclusive.
     * @param      endIndex     the ending index, exclusive.
     * @return     the specified substring.
     * @exception  IndexOutOfBoundsException  if the
     *             <code>beginIndex</code> is negative, or
     *             <code>endIndex</code> is larger than the length of
     *             this <code>String</code> object, or
     *             <code>beginIndex</code> is larger than
     *             <code>endIndex</code>.
     */
    public String substring(int beginIndex, int endIndex) {
  if (beginIndex < 0) {
      throw new StringIndexOutOfBoundsException(beginIndex);
  }
  if (endIndex > count) {
      throw new StringIndexOutOfBoundsException(endIndex);
  }
  if (beginIndex > endIndex) {
      throw new StringIndexOutOfBoundsException(endIndex - beginIndex);
  }
  return ((beginIndex == 0) && (endIndex == count)) ? this :
      new String(offset   beginIndex, endIndex - beginIndex, value);
    }

}

从上述的源代码可以看出,使用substring获取子字符串方法中,原有字符串的内容value(char[])将继续重用

代码语言:javascript复制
new String(offset   beginIndex, endIndex - beginIndex, value);

这种方式提高了运算速度却要在内存中保留原来字符串的内容

二、substring内存泄露示例

示例 --> 读取一个5000个字符的字符串,采用substring截取其中的30个字符,在这种情况下,30个字符在内存中还是使用了5000个字符。

设想一下:

如果字符串更大,比如一百万个字符,而substring只需要其中的几十个,这样的情况下将会占有较多的内存空间。如果实例多需要调用的次数多,那么很容易造成内存泄漏。

请看下面的一个例子:

代码语言:javascript复制
package my.memoryLeak;

import java.util.ArrayList;
import java.util.List;

public class MemoryLeakExample {

  public static void main(String[] args) {
    
    /** -XX:PermSize=1M -XX:MaxPermSize=1M */
    List<String> substringList = new ArrayList<String>();
    
    /**
     * 循环3000次。
     * 第i次循环截取前i个字符串
     */
    for (int i = 1; i <= 3000; i  ) {
      HugeString huge = new HugeString();
      System.out.println(i);
      substringList.add(huge.subString1(0, i));
    }
  }
}

class HugeString {
  
  private String str = new String(new char[1000000]);

  /**
   * 调用String的subString方法来实现。
   * 例如:读取一个5000个字符的字符串,采用substring截取其中的30个字符,在这种情况下,30个字符在内存中还是使用了5000个字符。
     * 设想一下:如果字符串更大,比如一百万个字符,而substring只需要其中的几十个,
     * 这样的情况下会将会占有较多的内存空间。如果实例多需要调用的次数多,那么很容易造成内存泄漏。
   * Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
   *        at java.util.Arrays.copyOf(Unknown Source)
   *        at java.lang.String.<init>(Unknown Source)
   *        at my.memoryLeak.Huge.<init>(LeakTest.java:38)
   *        at my.memoryLeak.MemoryLeakExample.main(MemoryLeakExample.java:13)
   * 
   */
  public String subString1(int begin, int end) {
    return str.substring(begin, end);
  }

  /**
   * 采用新建的方式,避免在内存中占有较多的内容。
   */
  public String subString2(int begin, int end) {
    return new String(str.substring(begin, end));
  }

  /**
   * 将substring的内容存放到常量池。
   * 这种情况下,会用到PermGen space,如果过度使用,可能导致PermGen Sapce用完,跑出异常。
   * 
   * 可以使用如下参数调整大小,如
   *  -XX:PermSize=1M -XX:MaxPermSize=1M
   *  
   *  Exception in thread "main" java.lang.OutOfMemoryError: PermGen space
   *           at java.lang.String.intern(Native Method)
   *           at my.memoryLeak.HugeString.subString3(MemoryLeakExample.java:55)
   *           at my.memoryLeak.MemoryLeakExample.main(MemoryLeakExample.java:15)
   */
  public String subString3(int begin, int end) {
    return str.substring(begin, end).intern();
  }
}

三、避免方法

3.1 创建新的字符串

代码语言:javascript复制
  /**
   * 采用新建的方式,避免在内存中占有较多的内容。
   */
  public String subString2(int begin, int end) {
    return new String(str.substring(begin, end));
  }

3.2 使用intern方法

代码语言:javascript复制
  /**
   * 将substring的内容存放到常量池。
   * 这种情况下,会用到PermGen space,如果过度使用,可能导致PermGen Sapce用完,跑出异常。
   * 
   * 可以使用如下参数调整大小,如
   *  -XX:PermSize=1M -XX:MaxPermSize=1M
   *  
   *  Exception in thread "main" java.lang.OutOfMemoryError: PermGen space
   *           at java.lang.String.intern(Native Method)
   *           at my.memoryLeak.HugeString.subString3(MemoryLeakExample.java:55)
   *           at my.memoryLeak.MemoryLeakExample.main(MemoryLeakExample.java:15)
   */
  public String subString3(int begin, int end) {
    return str.substring(begin, end).intern();
  }

在测试代码中,采用默认VM参数的,

分别调用huge.subString1(0, i), huge.subString2(0, i)和huge.subString3(0, i)运行程序,得到的结果如下:

  • 采用huge.subString1(0, i)遇到OutOfMemoryError
  • 采用huge.subString2(0, i)和huge.subString3(0, i)的运行正常

采用intern()方法会有其它的影响,因为我们将使用PermGen Space. 除非VM有足够的空间,否则也会抛出OutOfMemoryError.

比如:

使用参数-XX:PermSize=1M -XX:MaxPermSize=1M

采用huge.subString3(0, i)再运行一下:

在这种情况下,只有采用huge.subString2(0, i)的方式还能正常运行,采用huge.subString1(0, i)和huge.subString3(0, i)方法都产生了OutOfMemoryError。 比较一下打印出来的循环次数,采用intern()方法运行次数比直接采用String.substring的运行次数多很多。 通过上面的例子可以得出如下几个结论:

  1. String.substring存在内存泄漏的危险。
  2. 采用新建字符串和String.intern()的方法可以优化直接调用String.substring。

首先选择的是新建字符串。其次才是选择通过intern()方法。intern()方法使用有其局限性。这个只有在从大字符串中截取比较小的子字符串,并且原来的字符串不需要再继续使用的场景下有较好的作用。

JDK 1.7之后,已经对这快代码进行了优化和调整,JDK1.7中substring的源码如下:

代码语言:javascript复制
    public String(char value[], int offset, int count) {
        if (offset < 0) {
            throw new StringIndexOutOfBoundsException(offset);
        }
        if (count < 0) {
            throw new StringIndexOutOfBoundsException(count);
        }
        // Note: offset or count might be near -1>>>1.
        if (offset > value.length - count) {
            throw new StringIndexOutOfBoundsException(offset   count);
        }
        this.value = Arrays.copyOfRange(value, offset, offset count);
    }

这里的value并没有公用,而是调用Arrays.copyOfRange的方法拷贝出了需要用的char的数组,而不是直接使用原来的。

0 人点赞