`
MouseLearnJava
  • 浏览: 466228 次
  • 性别: 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下面测试出来的结果的。

相关推荐

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

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

    C#获取程序运行内存

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

    string字符串解释

    理解字符串的内存管理对于避免内存泄漏和提高程序性能至关重要。 5. **字符串常量与变量**:字符串可以是常量(不可改变)或变量(可改变)。在C/C++中,字符串常量是字符数组的别名,而Java和Python则有特殊的字符...

    Java宝典大全

    - **熟悉String的使用**: 如何正确使用`String`类的各种方法,如`equals()`、`indexOf()`、`substring()`等。 - **熟悉Java关键字**: 了解Java语言的关键字及其用途,如`final`、`static`、`abstract`等。 - **集合...

    Java SE Tutorials (Last Updated 2011-03-17)

    17. **性能优化**:学习如何分析和优化Java应用程序的性能,包括JVM参数调整、内存管理和代码优化技巧。 通过这些教程,你将能够全面掌握Java SE编程的基本技能,并具备解决实际问题的能力。由于Java是一种广泛应用...

    java考试课件和试题

    10. **Java的内存管理与垃圾收集**: 理解Java的内存模型,尤其是堆和栈的区别,以及垃圾收集的工作原理,对于优化代码性能和避免内存泄漏至关重要。 11. **Java 8及以上版本的新特性**: 比如Lambda表达式、Stream ...

    Java91个迷惑的问题

    4. **字符串操作**:Java中的`String`类有许多方法,如`concat()`, `substring()`, `trim()`等,理解它们的用法和行为差异是必要的。 5. **数组与集合**:Java中的数组和集合(如ArrayList, HashSet, HashMap等)的...

    java初学者完整代码+注释13

    21. **性能优化**:学习如何分析和优化Java程序的性能,包括垃圾回收、内存泄漏和代码优化技巧。 "day13"文件可能包含第13天学习的具体代码示例和练习,涵盖上述知识点的一个或多个部分。通过详细阅读和实践这些...

    ocjp考试题库

    1. String类:深入理解字符串的不可变性,字符串连接的性能优化,以及常用的方法如substring、indexOf、replace等。 2. Date和Calendar类:了解日期时间处理,如何进行日期的计算和格式化。 3. Reflection API:...

    一份面向Java初学者和初级工程师的知识点总结和面试题解析,着重关注面试中最常见的知识点。.zip

    2. **垃圾回收机制**:了解GC的基本原理,包括Minor GC和Major GC,以及如何避免内存泄漏。 3. **设计模式**:单例、工厂、观察者等23种设计模式,是解决常见软件设计问题的模板。 4. **Spring框架**:学习依赖...

    几乎完整的前端面试问题答案

    - 代码优化:学习减少DOM操作、避免内存泄漏、使用缓存等技巧。 - SEO优化:理解网页对搜索引擎的友好性,如元标签的使用。 10. **浏览器工作原理** - 渲染过程:了解浏览器的解析HTML、构建DOM树、CSSOM树,到...

    C#面试题(面试必备!)

    9. **字符串处理**:熟练掌握String类的常用方法,如Substring、IndexOf、Replace、Trim等,以及StringBuilder类在性能优化中的应用。 10. **文件和流**:知道如何读写文件,理解文件流的概念,以及如何使用...

    Java面试题

    垃圾回收(GC)是Java运行时环境的一部分,负责自动管理内存,回收不再使用的对象所占用的空间,避免程序因内存泄漏而导致崩溃。GC的触发时机、回收策略和算法对程序性能有重要影响。 #### 12. Java Web基础知识 -...

    java6string源码-CSP:http://www.streamboard.tv/svn/CSP/

    深入理解String的源码对于提升Java开发技能和优化代码性能至关重要。在这个CSP项目中,我们可以找到对Java 6版本中String类的源码解析,这对于我们学习Java内部机制,特别是字符串操作的底层实现,具有很大的帮助。 ...

    清华大学java课件

    14. **性能优化**:分析和解决Java程序的性能问题,如内存泄漏、CPU占用过高、GC调优等。 15. **并发编程**:深入理解并发编程的相关概念,如并发容器、线程池、并发工具类等。 16. **Spring框架**:学习使用...

    SimpleJava.pdf

    JDK6和JDK7中的substring()方法 - **方法演变**:比较两个版本中`substring()`方法的差异。 - **性能改进**:JDK7中对substring()进行了优化,减少了内部拷贝操作,提高了效率。 - **应用场景**:探讨在不同场景下...

    数据结构与算法java进阶(百度T5推荐)

    - Java中字符串(String)是一个不可变的对象,可以使用String类的各种方法进行字符串操作,如concat()、equals()、substring()等。 - **1.1.4 数组** - 数组是一种基本的数据结构,用于存储相同类型的元素。Java...

    Java-Interview-Prep:精选的Hackerrank问题,算法和其他有用练习的清单,为面试做准备

    10. **JVM内存模型**:理解堆内存、栈内存、方法区、本地方法栈的概念,以及垃圾回收机制(GC),能够分析内存泄漏和性能优化。 11. **Java 8及以后的新特性**:包括Lambda表达式、流API(Stream)、Optional类、...

    黑马程序员入学Java知识(精华总结)

    - **安全性**:Java提供了严格的内存管理机制,减少了内存泄漏和缓冲区溢出的风险。 - **健壮性**:Java强调“一次编写,到处运行”,确保了程序的稳定性。 - **性能优化**:随着JIT编译器的发展,Java的性能得到了...

    Introduction_to_Java

    - 内存区域包括堆、栈、方法区、本地方法栈等,了解其工作原理有助于避免内存泄漏。 15. **JVM(Java虚拟机)** - JVM是Java程序的运行环境,负责解析字节码并执行。 - 了解JVM的工作机制,如类加载、内存模型、...

Global site tag (gtag.js) - Google Analytics