String及StringTable(二):java中的StringTable

时间:2022-07-23
本文章向大家介绍String及StringTable(二):java中的StringTable,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

文章目录
  • 1.什么是StringTable
  • 2.java中字符串拼接的秘密
  • 3.intern()方法与StringTable调优
    • 3.1 intern方法结束
    • 3.2 Stringtable参数调优
  • 4.String去重XX:+UseStringDeduplication
  • 5.若干有些坑的面试题
    • 5.1 不同方法创建String的内存模型

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操作。

实际上这个加法操作,被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。

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

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

3.intern()方法与StringTable调优

3.1 intern方法结束

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

String a = "123";

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

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

String a = new String("123");

此过程不会与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”。这个代码我们可以反编译再看看:

可以看到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

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数组进行复用。 优化的结果如下图:

优化后:

我们可以看到在优化之后,相同的字符串都指向了相同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的内存模型:

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