`
MouseLearnJava
  • 浏览: 465461 次
  • 性别: Icon_minigender_1
  • 来自: 杭州
社区版块
存档分类
最新评论

String substring的内存泄漏分析和优化方法

    博客分类:
  • Java
阅读更多
本文将对String.substring方法可能产生内存泄漏的问题进行分析,并给出相应的优化方法。

String.substring内存泄漏分析

首先看一下JDK6 String.substring的源代码:

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[])将继续重用。这种方式提高了运算速度却要在内存中保留原来字符串的内容。

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

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

请看下面的一个例子:

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();
	}
}


避免substring的优化方法

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


2. 使用intern()
	/**
	 * 将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)运行程序,得到的结果如下:
a)采用huge.subString1(0, i)遇到OutOfMemoryError




b)采用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()方法使用有其局限性。这个只有在从大字符串中截取比较小的子字符串,并且原来的字符串不需要再继续使用的场景下有较好的作用。
  • 大小: 44.2 KB
  • 大小: 46.3 KB
3
3
分享到:
评论
4 楼 MouseLearnJava 2013-08-23  
frank-liu 写道
    楼主所说的这种场景适用于连续访问大的字符串的情况,比如说从网上连续读字符串,每次读出来一大堆的时候取中间一部分。在jdk6及更早的版本里用substring()方法会导致新生成的string实际上内部还是有指向原来数组内部char[]的引用,gc就不能把生成的这些大的字符串回收,这样就导致内存溢出了。
    只是举的这个例子有点容易让人误解,因为ArrayList里面添加的元素如果不用remove方法移除的话,后面只要这个ArrayList还在被使用它里面的元素就gc不了。这样如果加入的元素过多,有可能导致把系统内存耗光,这就相当于故意耗内存而不是substring的错了。


谢谢你的评论。

确实,如果一直创建对象并放入ArrayList中也会引起堆内存的溢出。
3 楼 frank-liu 2013-08-23  
    楼主所说的这种场景适用于连续访问大的字符串的情况,比如说从网上连续读字符串,每次读出来一大堆的时候取中间一部分。在jdk6及更早的版本里用substring()方法会导致新生成的string实际上内部还是有指向原来数组内部char[]的引用,gc就不能把生成的这些大的字符串回收,这样就导致内存溢出了。
    只是举的这个例子有点容易让人误解,因为ArrayList里面添加的元素如果不用remove方法移除的话,后面只要这个ArrayList还在被使用它里面的元素就gc不了。这样如果加入的元素过多,有可能导致把系统内存耗光,这就相当于故意耗内存而不是substring的错了。
2 楼 MouseLearnJava 2013-08-22  
JDK1.7后对substring做了改进。JDK1.6及其以下的版本中substring都会共享value。我贴的代码是JDK1.6版本的。

上面的例子只是说明了substring可能产生的问题,以下面的代码为例,

        /** -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));   
       } 
 


尽管我们使用了huge.substring截取了很小部分的子字符串,却占用了char[1000000]的内存空间。这循环体执行过程中,huge添加到了list中,垃圾回收机制并不回回收huge中的字符串。

随着实例的增多,占用的内存空间也将增多,这就是导致出现问题的原因。

目前,很多系统依然采用1.6及其以下的JDK版本,所以,使用substring时还是需要小心点的。 不过,一般情况下,这种问题很少发生。
1 楼 lbfhappy 2013-08-22  
我这里jdk1.7的源码里面是看不到任何内存泄漏的可能的啊。
    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);
    }



以上是String的构造方法,这里的value并没有公用,而是调用Arrays.copyOfRange的方法拷贝出了需要用的char的数组,而不是直接使用原来的。
并且就算是直接使用原来的,共用同一个引用的话,那更是一点问题都没有啊,就算是再多的引用指向,也不会增加多少内存啊,只是多一个引用而已,又不会多很多个char[]的内存分配。

不知道楼主是在什么版本的JDK下面测试出来的结果的。

相关推荐

    Java中由substring方法引发的内存泄漏详解

    下面我们将详细介绍 Java 中由 substring 方法引发的内存泄漏问题的原因和解决方法。 一、内存泄漏的定义 内存泄漏(leak of memory)是指为一个对象分配内存之后,在对象已经不在使用时未及时的释放,导致一直...

    jdk6-8String类

    在JDK的不同版本中,`String`类经历了一些优化和改进,尤其是在性能和内存管理方面。这里我们将对JDK 1.6、1.7和1.8中的`String`类进行对比研究,探讨其中的关键变化。 ### JDK 1.6中的String 在JDK 1.6中,`...

    C语言实现String字符串及其函数stringUtil

    通过分析`stringUtil.c`源代码和`stringUtil.h`头文件,我们可以了解一些关键知识点。 1. **自定义字符串数据结构**:在C语言中,字符串通常以字符数组的形式存在。在`stringUtil.h`中,可能定义了一个名为`String`...

    了解String.substring的原理

    总结来说,`String.substring`在不同的JDK版本中有不同的实现,从JDK 1.6到1.7的改进主要是为了防止因共享字符数组导致的内存泄漏。在编写涉及大量字符串操作的代码时,理解这些细节可以帮助我们更好地控制内存使用...

    Java substring方法实现原理解析

    在 JDK 6 中,使用 substring 方法可能会出现存储空间的浪费,甚至可能造成内存泄漏。在 JDK 7 中,这个问题已经被解决。 Java substring 方法的实现原理是不同的版本的 JDK 中实现方式各不相同。了解这些差异可以...

    string字符串解释

    7. **源码分析**:对于开源语言,如C++标准库中的`std::string`,深入源码可以帮助理解其内部实现机制,如字符串的内存管理策略、效率优化等。 8. **字符串工具**:在开发中,可能会用到一些工具库,如C++的Boost库...

    C#获取程序运行内存

    了解内存使用情况后,开发者可以针对代码进行优化,减少不必要的内存分配,例如及时释放不再使用的对象,避免内存泄漏,或者利用内存池等机制提高内存利用率。 总结起来,通过C#的`System.Diagnostics.Process`类...

    自己编写的string的库函数

    总之,编写自己的string库函数是一个挑战性的任务,它涵盖了内存管理、数据结构、算法、异常处理等多个核心编程概念,同时也是深入理解和掌握C++语言的好方法。你的实践不仅锻炼了编程技巧,也证明了你对C++标准库的...

    Java中String类(字符串操作)的10个常见问题和解决方法

    从JDK 7开始,substring()方法会创建一个新的char[]数组,这样可以避免潜在的内存泄漏。 7. String、StringBuilder与StringBuffer的对比 StringBuilder是可变的,适用于频繁修改字符串内容的场景。StringBuffer也是...

    Java中的String为什么是不可变的共7页.pdf.z

    9. **内存管理**:由于每次修改都会创建新对象,过多的`String`操作可能导致内存泄漏。因此,在处理大量字符串操作时,需要注意性能优化。 10. **垃圾回收**:不可变性也有助于垃圾回收,因为当一个`String`对象...

    Arduino 分割字符串库,strFenGe.rar

    6. **优化和替代方案**:对于内存敏感的项目,可以考虑使用字符数组(C 风格字符串)和指针,或者利用 `split()` 函数的返回值,避免创建过多的 `String` 对象。另外,还可以使用 `char*` 和 `strtok()` 函数从 C ...

    一文带你吃透Java中的String类.rar

    但是,如果String与其他对象组合形成引用链,可能会导致内存泄漏,这时需要注意及时解除引用,以便垃圾回收器能正确工作。 总之,Java中的String类是一个功能强大且基础的类,理解其特性和使用技巧对于提升编程效率...

    每天学点C++(C++实例教程:教程+源码)01string容器.zip

    在实际项目中,正确使用`std::string`可以避免许多内存泄漏和安全性问题。 总之,`std::string`是C++中处理字符串的标准工具,具有高效、灵活和安全的特点。通过学习这个实例教程,你可以掌握如何创建、修改、查询...

    介绍 Java 的内存泄漏

    例如,在JDK6中,String的substring()方法在创建新字符串时,没有正确地解除对原字符串部分字符的引用,导致了内存泄漏。这个问题在JDK7中得到了修复。 防止内存泄漏的一些策略包括: 1. 适时清理集合类,避免使用...

    C++ string详解

    1. **内存管理**:`string`类内部自动管理内存,程序员无需担心内存溢出或泄漏问题。 2. **安全性**:`string`类提供了丰富的接口来确保安全地访问和修改字符串内容。 3. **便捷性**:`string`类支持多种操作,如...

    Android开发规范

    应使用现有的对象数据,如利用substring而不是创建新String对象,直接将字符串附加到StringBuffer中而非创建临时对象,以及优先使用基本数据类型数组代替包装类数组。 2. 使用本地方法:对于字符串处理等性能敏感...

    李兴华老师的面向对象

    ### 面向对象知识点详解 #### 一、引用数据的操作深入 在Java中,数据类型分为两大类:基本数据类型和引用数据类型。其中,引用数据类型包括类...同时,熟悉String类的特性和方法也是日常开发中必不可少的一部分。

    Java宝典大全

    - `HeapOutOfMemory`: 增加堆空间大小,优化内存使用策略。 - `YoungOutOfMemory`: 调整年轻代大小,优化GC策略。 - `MethodAreaOutOfMemory`: 增大PermGen或Metaspace大小。 - `ConstantPoolOutOfMemory`: 减少...

Global site tag (gtag.js) - Google Analytics