在java中我们无须关心内存的释放,JVM提供了内存管理机制,有GC回收不需要的对象。但实际使用中仍然会导致一系列的内存问题,常见的就是内存泄漏和内存溢出。
内存泄漏(leak of memory): 是指为一个对象分配内存之后,在对象已经不再使用时未及时的释放,导致一直占据内存单元,使实际可用内存减少,就好像内存泄漏了一样。
内存溢出(out of memory): 通俗的说就是内存不够用了,比如在一个无限循环中不断创建一个大的对象,很快就会引发内存溢出。
由subString方法引发的内存泄漏
substring(int beginIndex, int endIndex)是String类提供的的一个截取字符串的方法,但是这个方法在JDK6和JDK7中的实现是不同的,了解他们实现的细节上的差异,能够帮助避免在JDK6中不当使用substring导致内存泄漏的问题。
1. substring的作用
substring(int beginIndex, int endIndex)方法返回一个子字符串,从父字符串的beginIndex开始,结束语endIndex-1. 父字符串的下标充0开始,子字符串包含beginIndex而不包含endIndex。
String x = "abcdef"; x = x.substring(1,3); System.out.println(x);
上述程序输出的是“bc”
2. 实现原理
String类是不可变的,当上述第二句中x被重新赋值的时候,它会指向一个新的字符串对象,如下图所示:
然而,这幅图并没有准确说明堆中发生的实际情况,当substring被调用的时候真正发生的才是两者(JDK6和JDK7)的差别。
JDK6中的substring实现
String对象被当做一个char数组来存储,在String类中有3个域:char[] value、int offset、int count, 分别用来存储真是的字符串数组,数组的起始位置,String的字符数。由这3个变量就可以决定一个字符串。当substring方法被调用的时候,它会创建一个新的字符串,但是上述的char数组value仍然会使用原来父数组的那个value。父数组和子数组的唯一差别就是count和offset的位置不一样,下图形象的说明了这一过程:
查看此方法的源代码:
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); //使用的是和父字符串同一个char数组value }
在方法最后,返回了一个新建的String对象。查看该String的构造函数:
// 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内的char数组对象。但在这种通过偏移量来截取字符串的方法中,String原生内容value被复制到新的子字符串中。设想,如果原始字符串很大,截取的字符串长度却很短,那么截取的子字符串中包含了原生字符串的所有内容,并占用了相应的内存空间,而仅仅通过偏移量和长度来决定自己的实际取值。这种算法提高了运算的速度,却浪费了大量的内存空间。(以空间换时间策略)
由此,引发的内存泄漏情况:
String str = new String(new char[100000]); String sub = str.substring(1,3); str = null
这段程序有两个字符串变量str、sub。sub字符串是由父字符串str截取得到的,假如上述这段程序在JDK6中运行,那么sub和str的内部char数组value是公用了同一个,str和sub的差别就在于数组中的起始位置beginIndex和字符串长度count的不同。在第3句,我们使str引用为空,本意是释放str占用的内存空间,但是这个时候GC是无法回收这个大的char数组的,因为还在被sub字符串内部引用着,这种浪费是非常明显的,这就很容易导致内存问题,解决这个问题,可以通过以下方法:
String str = new String(new char[100000]); String sub = str.substring(1,3) + ""; str = null
利用的就是字符串的拼接技术,它会创建一个新的字符串,这个新的字符串会使用一个新的内部char数组存储自己实际需要的字符,这样父字符串的char数组就不会被其他引用,令str=null,在下一次GC回收的时候会回收整个str占用的空间。
JDK7中的substring实现
在JDK7中改进了substring的实现,它实际是为截取的字符串在堆中创建了一个新的插入数组用于保存字符串的字符。下图说明了JDK7中substring的实现过程:
查看JDK7中String类的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 - beginIndex; if (subLen < 0) { throw new StringIndexOutOfBoundsException(subLen); } return ((beginIndex == 0) && (endIndex == value.length)) ? this : new String(value, beginIndex, subLen); }
最后新建的String的构造方法如下:
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); }
Arrays类的copyOfRange方法如下:
public static char[] copyOfRange(char[] original, int from, int to) { int newLength = to - from; if (newLength < 0) throw new IllegalArgumentException(from + " > " + to); char[] copy = new char[newLength]; System.arraycopy(original, from, copy, 0, Math.min(original.length - from, newLength)); return copy; }
由此可见,JDK7中substring是为字符串创建了一个新的char数组去存储子字符串中的字符,这样子字符串和父字符串就没什么必然的联系了,当父字符串的引用失效的时候,GC就会适时的回收父字符串占用的内存空间。
PS: 补充说明String对象及其特点
在Java中,String对象可以认为是char数组的延伸和进一步封装。它主要有3部分组成:char数组、偏移量、String的长度。char数组表示String的内容,它是String对象所表示字符串的超集。String的真实内容还需要由偏移量和长度在这个char数组中进行定位和截取。
String对象有3个基本特点:
- 不变性
- 针对常量池的优化
- 类的final定义
- 不变性是指String对象一旦生成,则不能对它进行改变。String的这个特性可以泛化成不变(immutable)模式,即一个对象的状态在对象被创建之后就不再发生变化。不变模式主要作用于当一个对象需要被多线程共享,并且访问频繁时,可以省略同步和锁等待的时间,从而大幅提高系统性能。
- 针对常量池的优化是指:当两个String对象拥有相同的值时,它们只引用常量池中的同一个拷贝。当同一个字符串反复出现时,这个技术可以大幅度节省内存空间。
String str1 = "abc"; String str2 = "abc"; String str3 = new String("abc"); System.out.println(str1 == str2); //返回true System.out.println(str1 == str3); //返回false System.out.println(str1 == str3.intern()); //返回true
以上代码显示str1 和 str2引用了相同的地址,但是str3重新开辟了一块内存空间,但即便如此,str3在常量池中的位置和str1是一样的,也就是说,虽然str3单独占用了堆空间,但是它所指向的实体和str1完全一样。最后一行使用了intern()方法,该方法返回了String对象在常量池中的应用。
相关推荐
Java 中由 substring 方法引发的内存泄漏详解 Java 中的 substring 方法是一个非常常用的字符串操作方法,但是在 JDK 1.6 中,如果不当使用该方法,可能会导致严重的内存泄漏问题。下面我们将详细介绍 Java 中由 ...
在 JDK 6 中,使用 substring 方法可能会出现存储空间的浪费,甚至可能造成内存泄漏。在 JDK 7 中,这个问题已经被解决。 Java substring 方法的实现原理是不同的版本的 JDK 中实现方式各不相同。了解这些差异可以...
了解内存使用情况后,开发者可以针对代码进行优化,减少不必要的内存分配,例如及时释放不再使用的对象,避免内存泄漏,或者利用内存池等机制提高内存利用率。 总结起来,通过C#的`System.Diagnostics.Process`类...
例如,在JDK6中,String的substring()方法在创建新字符串时,没有正确地解除对原字符串部分字符的引用,导致了内存泄漏。这个问题在JDK7中得到了修复。 防止内存泄漏的一些策略包括: 1. 适时清理集合类,避免使用...
如果忘记释放内存,这部分内存就无法再被程序正确地使用,形成了内存泄漏,长时间累积可能导致程序性能下降甚至崩溃。 在上面的代码示例中,`max`函数使用动态内存分配来存储两个整数中的较大值。函数首先使用`new`...
理解`substring`的工作原理对于编写高效且无内存泄漏的代码至关重要。 1. **String对象放入常量池的时机** 在Java中,`String`对象只有在两种情况下会被放入常量池: - 当使用双引号定义字符串时,如`String str1...
在使用Microsoft SQL Server(MSSQL)数据库时,有时会遇到CPU或内存占用过高的问题,这可能会影响系统的性能和稳定性。本篇文章将探讨如何快速定位导致MSSQL CPU占用高的SQL语句,以帮助优化数据库性能。 首先,...
在JDK 7u6之前的版本中,`substring()`方法会保留对原始字符串的引用,这在内存有限的情况下可能导致OOM。通过调整JVM的内存限制并运行此代码,可以看到内存溢出的情况。 除了以上两个示例,还有一些更复杂的方法...
理解作用域规则(全局、局部和块级)有助于避免变量冲突和内存泄漏。 10. **异步编程**:包括回调函数、Promise和async/await,用于处理非同步操作,如网络请求和定时任务,是现代JavaScript开发的重要部分。 这个...
- 如果库未提供清理方法,记得在不再使用分割结果时释放内存,以防止内存泄漏。 6. **优化和替代方案**:对于内存敏感的项目,可以考虑使用字符数组(C 风格字符串)和指针,或者利用 `split()` 函数的返回值,...
- **Java诊断工具**: Eclipse Memory Analyzer (MAT) 和 VisualVM 等工具可以帮助开发者诊断内存泄漏等问题。 ##### 1.2 Java基础知识 - **阅读源代码**: 了解Java核心库的实现细节对于深入理解Java语言至关重要。...
在1.6中,这个方法可能导致内存泄漏,因为它会将字符串添加到永久代(PermGen)中,这在处理大量动态字符串时可能会导致问题。 ### JDK 1.7中的String JDK 1.7对`String`类进行了一些性能优化,特别是在`...
12. 关闭不再使用的对象:及时关闭或释放不再使用的资源,避免内存泄漏。 【Android应用开发原则】 1. 不要让UI线程等待:在进行耗时操作时,如网络请求或复杂计算,应将操作放在后台线程中,以免阻塞UI线程导致...
它自动回收不再使用的对象所占用的内存,避免内存泄漏。当一个对象没有任何引用指向它时,这个对象就成为垃圾对象。JVM的垃圾收集器会在合适的时间进行垃圾回收,释放内存空间。`finalize()`方法就是在垃圾回收前被...
在上述代码中,注意每个子数组`subString`都是通过`new`关键字动态分配的,因此在使用完毕后需要记得释放内存以避免内存泄漏。但在示例中没有显示释放内存的部分,实际应用中应当在适当的时候调用`delete`。 接着,...
- 内存泄漏:识别和防止内存泄漏的方法。 3. **异常处理** - 异常分类:了解检查异常和运行时异常的区别。 - try-catch-finally:掌握异常处理的基本结构,理解finally块的重要性。 - 自定义异常:如何创建和抛...
从JDK 7开始,substring()方法会创建一个新的char[]数组,这样可以避免潜在的内存泄漏。 7. String、StringBuilder与StringBuffer的对比 StringBuilder是可变的,适用于频繁修改字符串内容的场景。StringBuffer也是...
此外,还需注意避免内存泄漏,确保活动销毁时清理相关资源。 8. **音量控制**:可以使用`AudioManager`来调整音量。`AudioManager`提供了`setStreamVolume()`方法来改变特定音频流(如音乐)的音量。 9. **线程...
Java 中会存在内存泄漏吗,请简单描述。Java 中实现多态的机制是什么?垃圾回收器的基本原理是什么?垃圾回收器可以马上回收内存吗?有什么办法主动通知虚拟机进行垃圾回收?等问题可以根据 Java 的语法和语义来回答...
- 内存溢出与内存泄漏问题 - 垃圾回收机制:自动内存管理 11. **IO与NIO** - NIO(New IO):非阻塞I/O,通道与缓冲区 - AIO(Asynchronous IO):异步I/O操作 12. **网络编程** - Socket编程:TCP与UDP的...