zoukankan      html  css  js  c++  java
  • 聊聊Java String.intern 背后你不知道的知识

    Java的 String类有个有意思的public方法:

    public String intern()
    返回标准表示的字符串对象。String类维护私有字符串池。
    
    调用此方法时,如果字符串池已经包含等于此字符串对象的字符串(通过equals方法确定),
    则返回池中的字符串。 否则,将此String对象添加到池中,并返回对此String对象的引用。

    这个功能为String提供了字符串池,我们可以使用它来优化内存。 但是,这有一个缺点:在OpenJDK中,String.intern()是本地方法,它实际上调用了JVM的相关方法来实现该功能。这样实现的原因是,当VM和JDK代码必须就特定String对象的标识达成一致时,String interning就必须是JDK-VM接口的一部分。

    这样的实现意味着:

    1. 您需要在每个intern调用使用JDK-JVM接口,这会浪费CPU。
    2. 性能受本地HashTable实现的影响,可能落后于高性能Java版本,特别是在并发访问的情况下。
    3. 由于Java Strings是来自VM的引用,因此它们成为GC rootset的一部分。 在许多情况下,这需要在GC停顿期间执行额外的工作。

    吞吐量实验

    我们可以构建简单的实验来说明问题。 使用HashMap和ConcurrentHashMap实现intern方法,这为我们提供了一个非常好的JMH基准:

     1 @State(Scope.Benchmark)
     2 public class StringIntern {
     3 
     4     @Param({"1", "100", "10000", "1000000"})
     5     private int size;
     6 
     7     private StringInterner str;
     8     private CHMInterner chm;
     9     private HMInterner hm;
    10 
    11     @Setup
    12     public void setup() {
    13         str = new StringInterner();
    14         chm = new CHMInterner();
    15         hm = new HMInterner();
    16     }
    17 
    18     public static class StringInterner {
    19         public String intern(String s) {
    20             return s.intern();
    21         }
    22     }
    23 
    24     @Benchmark
    25     public void intern(Blackhole bh) {
    26         for (int c = 0; c < size; c++) {
    27             bh.consume(str.intern("String" + c));
    28         }
    29     }
    30 
    31     public static class CHMInterner {
    32         private final Map<String, String> map;
    33 
    34         public CHMInterner() {
    35             map = new ConcurrentHashMap<>();
    36         }
    37 
    38         public String intern(String s) {
    39             String exist = map.putIfAbsent(s, s);
    40             return (exist == null) ? s : exist;
    41         }
    42     }
    43 
    44     @Benchmark
    45     public void chm(Blackhole bh) {
    46         for (int c = 0; c < size; c++) {
    47             bh.consume(chm.intern("String" + c));
    48         }
    49     }
    50 
    51     public static class HMInterner {
    52         private final Map<String, String> map;
    53 
    54         public HMInterner() {
    55             map = new HashMap<>();
    56         }
    57 
    58         public String intern(String s) {
    59             String exist = map.putIfAbsent(s, s);
    60             return (exist == null) ? s : exist;
    61         }
    62     }
    63 
    64     @Benchmark
    65     public void hm(Blackhole bh) {
    66         for (int c = 0; c < size; c++) {
    67             bh.consume(hm.intern("String" + c));
    68         }
    69     }
    70 }

    该测试试图在很多字符串上执行intern方法,但实际的intern仅在第一次遍历循环时发生,之后只访问map中的字符串。 size参数用于控制我们intern的字符串数量,从而限制我们正在处理的字符串表大小。 对于intern来说,通常都这样使用。

    使用JDK 8u131运行它:

    Benchmark             (size)  Mode  Cnt       Score       Error  Units
    
    StringIntern.chm           1  avgt   25       0.038 ±     0.001  us/op
    StringIntern.chm         100  avgt   25       4.030 ±     0.013  us/op
    StringIntern.chm       10000  avgt   25     516.483 ±     3.638  us/op
    StringIntern.chm     1000000  avgt   25   93588.623 ±  4838.265  us/op
    
    StringIntern.hm            1  avgt   25       0.028 ±     0.001  us/op
    StringIntern.hm          100  avgt   25       2.982 ±     0.073  us/op
    StringIntern.hm        10000  avgt   25     422.782 ±     1.960  us/op
    StringIntern.hm      1000000  avgt   25   81194.779 ±  4905.934  us/op
    
    StringIntern.intern        1  avgt   25       0.089 ±     0.001  us/op
    StringIntern.intern      100  avgt   25       9.324 ±     0.096  us/op
    StringIntern.intern    10000  avgt   25    1196.700 ±   141.915  us/op
    StringIntern.intern  1000000  avgt   25  650243.474 ± 36680.057  us/op

    可以看出 String.intern()明显更慢。慢的原因在于本地实现,这在perf record -g中清晰可见:

    -    6.63%     0.00%  java     [unknown]           [k] 0x00000006f8000041
       - 0x6f8000041
          - 6.41% 0x7faedd1ee354
             - 6.41% 0x7faedd170426
                - JVM_InternString
                   - 5.82% StringTable::intern
                      - 4.85% StringTable::intern
                           0.39% java_lang_String::equals
                           0.19% Monitor::lock
                         + 0.00% StringTable::basic_add
                      - 0.97% java_lang_String::as_unicode_string
                           resource_allocate_bytes
                     0.19% JNIHandleBlock::allocate_handle
                     0.19% JNIHandles::make_local

    虽然JNI转换成本相当高,但似乎在StringTable实现上也花了相当多的时间。 使用 -XX:+PrintStringTableStatistics,将输出如下内容:

    StringTable statistics:
    Number of buckets       :     60013 =    480104 bytes, avg   8.000
    Number of entries       :   1002714 =  24065136 bytes, avg  24.000
    Number of literals      :   1002714 =  64192616 bytes, avg  64.019
    Total footprint         :           =  88737856 bytes
    Average bucket size     :    16.708  ; <---- !!!!!!

    注意最后一行,平均每个bucket 16个元素表示已经过载。 更糟糕的是,字符串表不可调整大小(虽然有实验工作使它们可以调整大小,但是因为“其他原因”而被移除)。 通过设置更大的-XX:StringTableSize可能会减轻该问题:

    Benchmark             (size)  Mode  Cnt       Score       Error  Units
    
    # Default, copied from above
    StringIntern.chm           1  avgt   25       0.038 ±     0.001  us/op
    StringIntern.chm         100  avgt   25       4.030 ±     0.013  us/op
    StringIntern.chm       10000  avgt   25     516.483 ±     3.638  us/op
    StringIntern.chm     1000000  avgt   25   93588.623 ±  4838.265  us/op
    
    # Default, copied from above
    StringIntern.intern        1  avgt   25       0.089 ±     0.001  us/op
    StringIntern.intern      100  avgt   25       9.324 ±     0.096  us/op
    StringIntern.intern    10000  avgt   25    1196.700 ±   141.915  us/op
    StringIntern.intern  1000000  avgt   25  650243.474 ± 36680.057  us/op
    
    # StringTableSize = 10M
    StringIntern.intern        1  avgt    5       0.097 ±     0.041  us/op
    StringIntern.intern      100  avgt    5      10.174 ±     5.026  us/op
    StringIntern.intern    10000  avgt    5    1152.387 ±   558.044  us/op
    StringIntern.intern  1000000  avgt    5  130862.190 ± 61200.783  us/op

    然而这只能暂时缓解问题,因为你必须提前做好规划。 如果盲目地将String表大小设置为较大值,并且不使用它,则会浪费内存。 即使您使用很大的StringTable,JNI本地调用仍然会消耗CPU。

    GC停顿实验

    本地字符串表最大问题在于它是GC root的一部分。也就是说,它应该需要垃圾收集器进行特殊扫描/更新。 在OpenJDK中,这意味着在暂停期间额外工作。 实际上,对于Shenandoah(译者注:对于ZGC也如此),暂停主要依赖于GC root set大小,在String表中存在1M记录会导致以下结果:

    $ ... StringIntern -p size=1000000 --jvmArgs "-XX:+UseShenandoahGC -Xlog:gc+stats -Xmx1g -Xms1g"
    ...
    Initial Mark Pauses (G)    = 0.03 s (a = 15667 us) (n = 2) (lvls, us = 15039, 15039, 15039, 15039, 16260)
    Initial Mark Pauses (N)    = 0.03 s (a = 15516 us) (n = 2) (lvls, us = 14844, 14844, 14844, 14844, 16088)
      Scan Roots               = 0.03 s (a = 15448 us) (n = 2) (lvls, us = 14844, 14844, 14844, 14844, 16018)
        S: Thread Roots        = 0.00 s (a =    64 us) (n = 2) (lvls, us =    41,    41,    41,    41,    87)
        S: String Table Roots  = 0.03 s (a = 13210 us) (n = 2) (lvls, us = 12695, 12695, 12695, 12695, 13544)
        S: Universe Roots      = 0.00 s (a =     2 us) (n = 2) (lvls, us =     2,     2,     2,     2,     2)
        S: JNI Roots           = 0.00 s (a =     3 us) (n = 2) (lvls, us =     2,     2,     2,     2,     4)
        S: JNI Weak Roots      = 0.00 s (a =    35 us) (n = 2) (lvls, us =    29,    29,    29,    29,    42)
        S: Synchronizer Roots  = 0.00 s (a =     0 us) (n = 2) (lvls, us =     0,     0,     0,     0,     0)
        S: Flat Profiler Roots = 0.00 s (a =     0 us) (n = 2) (lvls, us =     0,     0,     0,     0,     0)
        S: Management Roots    = 0.00 s (a =     1 us) (n = 2) (lvls, us =     1,     1,     1,     1,     1)
        S: System Dict Roots   = 0.00 s (a =     9 us) (n = 2) (lvls, us =     8,     8,     8,     8,    11)
        S: CLDG Roots          = 0.00 s (a =    75 us) (n = 2) (lvls, us =    68,    68,    68,    68,    81)
        S: JVMTI Roots         = 0.00 s (a =     0 us) (n = 2) (lvls, us =     0,     0,     0,     0,     1)

    因为我们在root set中添加了内容,每次暂停会增加13ms。

    某些GC实现仅在完成重要操作时执行String表清理。 比如,如果不进行卸载类,从JVM角度来看清理String表是没有意义的(因为加载的类是intern字符串的主要来源)。 因此,此工作负载在G1和CMS中会也会表现出有趣的行为:

    public class InternMuch {
      public static void main(String... args) {
        for (int c = 0; c < 1_000_000_000; c++) {
          String s = "" + c + "root";
          s.intern();
        }
      }
    }

    用CMS跑一遍:

    $ java -XX:+UseConcMarkSweepGC -Xmx2g -Xms2g -verbose:gc -XX:StringTableSize=6661443 InternMuch
    
    GC(7) Pause Young (Allocation Failure) 349M->349M(989M) 357.485ms
    GC(8) Pause Initial Mark 354M->354M(989M) 3.605ms
    GC(8) Concurrent Mark
    GC(8) Concurrent Mark 1.711ms
    GC(8) Concurrent Preclean
    GC(8) Concurrent Preclean 0.523ms
    GC(8) Concurrent Abortable Preclean
    GC(8) Concurrent Abortable Preclean 935.176ms
    GC(8) Pause Remark 512M->512M(989M) 512.290ms
    GC(8) Concurrent Sweep
    GC(8) Concurrent Sweep 310.167ms
    GC(8) Concurrent Reset
    GC(8) Concurrent Reset 0.404ms
    GC(9) Pause Young (Allocation Failure) 349M->349M(989M) 369.925ms

    看起来结果还可以。 遍历重载的字符串表需要一段时间。 蛋疼的事情会在使用-XX:-ClassUnloading禁用类卸载后发生。你猜猜接下来会发生什么:

    $ java -XX:+UseConcMarkSweepGC -Xmx2g -Xms2g -verbose:gc -XX:-ClassUnloading -XX:StringTableSize=6661443 InternMuch
    
    GC(11) Pause Young (Allocation Failure) 273M->308M(989M) 338.999ms
    GC(12) Pause Initial Mark 314M->314M(989M) 66.586ms
    GC(12) Concurrent Mark
    GC(12) Concurrent Mark 175.625ms
    GC(12) Concurrent Preclean
    GC(12) Concurrent Preclean 0.539ms
    GC(12) Concurrent Abortable Preclean
    GC(12) Concurrent Abortable Preclean 2549.523ms
    GC(12) Pause Remark 696M->696M(989M) 133.920ms
    GC(12) Concurrent Sweep
    GC(12) Concurrent Sweep 175.949ms
    GC(12) Concurrent Reset
    GC(12) Concurrent Reset 0.463ms
    GC(14) Pause Full (Allocation Failure) 859M->0M(989M) 1541.465ms  <---- !!!
    GC(13) Pause Young (Allocation Failure) 859M->0M(989M) 1541.515ms

    FULL GC! 对于CMS,假设用户会调用System.gc(),使用ExplicitGCInvokesConcurrentAndUnloadsClasses会缓解这一情况。

    意见

    在假设改进内存占用空间或低级==优化的情况下,我们讨论了实现intern的方法。

    对于OpenJDK,String.intern()是本机JVM字符串表的代理,使用它需要注意:吞吐量,内存占用,暂停时间等问题。 很容易低估这些问题的影响。 手动控制的intern工作更加可靠,因为它们在Java端工作,只是普通Java对象,通常更容易调整大小,并且在不再需要时也可以完全丢弃。 GC辅助字符串去重复数据()确实可以减少很多问题。

    几乎在在我们进行每个项目中,从热路径中删除String.intern(),或者用手动方式替代它,都有很大的性能提升。 不要无脑使用String.intern(),好吗?

  • 相关阅读:
    protobufer安装
    flume sink processor
    flume 参考
    uniq 和 sort -u的不同
    XtraInputBox-How to set the XtraInputBox as password input?
    原型模式
    C#-IEditableObject Interface
    gridControl-The BindingSource.AddingNew event does not fire until I begin typing into the new row
    C#中Invoke 和 BeginInvoke 的区别
    C#启动时全屏显示窗体...
  • 原文地址:https://www.cnblogs.com/helloworld2048/p/java_string_intern.html
Copyright © 2011-2022 走看看