`

Java中的substring真的会引起内存泄露么?

    博客分类:
  • JAVA
 
阅读更多

转:

http://droidyue.com/blog/2014/12/14/substring-memory-issue-in-java/

http://www.cnblogs.com/techyc/p/3324021.html

 

Java中的substring真的会引起内存泄露么?

Dec 14th, 2014

在Java中开发,String是我们开发程序可以说必须要使用的类型,String有一 个substring方法用来截取字符串,我们想必也常常使用。但是你知道么,关于Java 6中的substring是否会引起内存泄露,在国外的论坛和社区有着一些讨论,以至于Java官方已经将其标记成bug,并且为此Java 7 还重新进行了实现。读到这里可能你的问题就来了,substring怎么会引起内存泄露呢?那么我们就带着问题,走进小黑屋,看看substring有没 有内存泄露,又是怎么导致所谓的内存泄露。

基本介绍

substring方法提供两种重载,第一种为只接受开始截取位置一个参数的方法。

1
public String substring(int beginIndex)

比如我们使用上面的方法,"unhappy".substring(2) 返回结果 "happy"

另一种重载就是接受一个开始截取位置和一个结束截取位置的参数的方法。

1
public String substring(int beginIndex, int endIndex)

使用这个方法,"smiles".substring(1, 5) 返回结果 "mile"

通过这个介绍我们基本了解了substring的作用,这样便于我们理解下面的内容。

准备工作

因为这个问题出现的情况在Java 6,如果你的Java版本号不是Java 6 需要调整一下。

终端调整(适用于Mac系统)

查看java版本号

1
2
3
4
13:03 $ java -version
java version "1.8.0_25"
Java(TM) SE Runtime Environment (build 1.8.0_25-b17)
Java HotSpot(TM) 64-Bit Server VM (build 25.25-b02, mixed mode)

切换到1.6

1
export JAVA_HOME=$(/usr/libexec/java_home -v 1.6)

Ubuntu使用alternatives --config java,Fedora上面使用alternatives --config java

如果你使用Eclipse,可以选择工程,右击,选择Properties(属性)— Java Compiler(Java编译器)进行特殊指定。

问题重现

这里贴一下java官方bug里用到的重现问题的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class TestGC {
    private String largeString = new String(new byte[100000]);
    String getString() {
        return this.largeString.substring(0,2);
    }
    public static void main(String[] args) {
        java.util.ArrayList list = new java.util.ArrayList();
        for (int i = 0; i < 1000000; i++) {
            TestGC gc = new TestGC();
            list.add(gc.getString());
        }
    }
}

然而上面的代码,只要使用Java 6 (Java 7和8 都不会抛出异常)运行一下就会报java.lang.OutOfMemoryError: Java heap space的异常,这说明没有足够的堆内存供我们创建对象,JVM选择了抛出异常操作。

于是有人会说,是因为你每个循环中创建了一个TestGC对象,虽然我们加入ArrayList只是两个字符的字符串,但是这个对象中又存储largeString这么大的对象,这样必然会造成OOM的。

然而,其实你说的不对。比如我们看一下这样的代码,我们只修改getString方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class TestGC {
    private String largeString = new String(new byte[100000]);
    String getString() {
        //return this.largeString.substring(0,2);
      return new String("ab");
    }
    public static void main(String[] args) {
        java.util.ArrayList list = new java.util.ArrayList();
        for (int i = 0; i < 1000000; i++) {
            TestGC gc = new TestGC();
            list.add(gc.getString());
        }
    }
}

执行上面的方法,并不会导致OOM异常,因为我们持有的时1000000个ab字符串对象,而TestGC对象(包括其中的largeString)会在java的垃圾回收中释放掉。所以这里不会存在内存溢出。

那么究竟是什么导致的内存泄露呢?要研究这个问题,我们需要看一下方法的实现,即可。

深入Java 6实现

在String类中存在这样三个属性

  • value 字符数组,存储字符串实际的内容
  • offset 该字符串在字符数组value中的起始位置
  • count 字符串包含的字符的长度

Java 6中substring的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
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);
}

上述方法调用的构造方法

1
2
3
4
5
6
//Package private constructor which shares value array for speed.
String(int offset, int count, char value[]) {
  this.value = value;
  this.offset = offset;
  this.count = count;
}

当我们读完上述的代码,我们应该会豁然开朗,原来是这个样子啊!

当我们调用字符串a的substring得到字符串b,其实这个操作,无非就是调整了一下b的offset和count,用到的内容还是a之前的value字符数组,并没有重新创建新的专属于b的内容字符数组。

举个和上面重现代码相关的例子,比如我们有一个1G的字符串a,我们使用substring(0,2)得到了一个只有两个字符的字符串b,如果b的 生命周期要长于a或者手动设置a为null,当垃圾回收进行后,a被回收掉,b没有回收掉,那么这1G的内存占用依旧存在,因为b持有这1G大小的字符数 组的引用。

看到这里,大家应该可以明白上面的代码为什么出现内存溢出了。

共享内容字符数组

其实substring中生成的字符串与原字符串共享内容数组是一个很棒的设计,这样避免了每次进行substring重新进行字符数组复制。正如其文档说明的,共享内容字符数组为了就是速度。但是对于本例中的问题,共享内容字符数组显得有点蹩脚。

如何解决

对于之前比较不常见的1G字符串只截取2个字符的情况可以使用下面的代码,这样的话,就不会持有1G字符串的内容数组引用了。

1
String littleString = new String(largeString.substring(0,2));

下面的这个构造方法,在源字符串内容数组长度大于字符串长度时,进行数组复制,新的字符串会创建一个只包含源字符串内容的字符数组。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public String(String original) {
  int size = original.count;
  char[] originalValue = original.value;
  char[] v;
  if (originalValue.length > size) {
      // The array representing the String is bigger than the new
      // String itself.  Perhaps this constructor is being called
      // in order to trim the baggage, so make a copy of the array.
      int off = original.offset;
      v = Arrays.copyOfRange(originalValue, off, off+size);
  } else {
      // The array representing the String is the same
      // size as the String, so no point in making a copy.
      v = originalValue;
  }
  this.offset = 0;
  this.count = size;
  this.value = v;
}

Java 7 实现

在Java 7 中substring的实现抛弃了之前的内容字符数组共享的机制,对于子字符串(自身除外)采用了数组复制实现单个字符串持有自己的应该拥有的内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public String substring(int beginIndex, int endIndex) {
    if (beginIndex < 0) {
      throw new StringIndexOutOfBoundsException(beginIndex);
    }
    if (endIndex > value.length) {
      throw new StringIndexOutOfBoundsException(endIndex);
    }
    int subLen = endIndex - beginIndex;
    if (subLen < 0) {
      throw new StringIndexOutOfBoundsException(subLen);
    }
    return ((beginIndex == 0) && (endIndex == value.length)) ? this
                : new String(value, beginIndex, subLen);
}

substring方法中调用的构造方法,进行内容字符数组复制。

1
2
3
4
5
6
7
8
9
10
11
12
13
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);
}

真的是内存泄露么

我们知道了substring某些情况下可能引起内存问题,但是这个叫做内存泄露么?

其实个人认为这个不应该算为内存泄露,使用substring生成的字符串b固然会持有原有字符串a的内容数组引用,但是当a和b都被回收之后,该字符数组的内容也是可以被垃圾回收掉的。

哪个版本实现的好

关于Java 7 对substring做的修改,收到了褒贬不一的反馈。

个人更加倾向于Java 6的实现,当进行substring时,使用共享内容字符数组,速度会更快,不用重新申请内存。虽然有可能出现本文中的内存性能问题,但也是有方法可以解决的。

Java 7的实现不需要程序员特殊操作避免了本文中问题,但是进行每次substring的操作性能总会比java 6 的实现要差一些。这种实现显得有点“糟糕”。

问题的价值

虽然这个问题出现在Java 6并且Java 7中已经修复,但并不代表我们就不需要了解,况且Java 7的重新实现被喷的很厉害。

其实这个问题的价值,还是比较宝贵的,尤其是内容字符数组共享这个优化的实现。希望可以为大家以后的设计实现提供帮助和一些想法。

受影响的方法

trim和subSequence都存在调用substring的操作。Java 6和Java 7 substring实现的更改也间接影响到了这些方法。

参考资源

以下三篇文章写得都比较不错,但是都稍微有一些问题,我都已经标明出来,大家阅读时,需要注意。

注意

上面的重现问题的代码中

1
2
3
4
String getString() {
  //return this.largeString.substring(0,2);
      return new String("ab");
}

这里最好不要写成下面这样,因为在JVM中存在字符串常量池,”ab”不会重新创建新字符串,所有的变量都会引用一个对象,而使用new String()则每次重新创建对象。

1
2
3
String getString() {
      return "ab";
}

关于字符串常量池,以后的文章会有介绍。

吐血推荐

如果你对本文这样的内容感兴趣,可以阅读以下Joshua Bloch大神写得书,虽然有点贵,还是英文的。 Java Puzzlers

分享到:
评论

相关推荐

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

    Java 中的 substring 方法是一个非常常用的字符串操作方法,但是在 JDK 1.6 中,如果不当使用该方法,可能会导致严重的内存泄漏问题。下面我们将详细介绍 Java 中由 substring 方法引发的内存泄漏问题的原因和解决...

    java中substring与substr的用法.pdf

    "java中substring与substr的用法" java 中的字符串处理是编程中最基本也是最重要的一部分,substring 和 substr 两个方法是 java 中最常用的字符串处理方法。在本文中,我们将详细介绍 substring 和 substr 两个...

    java中substring与substr的用法参考.pdf

    java中substring与substr的用法参考.pdf

    java 如何使用substring()方法截取子串

    在Java编程语言中,`substring()`方法是字符串类(String)的一个重要成员,它用于从原始字符串中提取子串。这个方法非常实用,特别是在处理文本数据时,我们需要根据特定的需求截取字符串的一部分。下面我们将详细...

    java中substring与substr的用法实用.pdf

    Java 中的 substring 与 substr 方法 Java 语言中提供了两种截取字符串的方法:substring 和 substr,这两种方法都是用于从字符串中提取指定范围的子字符串。下面对这两种方法的用法进行详细介绍: substring 方法...

    java中截取带汉字的字符串

    ### Java中截取带汉字的字符串 在Java编程语言中,处理包含中文字符的字符串时,经常遇到的一个问题是如何正确地截取这些字符串。如果直接按照字节(byte)来进行分割,很容易导致中文字符被截断一半,从而形成乱码。...

    Java如何获取系统cpu、内存、硬盘信息

     前段时间摸索在Java中怎么获取系统信息包括cpu、内存、硬盘信息等,刚开始使用Java自带的包进行获取,但这样获取的内存信息不够准确并且容易出现找不到相应包等错误,所以后面使用sigar插件进行获取。下面列举出...

    Java substring方法实现原理解析

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

    浅谈Java的String中的subString()方法

    总之,`Java` 中的 `String` 类的 `substring()` 方法是提取字符串子串的重要工具,通过指定开始和结束索引,可以灵活地获取所需的部分字符串。正确理解和使用这个方法,能够有效地提升代码的可读性和效率。在编写...

    Java中substring的使用方法

    str=str.substring(int beginIndex);截取掉str从首字母起长度为beginIndex的字符串,将剩余字符串赋值给str;  str=str.substring(int beginIndex,int endIndex);截取str中从beginIndex?始至endIndex结束时的...

    Java截取(提取)子字符串(substring()).pdf

    在Java编程语言中,`substring()`方法是`String`类的一个非常重要的成员,它用于从一个给定的字符串中截取或提取出一部分新的子字符串。`substring()`提供了两种主要的使用形式,这两种形式都在处理字符串时发挥着...

    js substr,substring与java substring和C# substring的区别解析

    js substr(start[,length])表示从start位置开始取length个字符串 js substring(start,end)表示从start,到end之间的字符串,包括start位置的字符但是不包括end... 您可能感兴趣的文章:Js中的substring,substr与C#中的

    Substring字符串截取-kaic

    在编程领域,特别是涉及到文本处理的时候,`substring`方法是一个非常常见且重要的工具,它用于从一个字符串中截取部分子字符串。这个方法在Java、JavaScript等许多编程语言中都有提供,我们主要以Java为例来详细...

    数据库中的substring

    在IT领域,数据库是存储和管理数据的核心工具,而`substring`函数是数据库查询中一个非常重要的字符串操作函数。它允许我们从一个字符串中提取出指定部分,这在处理大量文本数据时尤其有用。让我们深入探讨一下`...

    substring截取字符串-Java中的方法-参考价值不大,需要的下.docx

    这是Java中字符串不可变性的体现。 3. 实际应用: `substring()`方法广泛应用于各种场景,如数据处理、文本分析、日志记录等。例如,如果你有一个很长的URL,可能需要截取其中的一部分;或者在处理用户输入时,可能...

    Java中substring的参数及字符串的相等判断

    字符串操作无疑在各种编程语言及平台上都是必不可少的,功能相通,但用法却存在微妙的区别,比如java中取子串及相等的判断,切入正题。  1. substring  常用的用法包括:  (1)取索引为startidx之后(包括...

    类似subString

    疯狂java讲义第四章作业题,按字节截取一个字符串,遇到汉字的时候会告诉你是哪个汉字的哪个字节

    java 中文Unicode转换

    本文将深入探讨如何在Java中进行中文字符到Unicode编码的转换,以及如何从Unicode编码还原为中文字符。 首先,我们来了解Unicode的基本概念。Unicode是一个国际标准,它为每个字符分配了一个唯一的数字,这个数字被...

    java-16位内存数据转化为double型

    在深入解析这段代码之前,我们首先来了解一下Java中如何实现16位内存数据转化为double型。 ### Java中16位内存数据转化为double型 在Java中,处理二进制、十六进制和浮点数之间的转换,通常涉及到以下步骤: 1. *...

Global site tag (gtag.js) - Google Analytics