Java 7、8中的String.intern(3)

来源:互联网 发布:实时监控软件 编辑:程序博客网 时间:2024/04/28 15:52
本文由 ImportNew - 文 学敏 翻译自 java-performance。欢迎加入Java小组。转载请参见文章末尾的要求。

我想再回到之前(第一部分、第二部分)讨论过的String.intern方法。过去的几个月,我在自己的业余项目中大量使用intern方法,主要是为了研究为每个非暂存String对象使用String.intern方法的利弊(非暂存是指对象的生存期能达到数秒以上,而且很有可能进入老年代回收区)。

我之前也提到过,Java 7、8中String.intern的优点是:

    执行非常快,在多线程模式中(仍然使用全局字符串池)几乎没有性能损失
    节省内存,允许你的数据集更小,(通常会)让你的程序运行更快

这个方法的主要缺点是(之前也提过):

    需要提前设置JVM的-XX:StringTableSize=N参数,字符串池使用这个固定的值(要扩展JVM的字符串池,需要重启虚拟机)
    在整个程序的很多地方需要加入String.intern的调用(可能通过你自己的封装去调用)——这增加了代码的维护代价

经过几个月在我项目中使用String.intern,我觉得这个方法应该用在只有有限值的域上(比如人名、州/省名)。我们不应该在一些很可能不会重复使用的对象上使用intern方法——这会浪费CPU时间。

举例来说,假设你正在给政府写一个个人资料管理工具(与社交网络注册信息比较而言,你会有很多非空的域)。

如果你不得不在内存中保存所有的数据,那么使用intern是很有意义的:

    人的名字 – 即使在多民族国家,比如澳大利亚,多数民族(人口占多数的民族)的数量很少。这使得在用的人名总数在几千以下,而常用的名字甚至少于1000。
    人的姓氏 – 在中国重复性大,其他国家就不太好,但重复的概率已经足够好了。
    公寓号 – 在大部分国家,公寓号可能包含字母,但通常是从1递增的数字,也就是说只有有限数目的数字。
    街道名(去掉街道类型,比如‘road’/’avenue’/’street’) – 它们的数量很少
    州/地区/省 – 只有一些

另一方面,如果你没法将所有数据分割为小块,那最好不要使用intern。举例来说,街道地址的完整名称,像“100 King st”,要比分隔开的“100”或者“King”更唯一。

我们在JDK中的HashMap中分别添加字符串和使用intern的字符串,并对二者做比较。这或多或少地可以显示出将intern作用于唯一性的字符串会产生更多代价。我将使用我的工作站来测试,CPU型号为Intel Xeon E5-2650(8核16线程,2GHz),128G内存,并把-Xmx和-Xms设置为同样的值以减少垃圾回收次数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
    
private static void testInsertVsIntern()
{
    //in order to compile these methods
    testMapInsertion( 100 * 1000 );
    testMapInsertionIntern( 100 * 1000 );
    System.gc();
 
    System.out.println( "Now real run" );
 
    testMapInsertion( 50 * 1000 * 1000 + 100 );
    System.gc();
    testMapInsertionIntern( 50 * 1000 * 1000 + 100 );
}
 
private static void testMapInsertion( final int cnt )
{
    final Map<Integer, String> map = new HashMap<Integer, String>( cnt );
    long start = System.currentTimeMillis();
    for ( int i = 0; i < cnt; ++i )
    {
        final String str = Integer.toString( i );
        map.put( i, str );
        if ( i % 1000000 == 0 ) //1M
        {
            System.out.println( i + "; time (insert) = " + ( System.currentTimeMillis() - start ) / 1000.0 + " sec" );
            start = System.currentTimeMillis();
        }
    }
    System.out.println( "Total length = " + map.size() );
}
 
private static void testMapInsertionIntern( final int cnt )
{
    final Map<Integer, String> map = new HashMap<Integer, String>( cnt );
    long start = System.currentTimeMillis();
    for ( int i = 0; i < cnt; ++i )
    {
        final String str = Integer.toString( i );
        map.put( i, str.intern() ); //here is the difference!
        if ( i % 1000000 == 0 ) //1M
        {
            System.out.println( i + "; time (intern) = " + ( System.currentTimeMillis() - start ) / 1000.0 + " sec" );
            start = System.currentTimeMillis();
        }
    }
    System.out.println( "Total length = " + map.size() );
}

如你所见,两个测试方法的唯一区别是testMapInsertionIntern方法调用了String.intern()。两个方法其他部分都一样。

第一个测试只是往map中添加Integer、String键值对。整个测试用了0.065-0.07秒添加了100,0000个键值对(这个时间也包括整型到字符串的转化),也就是说插入速度稳定在16M键值对每秒。

我使用-XX:StringTableSize=1000003设置了虚拟机的字符串池。我得到了以下结果(测试中只有一次minor gc):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
    
1000000; time (intern) = 0.231 sec
2000000; time (intern) = 0.251 sec
3000000; time (intern) = 0.268 sec
4000000; time (intern) = 0.285 sec
5000000; time (intern) = 0.311 sec
6000000; time (intern) = 0.333 sec
7000000; time (intern) = 0.369 sec
8000000; time (intern) = 0.399 sec
9000000; time (intern) = 0.444 sec
10000000; time (intern) = 0.507 sec
11000000; time (intern) = 0.532 sec
12000000; time (intern) = 0.614 sec
13000000; time (intern) = 0.686 sec
14000000; time (intern) = 0.797 sec
15000000; time (intern) = 0.837 sec
16000000; time (intern) = 0.902 sec
17000000; time (intern) = 0.962 sec
18000000; time (intern) = 1.019 sec
19000000; time (intern) = 1.083 sec
20000000; time (intern) = 1.121 sec
21000000; time (intern) = 1.204 sec
22000000; time (intern) = 1.226 sec
23000000; time (intern) = 1.292 sec
24000000; time (intern) = 1.312 sec
25000000; time (intern) = 1.379 sec
26000000; time (intern) = 1.444 sec
27000000; time (intern) = 1.491 sec
28000000; time (intern) = 1.542 sec
29000000; time (intern) = 1.569 sec
30000000; time (intern) = 1.732 sec
31000000; time (intern) = 1.74 sec
32000000; time (intern) = 1.735 sec
33000000; time (intern) = 1.842 sec
34000000; time (intern) = 1.893 sec
35000000; time (intern) = 1.989 sec
36000000; time (intern) = 1.971 sec
37000000; time (intern) = 2.033 sec
38000000; time (intern) = 2.139 sec
[GC 4195274K->4207538K(16078208K), 5.2907230 secs]
39000000; time (intern) = 7.46 sec
40000000; time (intern) = 2.259 sec
41000000; time (intern) = 2.28 sec
42000000; time (intern) = 2.346 sec
43000000; time (intern) = 2.394 sec
44000000; time (intern) = 2.414 sec
45000000; time (intern) = 2.492 sec
46000000; time (intern) = 2.536 sec
47000000; time (intern) = 2.619 sec
48000000; time (intern) = 2.654 sec
49000000; time (intern) = 2.673 sec
50000000; time (intern) = 2.775 sec

可以看到,处理最开始的100M的字符串所用时间(是不使用intern)的3.5倍,接下来处理的字符串使用的时间更多。回到前边人名、地址的例子,就意味着处理完整的街道名将花费3.5到4倍的时间,而没有其他好处(大部分这样的街道名是唯一的)。
相关文章

String.intern in Java 6, 7 and 8 – string pooling文章描述了Java 7、8中String.intern()的实现与使用的益处。

String.intern in Java 6, 7 and 8 – multithreaded access 文章描述了在多线程中使用Sring.intern()的性能特点。
总结

尽管在Java 7以上对String.intern()做了很细致的优化,但它耗费的时间仍是很显著的(尤其对CPU密集型程序)。文章中的简单例子中,没有调用String.intern()的测试要快3.5倍左右。为稳定起见,你最好不要在每个存活期长的字符串使用String.intern()方法。然而可以使用intern处理只有有限值的域(比如州/省)- 这种情形下节省的内存可以抵消初始CPU的代价。


文章转载自:http://www.importnew.com/12681.html


原文链接: java-performance 翻译: ImportNew.com- 文 学敏
译文链接: http://www.importnew.com/12681.html
[ 转载请保留原文出处、译者和译文链接。]

0 0
原创粉丝点击