Java substring方法与内存溢出

说明:本文是阅读《Java程序性能优化》(作者:葛一明)一书中关于substring方法内存溢出一节的笔记。


一、String对象及其特点

1、在C语言中对字符串的处理通常是采用char数组,但是对于数组本身来说,它无法封装字符串操作的一些基本方法,所以在Java中,String对象可以看成是char数组的一种封装。Java中数组的基本实现如下图,主要由char数组、偏移量和String的长度这三部分组成,其中char数组表示的是String的内容,它是String对象所表示的字符串的超集,而String的真实内容就是由这个偏移量和长度在这个char数组中进行定位和截取。理解了这点,就能更好的理解为什么substring方法可能会导致内存溢出。

2、不变性与不变模式

String对象一旦生成,则不能被改变。该特性可以泛化成不变模式,即一个对象的状态在对象被创建之后就不再发生变化。该模式主要用于多线程共享对象,可以省略同步或锁等待的时间,从而提高性能。

3、针对常量池的优化

当两个String对象拥有相同的值时,它们只引用常量池中的同一个拷贝。如下图所示,str1和str2引用了相同的地址,但str3却重新开辟了一块内存空间,但是str3在常量池中的位置和str1是一样的,也就是说,str3虽然单独占用了堆空间,但它指向的实体和str1完全一样。而使用intern()方法就可以返回String对象在常量池中的引用。

4、类的final定义

final定义的String类使得该类不能有任何子类,保护了系统的安全,同时,在JDK1.5之前,使用final定义有助于帮助虚拟机寻找机会,内联所有的final方法,从而提高系统效率;在JDK1.5之后,这种优化的效果并不明显。

二、substring方法与内存溢出

1、Java中Stirng类的substring方法用于截取子字符串,该方法有两个定义:

  • public Stirng substring(int beginIndex)
  • public String substring(int beginIndex, int endIndex)

以第二个方法为例,在JDK6中该方法的实现如下图:

在方法的最后,使用了String类中如下图所示的一个构造函数新建了一个String对象作为返回:

注意这个构造函数的定义与在构造函数之上的注释语句,这是一个包作用域的构造函数,其目的是为了高效而快速的共享String内的char数组。但是这种通过偏移量来截取字符串的方法使得String的原生内容value数组被复制到新的子字符串中,如果原生字符串很大,截取的字符串长度却很短,那么截取的子字符串中包含了原生字符串的所有内容,并占据了相应的内存空间,而仅仅通过偏移量和长度来决定子字符串的实际内容,这虽然提高了运算速度却浪费了大量的内存空间,所以说这个构造函数使用了以空间换取时间的策略,但是这有可能就会导致内存溢出。

2、实例演示substring方法导致内存溢出

如下代码所示,当在main方法中执行的是strA.getSub(1, 5)时,在我的机器上运行时就出现了“Exception in thread "main" java.lang.OutOfMemoryError: Java heap space”的异常信息,跟踪内存和GC情况可以发现程序占用的内存不断扩大,直到溢出,虽然垃圾收集器不停的工作,但是每次回收的内容很小。如果没有出现该信息,可以试着加大for循环的循环次数。而同样的循环次数,对于执行的是strB.getSub(1, 5)时就不会出现该异常信息,跟踪内存和GC情况可以发现垃圾收集器每次总能将系统的内存消耗量释放到初始状态。

[java]  view plain  copy
  1. public class SubstringOOM {  
  2.   
  3.     private static class StrA {  
  4.         // 指定了String对象中的char数组以及长度等信息,这里使用的长度是100000,说明String的原生内容很长  
  5.         private String str = new String(new char[100000]);  
  6.           
  7.         // 使用了有可能导致内存溢出的构造函数来截取子字符串  
  8.         public String getSub(int begin, int end) {  
  9.             return str.substring(begin, end);  
  10.         }  
  11.     }  
  12.       
  13.     private static class StrB {  
  14.         // 指定了String对象中的char数组以及长度等信息,这里使用的长度是100000,说明String的原生内容很长,与StrA中一样  
  15.         private String str = new String(new char[100000]);  
  16.           
  17.         // 虽然使用了有可能导致内存溢出的构造函数来截取子字符串,但是重新生成了String对象  
  18.         public String getSub(int begin, int end) {  
  19.             return new String(str.substring(begin, end));  
  20.         }  
  21.     }  
  22.       
  23.     public static void main(String[] args) {  
  24.         List<String> strList = new ArrayList<String>();  
  25.           
  26.         for (int i = 0; i < 10000; i++) {  
  27.             StrA strA = new StrA();  
  28.             StrB strB = new StrB();  
  29.             strList.add(strA.getSub(15));  
  30.         }  
  31.     }  
  32. }  
当执行的是strB.getSub(1, 5)时,它还使用了没有内存泄漏的String的另一个构造函数来重新生成了String对象,使得由substring方法返回的、有可能存在内存溢出的String对象失去所有的强引用,从而被垃圾收集器识别为垃圾对象而进行回收。

3、关于substring方法中使用的String类构造函数

从前面的分析中知道了substring方法有可能导致内存溢出是因为它使用了一个包内私有的构造函数生成了新的String对象,包内私有就是说我们的应用程序是不能使用它的,所以在实际使用中,不必担心,但是应该了解java.lang包内的对象对它的调用是否会引起内存溢出。比如在JDK6中Integer类的toString(int i)方法中就使用了该构造函数,所以这些方法都有可能和substring方法一样存在内存泄漏的问题。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值