您的位置:首页 > 其它

使用String的intern方法节省内存

2018-05-26 14:52 295 查看
AttilaSzegedis在他讲述JVM知识的文档中一直强调,清楚知道内存中存储的数据量是非常重要的。我一开始感到十分惊讶,因为一般情况下,在企业开发中并不是经常需要关注对象的大小。他对此给出了Twitter的一个例子。

先思考一个内存占用的问题:字符串“HelloWorld”会占用多少字节内存?

答案:在32位虚拟机上是62字节,在64位虚拟机上是86字节。

分别为8/16(字符串的对象头)+11*2(字符)+[8/16(字符数组的对象头)+4(数组长度),加上字节对齐所需的填充,共为16/24字节]+4(偏移)+4(偏移长度)+4(哈希码)+4/8(指向字符数组的引用)【在64位虚拟机上,String对象的内存占用会因为字节对齐而填充为40字节】

假如现在有许多推特消息的地点信息需要存储。

地点信息对应的类也许会像这样实现。

1
2
3
4
5
6
7
classLocation{
Stringcity;
Stringregion;
StringcountryCode;
doublelong;
doublelat;
}
很明显的一点,当加载地点信息时,实际上是加载了许多的字符串,而以Twitter的用户规模,肯定有许多字符串是重复的。按照Attila的说法,即使是32GB大小的堆,也放不下所有数据。现在的问题是:能够通过什么方法来减少内存的占用,从而所有数据都能被加载进内存中?

我们先来看两个解决方案,它们两者是相辅相成的。

Attilas提出的方法

可以看出,在地点类所存储的信息里,总有一部分是重复的,所以可以很简单地以非技术手段解决这个问题。我们可以把地点类拆分成下面的两个类:

1
2
3
4
5
6
7
8
9
10
classSharedLocation{
Stringcity;
Stringregion;
StringcountryCode;
}
classLocation{
SharedLocationsharedLocation;
doublelong;
doublelat;
}
因为很少有城市会改变所在的地区和国家,所以这个简单的方法能够起作用。这些字符串的组合是唯一的。这种方法也很灵活,所以也能够进行处理上面所提唯一性不满足的情况。特别是对于用户输入的地点信息,这点显得更加重要。这样子的话,如果多条Twitter消息是来自同一个地点,例如“Solingen,NRW,DE”(DE指德国,NRW为德国北莱茵邦,Solingen与之后的Ratingen为德国城市名,译者注)的话,也只需要使用一个SharedLocation对象。

但是,其它的信息,如“Ratingen,NRW,DE”,仍然需要在内存中存储额外的3个字符串,而不是单独的一个“Ratingen”。上面的方法可以使内存中的数据总量下降到20GB。

使用Stringintern()方法

但是在不想或者不能够修改数据类的情况下怎么办呢?又或者是Twitter的那些人并没有20GB大小的堆。这种情况下可以使用intern()方法,它能够使内存中的不同字符串都只有一个实例对象。对于intern()方法,存在着许多误解。许多人会问道,intern()方法是不是可以在字符串进行等价比较时,提高效率,毕竟在使用intern时,相等的字符串实际上都是同一个对象。确实如此,intern可以做到这一点。(对于其他的任何对象来说,这个规律也是成立的。)(在进行equals比较时,如果两个对象是同一个的话,在“==”比较时就能得出结果,所以可以提高equals比较的效率,而不管比较的对象是字符串还是其他类型的对象,译者注。)

1
2
3
4
5
6
7
//java.lang.String
publicbooleanequals(ObjectanObject){
if(this==anObject){
returntrue;
}
//...
}
但在等价比较上的性能提升并不是应该使用intern的理由。实际上,intern的目的在于复用字符串对象以节省内存。

在明确知道一个字符串会出现多次时才使用intern(),并且只用它来节省内存。

使用intern()方法的效率,取决于重复的字符串与唯一的字符串的比值。另外,还要看在产生字符串对象的地方,代码是不是容易进行修改。

intern原理

intern()方法需要传入一个字符串对象(已存在于堆上),然后检查StringTable里是不是已经有一个相同的拷贝。StringTable可以看作是一个HashSet,它将字符串分配在永久代上。StringTable存在的唯一目的就是维护所有存活的字符串的一个对象。如果在StringTable里找到了能够找到所传入的字符串对象,那就直接返回它,否则,把它加入StringTable:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//OpenJDK6code
JVM_ENTRY(jstring,JVM_InternString(JNIEnv*env,jstringstr))
JVMWrapper("JVM_InternString");
JvmtiVMObjectAllocEventCollectoroam;
if(str==NULL)returnNULL;
oopstring=JNIHandles::resolve_non_null(str);
oopresult=StringTable::intern(string,CHECK_NULL);
return(jstring)JNIHandles::make_local(env,result);
JVM_END

oopStringTable::intern(Handlestring_or_null,jchar*name,
intlen,TRAPS){
unsignedinthashValue=hash_string(name,len);
intindex=the_table()->hash_to_index(hashValue);
oopstring=the_table()->lookup(index,name,len,hashValue);

//Found
if(string!=NULL)returnstring;

//Otherwise,addtosymboltotable
returnthe_table()->basic_add(index,string_or_null,name,len,
hashValue,CHECK_NULL);
}
因此,相同字符串的对象只会有一个。

intern用法

intern适合用在需要读取数据并将这些对象或者字符串纳入一个更大范围作用域的情况。需要注意的是,硬编码在代码中的字符串(例如常量等等)都会被编译器自动的执行intern操作。

看一个例子:

1
2
3
4
5
6
7
8
Stringcity=resultSet.getString(1);
Stringregion=resultSet.getString(2);
StringcountryCode=resultSet.getString(3);
doublecity=resultSet.getDouble(4);
doublecity=resultSet.getDouble(5);

Locationlocation=newLocation(city.intern(),region.intern(),countryCode.intern(),long,lat);
allLocations.add(location);
所有新创建的地点对象都会使用intern得到的字符串。而从数据库读取到的临时字符串则会被垃圾回收。

如何确定intern的效率

最好的方法是对整个堆执行一次堆转储。堆转储也会在发生OutOfMemoryError时执行。

在MAT(内存分析工具,译者注)中打开转储文件,然后选择java.lang.String,依次点击“JavaBasics”、“GroupByValue”。



根据堆的大小,上面的操作可能耗费比较长的时间。最后可以看到类型这样的结果。按“RetainedHeap”或者是“Objects”列进行排序,可以发现一些有趣的东西:



从这快照中我们可以看到,空的字符串占用了大量的内存!两百万个空字符串对象占用了总共130MB的空间。另外可以看到一部分被加载的JavaScript脚本,一些作为键的字符串,它们被用于定位。另外,还有一些与业务逻辑相关的字符串。

这些与业务逻辑相关的字符串是最容易进行intern操作的,因为我们清楚地知道它们是在什么地方被加载进内存的。对于其他字符串,可以通过“MergeshortestPathtoGCRoot”选项来找到它们被存储的位置,这个信息也许能够帮助我们找到该使用intern的地方。

intern的利弊

既然intern()方法有这些好处,为什么不经常使用呢?原因在于它会降低代码效率。下面给出一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
privatestaticfinalintMAX=40000000;
publicstaticvoidmain(String[]args)throwsException{
longt=System.currentTimeMillis();
String[]arr=newString[MAX];
for(inti=0;i<MAX;i++){
arr[i]=newString(DB_DATA[i%10]);
//and:arr[i]=newString(DB_DATA[i%10]).intern();
}
System.out.println((System.currentTimeMillis()-t)+"ms");
System.gc();
System.out.println(arr[0]);
}
代码中使用了字符串数组来维护到字符串对象的强引用,另外我们还打印了数组的第一个元素来避免数组由于代码优化而将数组给销毁了。接着从数据库加载10个不同的字符串,但在这里我使用了newString()来创建一个临时的字符串,这和从数据库里读是一样的。最后我们调用了系统的GC()方法,这样就能排除其他不相关对象的影响,保证结果的正确。在64位,8G内存,i5-2520M处理器的Windows系统上运行上面的代码,环境为JDK1.6.0_27,指定虚拟机参数-XX:+PrintGCDetails-Xmx6G-Xmn3G记录垃圾回收日志。结果如下:

没有使用intern()方法的结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
1519ms
[GC[PSYoungGen:2359296K->393210K(2752512K)]2359296K->2348002K(4707456K),5.4071058secs][Times:user=8.84sys=1.00,real=5.40secs]
[FullGC(System)[PSYoungGen:393210K->392902K(2752512K)][PSOldGen:1954792K->1954823K(1954944K)]2348002K->2347726K(4707456K)[PSPermGen:2707K->2707K(21248K)],5.3242785secs][Times:user=3.71sys=0.20,real=5.32secs]
DE
Heap
PSYoungGentotal2752512K,used440088K[0x0000000740000000,0x0000000800000000,0x0000000800000000)
edenspace2359296K,18%used[0x0000000740000000,0x000000075adc6360,0x00000007d0000000)
fromspace393216K,0%used[0x00000007d0000000,0x00000007d0000000,0x00000007e8000000)
tospace393216K,0%used[0x00000007e8000000,0x00000007e8000000,0x0000000800000000)
PSOldGentotal1954944K,used1954823K[0x0000000680000000,0x00000006f7520000,0x0000000740000000)
objectspace1954944K,99%used[0x0000000680000000,0x00000006f7501fd8,0x00000006f7520000)
PSPermGentotal21248K,used2724K[0x000000067ae00000,0x000000067c2c0000,0x0000000680000000)
objectspace21248K,12%used[0x000000067ae00000,0x000000067b0a93e0,0x000000067c2c0000)
使用了intern()方法的结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
4838ms
[GC[PSYoungGen:2359296K->156506K(2752512K)]2359296K->156506K(2757888K),0.1962062secs][Times:user=0.69sys=0.01,real=0.20secs]
[FullGC(System)[PSYoungGen:156506K->156357K(2752512K)][PSOldGen:0K->18K(5376K)]156506K->156376K(2757888K)[PSPermGen:2708K->2708K(21248K)],0.2576126secs][Times:user=0.25sys=0.00,real=0.26secs]
DE
Heap
PSYoungGentotal2752512K,used250729K[0x0000000740000000,0x0000000800000000,0x0000000800000000)
edenspace2359296K,10%used[0x0000000740000000,0x000000074f4da6f8,0x00000007d0000000)
fromspace393216K,0%used[0x00000007d0000000,0x00000007d0000000,0x00000007e8000000)
tospace393216K,0%used[0x00000007e8000000,0x00000007e8000000,0x0000000800000000)
PSOldGentotal5376K,used18K[0x0000000680000000,0x0000000680540000,0x0000000740000000)
objectspace5376K,0%used[0x0000000680000000,0x0000000680004b30,0x0000000680540000)
PSPermGentotal21248K,used2725K[0x000000067ae00000,0x000000067c2c0000,0x0000000680000000)
objectspace21248K,12%used[0x000000067ae00000,0x000000067b0a95d0,0x000000067c2c0000)
可以看到结果差别十分的大。在使用intern()方法的时候,程序耗时多了3秒,但节省了很大一块内存。使用intern()方法的程序占用了253472K(250M)内存,而不使用的占用了2397635K(2.4G)。从这些可以看出使用intern的利弊。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: