[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在何时使用。
如果能理解上述问题,那么其他任何面试题基本不会有任何问题。
网友评论