美文网首页Java
String及StringTable(二):java中的Stri

String及StringTable(二):java中的Stri

作者: 冬天里的懒喵 | 来源:发表于2020-08-11 20:08 被阅读0次

    [toc]

    1.什么是StringTable

    在前面部门已经涉及到了对StringTable的一些基本使用。但是或许很多人还并不知道什么是StringTable。StringTable也可称为StringPool,是jvm在1.7之后,在堆内存中分配的一块区域,用于存放常用的字符串。这点与IntegerCace类似,实际上在java中,存在很多这样的常量池。其目的只有一个,就是为了复用,节约内存。
    StringTable实际上是一个固定大小的HashTable。因此被称为StringTable。其默认大小为60013。这个值是可以设置的,可以通过-XX: StringTableSize 设置这个值的大小。而最早在jdk1.6的时候这个值是固定的为1009。而在jdk1.8中1009是可设置的最小值。
    实际上,这个值的变化,也可以从中看出,java应用不断大型化的过程。包括垃圾回收器,也是从CMS演化到G1,这些都是为了支持在更多的内存中进行更加复杂的业务支撑。
    StringTable的长度不能像HashMap那样动态扩容。因此,如果hash冲突,那么它只能采取拉链法来解决。这就类似于一个不能扩容的1.7版本中的HashMap。那么这样带来的坏处就是,随着链表长度的增加,StringTable中检索的时间复杂度会增加。这样会造成其性能急剧下降。
    虽然在1.8版本中默认长度为60013,但是如果某些特殊应用造程StringTable中链表的长度不断增加的话,势必会影响性能。
    StringTable我们可以通过-XX:+PrintStringTableStatistics进行查看,这个参数会将StringTable和SymbolTable在程序执行完之后都进行print。输出如下:

    SymbolTable statistics:
    Number of buckets       :     20011 =    160088 bytes, avg   8.000
    Number of entries       :     19108 =    458592 bytes, avg  24.000
    Number of literals      :     19108 =    816856 bytes, avg  42.749
    Total footprint         :           =   1435536 bytes
    Average bucket size     :     0.955
    Variance of bucket size :     0.955
    Std. dev. of bucket size:     0.977
    Maximum bucket size     :         7
    StringTable statistics:
    Number of buckets       :     60013 =    480104 bytes, avg   8.000
    Number of entries       :  10003541 = 240084984 bytes, avg  24.000
    Number of literals      :  10003541 = 560281272 bytes, avg  56.008
    Total footprint         :           = 800846360 bytes
    Average bucket size     :   166.690
    Variance of bucket size :    67.388
    Std. dev. of bucket size:     8.209
    Maximum bucket size     :       197
    

    前面学过String类的源码,实际上StringTable中存的是对String对象的引用,其内部的final char [] 数组,还是得在堆上分配单独的空间。

    2.java中字符串拼接的秘密

    在前面说过,String是一个immutable的对象。作为一个不可变的对象,我们在实际操作的过程中还能通过+操作。如下:

    String a = "1" + "2";
    

    我们看看这个过程究竟在编译过程中如何进行的。
    代码如下:

    public class StringAddTest {
    
        public static void main(String[] args) {
            String a = "1";
            String b = "2";
            String c = a + b;
        }
    }
    
    

    编译之后,执行 javap -c -l StringAddTest,输出:

    Compiled from "StringAddTest.java"
    public class com.dhb.Immulate.test.StringAddTest {
      public com.dhb.Immulate.test.StringAddTest();
        Code:
           0: aload_0
           1: invokespecial #1                  // Method java/lang/Object."<init>":()V
           4: return
        LineNumberTable:
          line 3: 0
        LocalVariableTable:
          Start  Length  Slot  Name   Signature
              0       5     0  this   Lcom/dhb/Immulate/test/StringAddTest;
    
      public static void main(java.lang.String[]);
        Code:
           0: ldc           #2                  // String 1
           2: astore_1
           3: ldc           #3                  // String 2
           5: astore_2
           6: new           #4                  // class java/lang/StringBuilder
           9: dup
          10: invokespecial #5                  // Method java/lang/StringBuilder."<init>":()V
          13: aload_1
          14: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
          17: aload_2
          18: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
          21: invokevirtual #7                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
          24: astore_3
          25: return
        LineNumberTable:
          line 6: 0
          line 7: 3
          line 8: 6
          line 9: 25
        LocalVariableTable:
          Start  Length  Slot  Name   Signature
              0      26     0  args   [Ljava/lang/String;
              3      23     1     a   Ljava/lang/String;
              6      20     2     b   Ljava/lang/String;
             25       1     3     c   Ljava/lang/String;
    }
    

    可以看到,jvm实际在编译之后,变成了StringBuilder操作。


    image.png

    实际上这个加法操作,被jvm编译的时候给优化为了StringBuild的append操作。上述5、6、7实际上如下:

    StringBuilder.append(a).append(b).toString();
    

    也就是说,字符串的加法,实际上在jvm编译的过程中直接优化变成了StringBuilder操作。这也是一个在面试过程中经常喜欢被问道的地方。
    但是需要注意的是,虽然字符串加法拼接是append方法,但是在某些情况下,jvm编译器并没有这么只能,比如如下两种:

    String a = "1";
    String b = "2";
    String c = "3";
    String d = a + b + c;
    

    另外一种:

    String d = "";
    for(int i=0;i< 100; i++) {
        d += i;
    }
    

    这两种的字节码就会处理得不一样,第一种会new一个StringBuilder之后append。


    image.png

    第二种则是在for循环内部每次for循环就会创建一个新的StringBuilder。


    image.png
    尽管jvm已经优化到同一行代码中的+可以用一个StringBuilder对象,但是比较复杂的代码还是会导致效率降低。因此如果对于比较复杂的字符串拼接,我们还是应该使用StringBuilder。

    3.intern()方法与StringTable调优

    3.1 intern方法结束

    通过前面的章节我们可以知道,代码中的字面量本身是会被存入到StringTable中去的。
    如下:

    String a = "123";
    

    实际上上述过程在jvm中如下图:


    image.png

    在Stack区中建立了String类型的变量a,其引用指向了StringTables中的字面量对象“123”,在前面我们学过String源码,可以知道String实际上内部是一个char数组,在StringTable中的对象也是通过指针指向了堆中的数组。
    如果我们采用new String的方式,则是如下:

    String a = new String("123");
    
    image.png

    此过程不会与StringTable有任何关系,直接会在Heap区创建这个对象。
    那么对于这种类型的String,如通过StringBuffer、StringBuilder等操作都是等价于重新new了一个String。
    如下:

    String m = "1";
    String n = "2";
    String k = m + n;
    String l = "1" + "2";
    System.out.println(k == "12");
    System.out.println(l == "12");
    

    上述输出结果为:

    false
    true
    

    这说明,k = m + n,前面了解了加法会在jvm中通过jvm优化为StringBuilder,而后面的直接字面量相加的话,jvm编译过程中直接将该值在编译过程中就改成了“12”。这个代码我们可以反编译再看看:


    image.png

    可以看到l直接变成了“12”。
    那么对于我们日常经过加法操作的字符串,怎样才能进入StringTable呢,String类中提供了一个native的intern方法。这个方法通过c语言实现,将变量添加到Stringtable之后,再将引用返回:

    String m = "1";
    String n = "2";
    String k = m + n;
    System.out.println(k.intern() == "12");
    

    上述代码则能够返回true。

    3.2 Stringtable参数调优

    那么对于StringTable,实际上如果一个程序中的StringTable过大,将会导致不少问题:

    //-XX:+PrintStringTableStatistics
    //-XX:StringTableSize=10000000
    public static void main(String[] args) {
        int size = 10000000;
        long start = System.currentTimeMillis();
        List<String> list = IntStream.rangeClosed(1,size)
                .mapToObj(i -> String.valueOf(i).intern())
                .collect(Collectors.toList());
        long end = System.currentTimeMillis();
        System.out.println(" size:{"+size+"} cost:{"+(end-start)+"}");
    
    }
    

    上述代码,我们首先设置-XX:+PrintStringTableStatistics对StringTable进行统计。在StringTable默认大小的情况下:

     size:{10000000} cost:{37626}
    SymbolTable statistics:
    Number of buckets       :     20011 =    160088 bytes, avg   8.000
    Number of entries       :     19108 =    458592 bytes, avg  24.000
    Number of literals      :     19108 =    816856 bytes, avg  42.749
    Total footprint         :           =   1435536 bytes
    Average bucket size     :     0.955
    Variance of bucket size :     0.955
    Std. dev. of bucket size:     0.977
    Maximum bucket size     :         7
    StringTable statistics:
    Number of buckets       :     60013 =    480104 bytes, avg   8.000
    Number of entries       :  10003449 = 240082776 bytes, avg  24.000
    Number of literals      :  10003449 = 560204832 bytes, avg  56.001
    Total footprint         :           = 800767712 bytes
    Average bucket size     :   166.688
    Variance of bucket size :    55.370
    Std. dev. of bucket size:     7.441
    Maximum bucket size     :       196
    

    总耗时为39秒多,StringTable中的每个bucket的平均长度是166。最长的部分达到了196。这样就会导致对StringTable检索会慢。我们现在对Stringtable进行优化,调整StringTableSize=10000000


    image.png
    size:{10000000} cost:{6831}
    SymbolTable statistics:
    Number of buckets       :     20011 =    160088 bytes, avg   8.000
    Number of entries       :     19108 =    458592 bytes, avg  24.000
    Number of literals      :     19108 =    816856 bytes, avg  42.749
    Total footprint         :           =   1435536 bytes
    Average bucket size     :     0.955
    Variance of bucket size :     0.955
    Std. dev. of bucket size:     0.977
    Maximum bucket size     :         7
    StringTable statistics:
    Number of buckets       :  10000000 =  80000000 bytes, avg   8.000
    Number of entries       :  10003450 = 240082800 bytes, avg  24.000
    Number of literals      :  10003450 = 560204928 bytes, avg  56.001
    Total footprint         :           = 880287728 bytes
    Average bucket size     :     1.000
    Variance of bucket size :     1.584
    Std. dev. of bucket size:     1.259
    Maximum bucket size     :         9
    

    这次只有不到7秒就执行完成。此时bucket的平均size大小为1。最大为9。这就说明,intern在使用过程中要慎重。

    4.String去重XX:+UseStringDeduplication

    通过前面的代码可以知道,StringTable可以对String对象进行复用。但是如果采用+或者其他方法,String则会单独在堆区存在。因此StringTable的复用实际上是非常有限的。根据JVM官方的统计:

    • 1.java应用内存里面的字符串占比大概是25%。
    • 2.java应用内存里面的字符串有13.5%是重复的。
    • 3.字符串的平均长度是45。
      实际上jvm内存中存在大量的重复字符串。如下代码:
    Stirng m = "12";
    String n = "3";
    String a = "123";
    String b = new String("123");
    String c = m + n;
    

    那么这样将会导致字符串“123”在jvm堆中重复三次,一次出现在Stringtable,另外两次出现在堆中。而char数组会重复两次。因为new String方法实际上会与字面量公用char数组。只要我们不是采用final static来修饰这些变量,那么这种情况将一直存在。
    jvm的研发团队,对此在G1垃圾回收器上进行了优化,可以采用-XX:+UseStringDeduplication参数将上述情况的最终字符串的char数组进行复用。
    优化的结果如下图:


    image.png

    优化后:


    image.png

    我们可以看到在优化之后,相同的字符串都指向了相同char数组。这样可以很好的节省内存空间。但是我们需要注意如下问题:

    • 1.XX:+UseStringDeduplication只能在G1垃圾收集器上才能生效。
    • 2.只适用于长期存在的对象。它不会对短期存活的对象做去重。如果字符串的生命周期很短,很可能还没来记得做去重就已经死亡了。可以参考DISAPPOINTING STORY ON MEMORY OPTIMIZATION一文。
    • 3.可以与-XX:StringDeduplicationAgeThreshold搭配使用,这个参数可以控制只有经过所指定的GC次数之后才能被去重。
    -XX:StringDeduplicationAgeThreshold=6
    
    • 4.会导致GC的开销时间增加。这是一定的。
    • 5.可以通过-XX:+PrintStringDeduplicationStatistics查看去重信息。
    • 6.jdk需要大于JDK 8 update 20版本。
      可见如果在jdk1.8 update20之后的版本,采用G1垃圾回收期,之后开启字符串去重还是很有用的。

    5.若干有些坑的面试题

    在了解了上述问题之后,那么如下问题就会很容易解决。

    5.1 不同方法创建String的内存模型

    有如下代码:

    Stirng m = "12";
    String n = "3";
    String a = "123";
    String b = new String("123");
    String c = m + n;
    String d = new String("123".toCharArray());
    

    请介绍此时a、b、c、d的内存模型:


    image.png

    此题是本文章第一部分和第二部分的综合,包括String构造方法中new String(String a)和new String(char[] a)的区别。另外还整合了StringTable在何时使用。
    如果能理解上述问题,那么其他任何面试题基本不会有任何问题。

    相关文章

      网友评论

        本文标题:String及StringTable(二):java中的Stri

        本文链接:https://www.haomeiwen.com/subject/sjaydktx.html