43

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

我主要担心的是:

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

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

最重要的是,一些其他类型的字符串可以有数百万个不同的值,但仍然有数万个副本(例如 ISIN 代码)。对于这些,我担心实习一百万个字符串基本上会减慢 intern() 方法的速度,以至于让我的应用程序陷入困境。

4

5 回答 5

39

我自己做了一些基准测试。对于搜索成本部分,我决定将 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 是池中的字符串数。当池中的字符串数量增加时,从池中查找一个字符串的时间会增加很多(对于 10'000 个字符串,每次查找 0.7 微秒,对于 1'000'000 个字符串,每次查找需要 40 微秒)。

ConcurrentHashMap 按预期缩放,池中的字符串数量对查找速度没有影响。

基于这个实验,我强烈建议避免使用 String.intern() 如果你要实习的字符串不止几个。

于 2012-05-17T01:19:36.767 回答
23

我最近写了一篇关于 Java 6、7 和 8 中 String.intern() 实现的文章:Java 6、7 和 8 中的 String.intern - 字符串池

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

于 2013-08-25T08:10:31.260 回答
5

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

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

YMMV。

于 2012-05-16T18:16:30.633 回答
1

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 size比较小。

于 2019-11-27T12:53:00.827 回答
-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,但枚举比字符串提供了这么多好处……相对于其他随机字符串的类型安全性、添加方法等的能力似乎是最好的方式去恕我直言

于 2014-05-19T11:34:07.813 回答