String在java内存_[Java]String内存陷阱简介

String 方法用于文本分析及大量字符串处理时会对内存性能造成一些影响。可能导致内存占用太大甚至OOM。

一、先介绍一下String对象的内存占用

一般而言,Java 对象在虚拟机的结构如下:

•对象头(object header):8 个字节(保存对象的 class 信息、ID、在虚拟机中的状态)

•Java 原始类型数据:如 int, float, char 等类型的数据

•引用(reference):4 个字节

•填充符(padding)

trans.gif

String定义:

JDK6:

private final char value[];

private final int offset;

private final int count;

private int hash;

JDK6的空字符串所占的空间为40字节

JDK7:

private final char value[];

private int hash;

private transient int hash32;

JDK7的空字符串所占的空间也是40字节

JDK6字符串内存占用的计算方式:

首先计算一个空的 char 数组所占空间,在 Java 里数组也是对象,因而数组也有对象头,故一个数组所占的空间为对象头所占的空间加上数组长度,即 8 + 4 = 12 字节 , 经过填充后为 16 字节。

那么一个空 String 所占空间为:

对象头(8 字节)+ char 数组(16 字节)+ 3 个 int(3 × 4 = 12 字节)+1 个 char 数组的引用 (4 字节 ) = 40 字节。

因此一个实际的 String 所占空间的计算公式如下:

8*( ( 8+12+2*n+4+12)+7 ) / 8 = 8*(int) ( ( ( (n) *2 )+43) /8 )

其中,n 为字符串长度。

二、举个例子:

1、substring

package demo;

import java.io.BufferedReader;

import java.io.File;

import java.io.FileInputStream;

import java.io.InputStreamReader;

public class TestBigString

{

private String strsub;

private String strempty = new String();

public static void main(String[] args) throws Exception

{

TestBigString obj = new TestBigString();

obj.strsub = obj.readString().substring(0,1);

Thread.sleep(30*60*1000);

}

private String readString() throws Exception

{

BufferedReader bis = null;

try

{

bis = new BufferedReader(new InputStreamReader(new FileInputStream(new File("d:\\teststring.txt"))));

StringBuilder sb = new StringBuilder();

String line = null;

while((line = bis.readLine()) != null)

{

sb.append(line);

}

System.out.println(sb.length());

return sb.toString();

}

finally

{

if (bis != null)

{

bis.close();

}

}

}

}

其中文件"d:\\teststring.txt"里面有33475740个字符,文件大小有35M。

用JDK6来运行上面的代码,可以看到strsub只是substring(0,1)只取一个,count确实只有1,但其占用的内存却高达接近67M。

11.jpg

然而用JDK7运行同样的上面的代码,strsub对象却只有40字节

2.jpg

什么原因呢?

来看下JDK的源码:

JDK6:

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;

}

JDK7:

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);

}

可以看到原来是因为JDK6的String.substring()所返回的 String 仍然会保存原始 String的引用,所以原始String无法被释放掉,因而导致了出乎意料的大量的内存消耗。

JDK6这样设计的目的其实也是为了节约内存,因为这些 String 都复用了原始 String,只是通过 int 类型的 offerset, count 等值来标识substring后的新String。

然而对于上面的例子,从一个巨大的 String 截取少数 String 为以后所用,这样的设计则造成大量冗余数据。 因此有关通过 String.split()或 String.substring()截取 String 的操作的结论如下:

•对于从大文本中截取少量字符串的应用,String.substring()将会导致内存的过度浪费。

•对于从一般文本中截取一定数量的字符串,截取的字符串长度总和与原始文本长度相差不大,现有的 String.substring()设计恰好可以共享原始文本从而达到节省内存的目的。

既然导致大量内存占用的根源是 String.substring()返回结果中包含大量原始 String,那么一个减少内存浪费的的途径就是去除这些原始 String。如再次调用 newString构造一个的仅包含截取出的字符串的 String,可调用 String.toCharArray()方法:

String newString = new String(smallString.toCharArray());

2、同样,再看看split方法

public class TestBigString

{

private String strsub;

private String strempty = new String();

private String[] strSplit;

public static void main(String[] args) throws Exception

{

TestBigString obj = new TestBigString();

obj.strsub = obj.readString().substring(0,1);

obj.strSplit = obj.readString().split("Address:",5);

Thread.sleep(30*60*1000);

}

JDK6中分割的字符串数组中,每个String元素占用的内存都是原始字符串的内存大小(67M):

3.jpg

而JDK7中分割的字符串数组中,每个String元素都是实际的内存大小:

41.jpg

原因:

JDK6源代码:

public String[] split(String regex, int limit) {

return Pattern.compile(regex).split(this, limit);

}

public String[] split(CharSequence input, int limit) {

int index = 0;

boolean matchLimited = limit > 0;

ArrayList matchList = new ArrayList();

Matcher m = matcher(input);

// Add segments before each match found

while(m.find()) {

if (!matchLimited || matchList.size() < limit - 1) {

String match = input.subSequence(index, m.start()).toString();

matchList.add(match);

public CharSequence subSequence(int beginIndex, int endIndex) {

return this.substring(beginIndex, endIndex);

}

三、其他方面:

1、String a1 = “Hello”; //常量字符串,JVM默认都已经intern到常量池了。

创建字符串时 JVM 会查看内部的缓存池是否已有相同的字符串存在:如果有,则不再使用构造函数构造一个新的字符串,

直接返回已有的字符串实例;若不存在,则分配新的内存给新创建的字符串。

String a2 = new String(“Hello”); //每次都创建全新的字符串

2、在拼接静态字符串时,尽量用 +,因为通常编译器会对此做优化。

public String constractStr()

{

return "str1" + "str2" + "str3";

}

对应的字节码:

Code:

0:   ldc     #24; //String str1str2str3         --将字符串常量压入栈顶

2:   areturn

3、在拼接动态字符串时,尽量用 StringBuffer 或 StringBuilder的 append,这样可以减少构造过多的临时 String 对象(javac编译器会对String连接做自动优化):

public String constractStr(String str1, String str2, String str3)

{

return str1 + str2 + str3;

}

对应字节码(JDK1.5之后转换为调用StringBuilder.append方法):

Code:

0:   new     #24; //class java/lang/StringBuilder

3:   dup

4:   aload_1

5:   invokestatic    #26; //Method java/lang/String.valueOf:(Ljava/lang/Objec

t;)Ljava/lang/String;

8:   invokespecial   #32; //Method java/lang/StringBuilder."":(Ljava/la

ng/String;)V

11:  aload_2

12:  invokevirtual   #35; //Method java/lang/StringBuilder.append:(Ljava/lang

/String;)Ljava/lang/StringBuilder;

15:  aload_3

16:  invokevirtual   #35; //Method java/lang/StringBuilder.append:(Ljava/lang

/String;)Ljava/lang/StringBuilder;  ――调用StringBuilder的append方法

19:  invokevirtual   #39; //Method java/lang/StringBuilder.toString:()Ljava/l

ang/String;

22:  areturn     ――返回引用

5

6

分享到:

18e900b8666ce6f233d25ec02f95ee59.png

72dd548719f0ace4d5f9bca64e1d7715.png

2012-09-23 00:42

浏览 12411

评论

9 楼

lin_yp

2012-09-23

tlde_ti 写道

http://stackoverflow.com/questions/2514349/lost-string-garbage-collection

------------------

看第一个回答,用new String(..)来使largestring回收只在 smallstring存在很长时间的情况下才应该使用.一般情况下smallstring存在的时间都比较短,smallstring生命周期结束以后largestring也就自动回收了,没有必要自己手动提前回收.

这篇文章针对的就是smallstring被缓存长期使用的情况

8 楼

leaow567

2012-09-23

辛苦了,学习了

7 楼

lin_yp

2012-09-23

内存分析用的是MemoryAnalyzer工具

6 楼

lucky16

2012-09-23

同楼下啊,,

楼主用的什么tool查看的内存分析啊?

是eclipse的插件还是其他的独立工具啊?

看着好像很强大的样子。。 文章分析的也还是比较透彻。

5 楼

来这里学java

2012-09-23

求教楼主使用什么工具来查看内存的??

ccf5e5c2a4777a14201b1dce9adb5168.gif

4 楼

tlde_ti

2012-09-23

http://stackoverflow.com/questions/2514349/lost-string-garbage-collection

------------------

看第一个回答,用new String(..)来使largestring回收只在 smallstring存在很长时间的情况下才应该使用.一般情况下smallstring存在的时间都比较短,smallstring生命周期结束以后largestring也就自动回收了,没有必要自己手动提前回收.

3 楼

jasshine

2012-09-23

很叼哦。。没仔细研究过。

2 楼

xieyongwei

2012-09-23

辛苦了

希望JE多一些这样有意义的文章发表

1 楼

rox

2012-09-23

写的很详细,辛苦了。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值