Java 7、8中的String.intern(3)
本文由 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设置为同样的值以减少垃圾回收次数
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的代价。
原文链接: java-performance 翻译: ImportNew.com - 文 学敏
译文链接: http://www.importnew.com/12681.html
[ 转载请保留原文出处、译者和译文链接。]
相关文章
- Java 6,7,8中的String.intern —— 多线程访问
- 10个有关String的面试问题
- Java中的String对象是不可变的吗
- 基本类型转String
- Java 1.7.0_06中String类内部实现的一些变化
- 为什么String类是不可变的?
- JVM垃圾回收机制
- Java垃圾回收精粹 — Part1
- Java编程细节之十个最佳实践
- Java中的设计模式