Java中的substring真的会引起java防止内存泄露露么

Java中堆栈,如何检测内存泄露问题,如何避免?_百度知道内存泄露简介 - 推酷
内存泄露简介
1. 什么是内存泄露?
内存泄露: 对象不再被使用,但是又有引用指向它,所以不能被GC回收。
为了更清楚的理解这个定义,我们需要知道对象在内存中的状态。下面这个插图将对象分为两种状态,
的对象中有一部分是
的。内存泄露就发生在这些对象所在的内存区域。
2. 为什么会发生内存泄露?
让我们来看接下来这个例子,它会告诉我们为什么会发生内存泄露。在这个例子中,对象A指向对象B。对象A的生命周期是(t1-t4)比对象B的生命周期(t2-t3)长很多。当对象B不再被程序所使用的时候,对象A依然指向它。这样的话GC就不能从内存中回收对象B。这样的话就可能会引起内存溢出。因为如果对象A指向很多个这样的对象,内存中就会存在很多不能被回收内存空间的对象。
还有另外一种情况,B指向了其它的很多对象,导致了其它的对象也不能被回收。
3.怎样预防内存泄露?
以下有几点预防内存泄露的小建议:
留心使用集合类,如:HashMap,ArrayList,因为内存泄露通常是它们引起的。当它们被声明为&
,它们的生命周期就跟应用的生命周期相同。
留心使用事件监听和回调函数。如果监听被注册了之后但是该类不再被使用的时候没有注销也会引起内存泄露。
成员变量如果是对象的话,需要使用null来销毁这个对象的引用。
4. 为什么JDK6中的substring()方法会导致内存泄露?
JDK6中subString()的源码
value[])&{
.offset&=&
String&substring(
beginIndex,&
endIndex)&{
//check&boundary
String(offset&+&beginIndex,&endIndex&-&beginIndex,&value);
实际上substring并没有去new 一个String对象,substring返回的字符串和之前的字符串是共用的一个字符数组。
只是数组的起点和长度改变了。所以之前的那个被截取的字符串就没有(也不能)被回收。
如果你想要让它能被回收,可以这样substring.
x.substring(a,b)+
String(x.substring(a,b))
StringBuilder&sb&=&
StringBuilder();
sb.append(x.substring(x,&y));
sb.append(
x&=&sb.toString();
所以,使用new String(x.substring(a,b))的方式效率更高。
原文:/2013/10/the-introduction-of-memory-leak-what-why-and-how/
已发表评论数()
请填写推刊名
描述不能大于100个字符!
权限设置: 公开
仅自己可见
正文不准确
标题不准确
排版有问题
主题不准确
没有分页内容
图片无法显示
视频无法显示
与原文不一致在Java中开发,String是我们开发程序可以说必须要使用的类型,String有一个substring方法用来截取字符串,我们想必也常常使用。但是你知道么,关于Java 6中的substring是否会引起内存泄露,在国外的论坛和社区有着一些讨论,以至于Java官方已经将其标记成bug,并且为此Java 7 还重新进行了实现。读到这里可能你的问题就来了,substring怎么会引起内存泄露呢?那么我们就带着问题,走进小黑屋,看看substring有没有内存泄露,又是怎么导致所谓的内存泄露。
substring方法提供两种重载,第一种为只接受开始截取位置一个参数的方法。
public String substring(int beginIndex)
比如我们使用上面的方法,”unhappy”.substring(2) 返回结果 ”happy”
另一种重载就是接受一个开始截取位置和一个结束截取位置的参数的方法。
public String substring(int beginIndex, int endIndex)
使用这个方法,”smiles”.substring(1, 5) 返回结果 ”mile”
通过这个介绍我们基本了解了substring的作用,这样便于我们理解下面的内容。
因为这个问题出现的情况在Java 6,如果你的Java版本号不是Java 6 需要调整一下。
终端调整(适用于Mac系统)
查看java版本号
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)
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里用到的重现问题的代码。
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方法。
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的实现
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);
上述方法调用的构造方法
//Package private constructor which shares value array for speed.
String(int offset, int count, char value[]) {
this.value =
this.offset =
this.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字符串的内容数组引用了。
String littleString = new String(largeString.substring(0,2));
下面的这个构造方法,在源字符串内容数组长度大于字符串长度时,进行数组复制,新的字符串会创建一个只包含源字符串内容的字符数组。
public String(String original) {
int size = original.
char[] originalValue = original.
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.
v = Arrays.copyOfRange(originalValue, off, off+size);
// The array representing the String is the same
// size as the String, so no point in making a copy.
v = originalV
this.offset = 0;
this.count =
this.value =
Java 7 实现
在Java 7 中substring的实现抛弃了之前的内容字符数组共享的机制,对于子字符串(自身除外)采用了数组复制实现单个字符串持有自己的应该拥有的内容。
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 - beginI
if (subLen & 0) {
throw new StringIndexOutOfBoundsException(subLen);
return ((beginIndex == 0) && (endIndex == value.length)) ? this
: new String(value, beginIndex, subLen);
substring方法中调用的构造方法,进行内容字符数组复制。
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实现的更改也间接影响到了这些方法。
以下三篇文章写得都比较不错,但是都稍微有一些问题,我都已经标明出来,大家阅读时,需要注意。
本文中解决java6中问题提到的字符串拼接不推荐,具体原因可以参考
本文中提到的有一个概念错误,新的字符串不会阻止旧的字符串被回收,而是阻止旧字符串中的内容字符数组。阅读时需要注意。
本文中提到的有一个测试,使用非new的形式有一点问题,其忽视了字符串常量池的存在,具体查看下面的注意。
上面的重现问题的代码中
String getString() {
//return this.largeString.substring(0,2);
return new String("ab");
这里最好不要写成下面这样,因为在中存在字符串常量池,”ab”不会重新创建新字符串,所有的变量都会引用一个对象,而使用new String()则每次重新创建对象。
String getString() {
return "ab";
关于字符串常量池,以后的文章会有介绍。
另外,这篇文章对的10大问题进行了详细的介绍,有兴趣的朋友也可以看看。
在文章中找不到问题答案?您还可以
热门栏目订阅java(53)
/blog//substring-memory-issue-in-java/
public String substring(int beginIndex)
public String substring(int beginIndex, int endIndex)
终端调整(适用于Mac系统)
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)
export JAVA_HOME=$(/usr/libexec/java_home -v 1.6)
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());
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());
深入Java 6实现
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);
//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;
共享内容字符数组
String littleString = new String(largeString.substring(0,2));
public String(String original) {
int size = original.count;
char[] originalValue = original.value;
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);
// 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 实现
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);
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 getString() {
//return this.largeString.substring(0,2);
return new String(&ab&);
String getString() {
return &ab&;
参考知识库
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
访问:46721次
积分:1139
积分:1139
排名:千里之外
原创:59篇
转载:70篇
(1)(3)(2)(1)(11)(2)(1)(13)(1)(12)(7)(1)(1)(4)(9)(2)(5)(3)(3)(3)(4)(7)(1)(1)(2)(3)(2)(2)(4)(6)(5)(3)(4)(1)内存泄漏:Java最显著的优势之一就是它的内存管理机制。你只需简单创建对象,然后Java垃圾回收机制便会小心的分配和释放内存。然而,事实并非如此简单,因为在Java应用程序中经常发生内存泄漏。下文说明了什么是内存泄漏,为什么会发生,以及如何防止它们。
1.什么是内存泄漏?
内存泄漏的定义: 对象不再被应用程序使用,但是垃圾回收器却不能移除它们,因为它们正在被引用。
要理解这个定义,我们需要理解对象在内存中的状态,下图说明了哪些是未被使用的以及哪些是未被引用的。
从图中可以看到被引用的对象和未被引用的对象。未被引用的对象将会被垃圾回收器回收,而被引用对象则不会被回收。未被引用的对象理所当然是未被使用的,因为没有其他的对象引用它。然而,未被使用的对象并不一定是未被引用的,其中一些是被引用的。这就是内存泄漏的起因。
2.为什么会发生内存泄漏?
让我们来看看下面这个例子,看看为什么内存泄漏会发生。在如下例子中,对象A引用了对象B。A的生命周期(t1—t4)要比B的生命周期(t2—t3)长很多。当B不再用于应用中时,A仍然持有对它的引用。在这种方式下,垃圾回收器就不能将B从内存中移除。这将可能导致出现内存不足的问题,因为如果A对更多的对象做同样的事情,那么内存中将会有很多无法被回收的对象,这将极度耗费内存空间。
也有可能B持有大量对其他对象的引用,这些被B引用的对象也不能够被回收。所有这些未被使用的对象将会耗费宝贵的内存空间。
3.如何阻止内存泄漏?
以下是一些阻止内存泄漏的快速动手技巧。
(1)注意集合类,例如HashMap,ArrayList,等等。因为它们是内存泄漏经常发生的地方。当它们被声明为静态时,它们的生命周期就同应用程序的生命周期一般长。
(2)注意事件监听器和回调,如果一个监听器已经注册,但是当这个类不再被使用时却未被注销,就会发生内存泄漏。
(3)“如果一个类管理它自己的内存,程序员应该对内存泄漏保持警惕。”[1] 很多时候当一个对象的成员变量指向其他对象时,不再使用时需要被置为null。
4.一个小测验:为什么在JDK6中substring()方法会引起内存泄漏?
为了回答这个问题,您可能需要阅读。
substring(int beginIndex, int endIndex)在JDK6与JDK7中的实现方式不一样,理解他们的差异有助于更好的使用它们。为了简单起见,下面所说的substring()指的就是substring(int beginIndex, int endIndex)方法。
1.substring()是做什么的?
substring(int beginIndex ,int endIndex)方法返回一个子字符串,返回的是从原字符串的beginIndex到endIndex-1之间的内容。
2.当substring()被调用的时候,内部发生什么事?
你或许会认为由于x是不可变的对象,当x被x.substring(1,3)返回的结果赋值后,它将指向一个全新的字符串如下图:
然而,这个图并不完全正确,或者说并没有完全表示出java 堆中真正发生的事情。那么当调用substring()的时候到底发生的了什么事呢?JDK 6与JDK7的substring方法实现有什么不一样呢?
3.JDK6中的substring()
java中字符串是通过字符数组来支持实现的,在JDK6中,String类包含3个域,char[] value、int offset、int count。分别用于存储真实的字符数组、数组的偏移量,以及String所包含的字符的个数。
当substring()方法被调用的时候,它会创建一个新的字符串对象,但是这个字符串的值在java 堆中仍然指向的是同一个数组,这两个字符串的不同在于他们的count和offset的值。
下面是jdk6中的原代码,是简化后只包含用来说明这个问题的关键代码:
4.jdk6中substring()将会导致的问题
如果你有一个非常长的字符串,但是你仅仅只需要这个字符串的一小部分,这就会导致性能问题(译注:可能会造成内存泄露,这个很早以前就有提及),因为你需要的只是很小的部分,而这个子字符串却要包含整个字符数组,在jdk6中解决办法就是使用下面的方法,它会指向一个真正的子字符串。
5.JDK7中的substring()
在JDK7中有所改进,substring()方法在堆中真正的创建了一个新的数组,当原字符数组没有被引用后就被GC回收了.因此避免了上述问题.
---------------------- 、、期待与您交流! ----------------------
参考知识库
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
访问:2671次
排名:千里之外
(2)(1)(1)(7)

我要回帖

更多关于 java 内存泄露分析 的文章

 

随机推荐