StringTable

基本特性

  • String:字符串使用一堆“”引起来表示
  • String声明为final的,不可被继承
  • String实现了Serializable接口:表示字符串时可支持序列化的。实现了Compareble接口:表示String可以比较大小
  • String在jdk及以前内部定义了final char[] value用于存储字符串。jdk9时改为byte[]

字符串常量池中是不会存储相同内容的字符串的

  • String的String Pool是固定大小的Hashtable,默认值大小长度是1009。如果放进String Pool的String非常多,就会造成Hash冲突,从而导致链表会很长,而链表长了后会直接造成的影响就是当调用String.intern时性能会大幅下降
  • 使用-XX:StringTableSize可设置StringTable的长度
  • 在jdk6中StringTable是固定的,就是1009的长度,所以如果常量池中的字符串过多就会导致效率下降很快。StringTableSize设置没有要求
  • 在jdk7中,StringTable的长度默认值是60013,1009是可设置的最小值

字符串拼接操作

  • 常量与常量的拼接结果是在常量池,原理是编译期优化

    ldc:将常量值(如字符串、数字、类引用等)加载到操作数栈上。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    String str1="a"+"b"+"c";
    String str2="abc";
    System.out.println(s2==s1); //true
    /*在生成字节码文件时
    0 ldc #7 <abc>
    2 astore_1
    3 ldc #7 <abc>
    5 astore_2
    */
    String str3="a";
    String str4="bc";
    String str5=str3+"bc";
    str5!=str2;
    //如果拼接字符的前后出现了变量,则相当于堆空间中new String(),具体的内容为拼接的结果
    /*
    6 ldc #9 <a>
    8 astore_3
    9 ldc #11 <bc>
    11 astore 4
    13 aload_3
    14 invokedynamic #13 <makeConcatWithConstants, BootstrapMethods #0>
    19 astore 5
    */
    str3=str3+str4;
    /*
    1.StringBuilder s=new StringBuilder();
    2.s.append("a");
    3.s.append("b");
    4.s.toString()-->类似于 new String("abc");
    */

    str6=str1.intern();
    str6=str1;

    str="ab";
    final String str7="a";
    final String str8="b";
    str9=str7+str8;
    str9=str;
  • 常量池中不会存在相同内容的常量

  • 只要其中有一个是变量,结果就在堆中。变量拼接的原理是StringBuilder

  • 如果拼接的结果调用intern()方法,则主动将常量池中还没有的字符串对象放入池中,并返回此对象地址

intern()方法

有的话就不管,没有的话创建引用指向new String() ,让引用指向常量池的字符串

如何保证变量s指向的是字符串常量池中的数据呢?

  1. String s=”wereash”;

  2. String s=new String(“wereash”).intern()

    String s=new StringBuilder(“wereash”).toString.intern();

new String(“wereash”)会创建几个对象?

  1. 一个对象是:new关键字在堆空间创建的
  2. 另一个对象是:字符串常量池中的对象。通过字节码指令:ldc可知
1
2
3
25 ldc #19 <wereash>
27 invokespecial #21 <java/lang/String.<init> : (Ljava/lang/String;)V>
30 astore 6

new String(“a”)+new String(“b”)呢?

1
2
3
4
5
6
7
8
9
10
11
/*
对象1: new StringBuilder()
对象2: new String("a")
对象3: 常量池中的"a"
对象4: new String("b")
对象5: 常量池中的"b"

深入剖析:StringBuilder的toString();
对象6: new String("ab");
toString()方法在字符串常量池中没有生成"ab"
*/

例题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
String s=new String("1");
s.intern();
String s2="1";
System.out.println(s==s2);//false,s指向堆空间中的String obj,而String Obj 才指向常量池中的1;而s2直接指向常量池中的“1”

String s3=new String("1")+new String("1");//
s3.intern();//在字符串常量池中生成"1",jdk6中:创建了一个新的对象”1“,也就有了新的地址
// jdk7中:并没有真正的创建一个新的对象,而是常量池中记录了new String("11")的地址
//常量池中没有11,那么就在常量池生成11,返回11地址,并将s3并且直接指向11
//为的是节省空间因为reference只占4个字节
String s4="11";
System.out.println(s3==s4);//true

String str=new String("wereash");
str.intern();
String str1="wereash";
System.out.println(str==str1);//false; 常量池中有wereash,那么仅返回常量池中wereash的地址

总结

  • jdk1.6中,将这个字符串对象尝试放入串池
    • 如果串池中有,则不会放入。返回已有串池中的对象的地址
    • 如果没有,就会把此对象复制一份,放入串池,并返回串池中的对象地址
  • jdk1.7起,将这个字符串对象尝试放入串池
    • 如果串池中有,则不会放入。返回已有的串池中的对象的地址
    • 如果没有,则会把对象的引用地址复制一份,放入串池,并返回串池中的引用地址

G1的String去重操作

  • 背景:对许多Java应用做的测试的出以下结果:
    • 堆存活数据集合里面String对象占25%
    • 堆存活数据集合里面重复的String对象有13.5%
    • String对象的平均长度是45
  • 许多大规模的Java应用的瓶颈在于内存,测试表明,在这些类型的应用里面,Java堆中存活的数据集合差不多25%是String对象,更进一步,这里面差不多一半String对象是重复的,重复的意思是说:
    • str1.equals(str2)=true。对上存在重复的String对象必然是一种内存浪费。这个项目将在G1垃圾回收器中实现自动持续对重复String对象进行去重,这样就能避免浪费内存

为什么用final修饰

主要目的就是保证String是不可变的。不可变就是第二次给String变量赋值的时候,不是在原内存地址上修改数据,而是重新指向一个新对象,新地址

1
2
3
4
5
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
}

String类的主力成员字段value是个char[]数组,而且用final修饰的。编译器不允许把value指向另一个地址。但可以直接对数组元素修改。

为什么设计为不可变

从内存角度来看

字符串常量池的要求:创建字符串时,如果该字符已经存在于池中,则将返回现有字符串的引用,而不是创建新的对象。字符串池的实现可以在运行时节约很多heap空间,多个String变量指向同一个内存地址。如果字符串可变的,用一个引用变更字符串将导致其他引用的值错误。

缓存Hashcode

字符串的Hashcode在Java中经常配合基于散列的集合一起正常运行。

安全性

String被广泛用作许多java类的参数,例如网络连接、打开文件等。如果对String的某一处改变不小心影响类该变量所有引用的表现,则连接或者文件将被更改,这可能导致严重的安全威胁