java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > String字符串导致的JVM内存泄漏

解读String字符串导致的JVM内存泄漏问题

作者:大力海棠

这篇文章主要介绍了解读String字符串导致的JVM内存泄漏问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教

String类存在于java.lang包中,在程序里使用挺广泛的,用来创建一个字符串对象变量,Java内部对String类做了一些特殊的处理,例如把String类声明成final类型,也就是说不能有子类,String类型对象一旦创建后就不可改变(你可能会想不是可以拼接字符串吗,怎么不可以改变String类对象了,别急,下面慢慢看),以及一些针对JVM的优化等,先来简单看看String类在Java中的一些特点。

被定义为final类

在Java语言中,String类型被定义成final修饰,导致String类不能拥有其他子类,最主要的是出于安全方面问题,JDK中的一些核心类,包括String类,内部实现都不是Java语言,而是调用了系统本地的API,这些API较为底层,需要和操作系统打交道,所以为了安全起见,String类定义为final修饰,不允许被继承,也就不会被重写。

不可改变

String对象的不可改变,也就是不变性,指的是String对象一旦创建成功后,就不能再对它进行修改,

看一个例子:

程序中进行了一次字符串拼接,都在同一个String对象str上操作,虽然如此,但是可以看到两次输出的hashCode是不同的,原因是String对象一旦在JVM常量池里面被创建,那么它的地址就不会被修改,即使我们对对象进行拼接等修改,也只是把新的字符串保存到一个新的地址中去。

所以说,所谓的String不可改变,不是指的该类型实例对象的指向不可改变,我们可以把上面程序中的str对象指向新的字符串地址,但是原来的字符串还是存在于JVM常量池中,没有改变,不可改变指的就是这个,字符串一旦创建后,一直存在,不可修改,对象可以修改指向,不指向它的地址,一旦该字符串没有任何变量指向它后,就等着GC把它回收掉。

常量池优化

String对象不可改变的好处是多线程环境下访问安全,性能高,因为对象不可被修改,所以多线程对它的访问只能是读操作,多个读操作即使不加同步处理也不会出现修改数据导致不一致的问题,所以减少了许多不必要的同步操作,提高了性能。

可以来看一个简单的例子:

证明在JVM内部,当两个String类型对象存放相同的字符串时,它们在常量池内部的引用是一样的:

程序中,String对象str_1和str_2它们的字符串内容是相同的,这两个对象在创建时都各自在堆中分配了自己的空间,所以输出str_1 == str_2的结果为false。

之后我们通过String.intern()方法输出两者在常量池中的引用,发现是一样的,也就是说,两个不同的String对象,因为值相同,所以在常量池中引用的是同一个副本,这是一种常量池优化,为了节省内存空间。

String造成的内存泄漏

内存泄漏上一篇日志讲过,指的是程序由于一些设计上的问题或者执行过程中出错,在申请内存,使用完毕后没有释放资源,内存堆积越来越多,最后堆空间被占用完,具体的例子就是一些已经不再被使用的对象没有被回收,一直常驻在内存中。

虽然GC会帮助我们自动回收那些已经不再被使用的对象,但是如果程序的一些逻辑设计不当,仍然会出现内存泄漏问题,最后导致内存溢出。

举个例子:

如果String类的substring()方法使用不恰当,也有可能导致内存泄漏,不过这个问题随着JDK的更新,早已被修复了,还是总结一下,当作一种设计上的前车之鉴,提醒自己日后留意类似的这种情况(例如创建定长数组时)。

String结构

String.substring()方法导致内存泄漏问题与String类的结构有关,String的结构分为三部分组成,count长度,value数组和offset偏移量,假设有这么一种情况,String对象的value数组可以存储500个字符,count长度标识有10字节,那么这个String对象实际占用的空间是10个字节,剩下的490个字节空间就放在那里了,它们一直没有被使用,直到这个String对象被释放前,它们都会常驻在内存中。

回到String.substring()方法,它的内部实现是这样的:

public String substring(int beginIndex, int endIndex) {
	if(beginIndex < 0) {
		throw new StringIndexOutOfBoundsException(beginIndex);
	}
	if(endIndex > count) {
		throw new StringIndexOutOfBoundsException(endIndexIndex);
	}
	if(beginIndex > endIndex) {
		throw new StringIndexOutOfBoundsException(endIndex - beginIndex);
	}
	return ((beginIndex ==0) && (endIndex == count)) ? this :
		new String(offset + beginIndex, endIndex - beginIndex, value);
}
String(int offset, int count, char value[]) {
	this.value = value;
	this.offset = offset;
	this.count = count;
}

可以看到方法内部对于一些边界情况抛出异常信息,最后调用了String类的构造函数,从传入的参数看,offset偏移量和count变量都发生了改变,但是value数组没有改变,使用的还是原来旧的引用,这么做的问题是,如果旧的String字符串被回收后,这个value值没有得到更新,而是跟着创建新的String对象,那么使用旧的value创建出来的新的String对象中多出来的原来已经被回收了的部分内存,就堆积在那里了,跟着新的对象常驻在内存中,随着字符串拼接操作,substring()方法被多次调用后,便可能会造成内存泄漏。

总结

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

您可能感兴趣的文章:
阅读全文