当前访客身份:游客 [ 登录  | 注册加入尚学堂]
启用新域名sxt.cn
新闻资讯

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

helloworld 发表于 2年前  | 评论(0 )| 阅读次数(555 )|   0 人收藏此文章,   我要收藏

我想再回到之前(第一部分第二部分)讨论过的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设置为同样的值以减少垃圾回收次数

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

    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的代价。

分享到:0
关注微信,跟着我们扩展技术视野。每天推送IT新技术文章,每周聚焦一门新技术。微信二维码如下:
微信公众账号:尚学堂(微信号:bjsxt-java)
声明:博客文章版权属于原创作者,受法律保护。如果侵犯了您的权利,请联系管理员,我们将及时删除!
(邮箱:webmaster#sxt.cn(#换为@))
北京总部地址:北京市海淀区西三旗桥东建材城西路85号神州科技园B座三层尚学堂 咨询电话:400-009-1906 010-56233821
Copyright 2007-2015 北京尚学堂科技有限公司 京ICP备13018289号-1 京公网安备11010802015183