String.intern()的性能损失

时间:2012-05-16 18:10:21

标签: java string performance

很多人都在谈论String.intern()的性能优势,但我实际上对性能损失更感兴趣。

我主要担心的是:

  • 搜索费用:intern()用于确定常量字符串是否存在于常量池中的时间。该成本如何随着该池中的字符串数量而缩放?
  • 同步:显然,整个JVM共享常量池。当从多个线程一遍又一遍地调用intern()时,该池如何表现?它执行了多少锁定?性能如何与争用一起扩展?

我担心所有这些事情,因为我目前正在研究一个财务应用程序,因为重复的字符串会导致使用太多内存。有些字符串基本上看起来像枚举值,并且只能有有限数量的潜在值(例如货币名称(“USD”,“EUR”))存在超过一百万份。在这种情况下,String.intern()看起来很简单,但我担心每次在某处存储货币时调用intern()的同步开销。

除此之外,其他一些类型的字符串可以有数百万个不同的值,但每个字符串仍然有数万个副本(例如ISIN代码)。对于这些,我担心实际上一百万字符串会基本上减慢intern()方法的速度,从而使我的应用程序陷入困境。

5 个答案:

答案 0 :(得分:36)

我自己做了一些基准测试。对于搜索成本部分,我决定将String.intern()与ConcurrentHashMap.putIfAbsent(s,s)进行比较。基本上,这两个方法做同样的事情,除了String.intern()是一个本机方法,它存储和读取直接在JVM中管理的SymbolTable,而ConcurrentHashMap.putIfAbsent()只是一个普通的实例方法。

您可以在github gist上找到基准代码(因为没有更好的位置)。您还可以在源文件顶部的注释中找到我在启动JVM时使用的选项(以验证基准测试没有偏差)。

无论如何这里是结果:

搜索成本(单线程)

<强>图例

  • count :我们尝试汇集的不同字符串的数量
  • 初始实习生:在字符串池中插入所有字符串所用的时间(以毫秒为单位)
  • 查找相同的字符串:使用与先前在池中输入的完全相同的实例,从池中再次查找每个字符串所用的时间(以毫秒为单位)
  • 查找相等字符串:从池中再次查找每个字符串所花费的时间(以毫秒为单位),但使用不同的实例

<强>的String.intern()

count       initial intern   lookup same string  lookup equal string
1'000'000            40206                34698                35000
  400'000             5198                 4481                 4477
  200'000              955                  828                  803
  100'000              234                  215                  220
   80'000              110                   94                   99
   40'000               52                   30                   32
   20'000               20                   10                   13
   10'000                7                    5                    7

<强> ConcurrentHashMap.putIfAbsent()

count       initial intern   lookup same string  lookup equal string
1'000'000              411                  246                  309
  800'000              352                  194                  229
  400'000              162                   95                  114
  200'000               78                   50                   55
  100'000               41                   28                   28
   80'000               31                   23                   22
   40'000               20                   14                   16
   20'000               12                    6                    7
   10'000                9                    5                    3

搜索成本的结论:String.intern()的调用成本高得惊人。它在O(n)中非常严重,其中n是池中字符串的数量。当池中的字符串数量增加时,从池中查找一个字符串的时间会增长得更多(每次查找0.7微秒,10'000个字符串,每次查找40微秒,1'000'000个字符串)。

ConcurrentHashMap按预期进行扩展,池中的字符串数量对查找速度没有影响。

根据这个实验,我强烈建议如果要实习多个字符串,请避免使用String.intern()。

答案 1 :(得分:21)

我最近在Java 6,7和8中写了一篇关于String.intern()实现的文章: String.intern in Java 6, 7 and 8 - string pooling

有一个-XX:StringTableSize JVM参数,它允许你使String.intern在Java7 +中非常有用。所以,不幸的是,我不得不说这个问题目前正在向读者提供误导性的信息。

答案 2 :(得分:5)

我发现使用fastutil哈希表并进行自己的实习而不是重用String.intern()会更好。使用我自己的哈希表意味着我可以自己做出关于并发性的决定,而且我不会竞争PermGen空间。

我这样做是因为我正在处理一个问题,因为它有数百万字符串,许多相同,我想(a)减少占地面积和(b)允许通过身份进行比较。对于我的问题,使用我的 String.intern()方法,实习比使用实际更好。

因人而异。

答案 3 :(得分:0)

String.intern变慢的原因有两个:
1. -XX:StringTableSize限制。
在Java中,它使用内部哈希表来管理字符串缓存,在Java 6中,默认的StringTableSize值为1009,这意味着string.intern为O(字符串对象的数量/ 1009),当创建越来越多的字符串对象时,它变得越来越慢。

  

\ openjdk7 \ hotspot \ src \ share \ vm \ classfile \ symbolTable.cpp

oop StringTable::intern(Handle string_or_null, jchar* name,  
                        int len, TRAPS) {  
  unsigned int hashValue = java_lang_String::hash_string(name, len);  
  int index = the_table()->hash_to_index(hashValue);  
  oop string = the_table()->lookup(index, name, len, hashValue);  
  // Found  
  if (string != NULL) return string;  
  // Otherwise, add to symbol to table  
  return the_table()->basic_add(index, string_or_null, name, len,  
                                hashValue, CHECK_NULL);  
}

2。在Java 6中,字符串缓存池位于perm区域中,而不是在堆中,大多数时候,我们将perm的大小配置为相对较小。

答案 4 :(得分:-1)

以下微基准测试建议使用枚举提供大约十倍的性能改进(通常的微基准警告适用)测试代码如下:

public class Test {
   private enum E {
      E1;
      private static final Map<String, E> named = new HashMap<String, E>();
      static {
         for (E e : E.values()) {
            named.put( e.name(), e );
         }
      }

      private static E get(String s) {
         return named.get( s );
      }
   }

   public static void main(String... strings) {
      E e = E.get( "E1" ); // ensure map is initialised

      long start = System.nanoTime();
      testMap( 10000000 );
      long end = System.nanoTime();

      System.out.println( 1E-9 * (end - start) );
   }

   private static void testIntern(int num) {
      for (int i = 0; i < num; i++) {
         String s = "E1".intern();
      }
   }

   private static void testMap(int num) {
      for (int i = 0; i < num; i++) {
         E e = E.get( "E1" );
      }
   }
}

结果(1000万次迭代): testIntern() - 0.8秒 testMap() - 0.06秒

当然是YMMV,但是枚举提供了比Strings更多的好处...类型安全性超过其他随机字符串,添加方法的能力等似乎是最好的方式去imho