StringTable为一个HashTable结构,结构如下:
key的计算
- 使用了字符串和字符串长度计算获得一个唯一的hash值,
这个hash值和变量名没有关系奥- 利用这个hash值获得它对应的唯一的序号,把这个序号作为Key
- 这个key就可以在hash表里用hash函数计算获得一个位置了。
value
记录的是这个对象的真实内存地址
hashtable的基本链接的结构
struct HashtableEntry {
INT_PTR hash;
void* key; //hashValue
void* value; //instanceOopDesc
HashtableEntry* //next;
};
StringTable的大小是可以调整的,通过参数
-XX:StringTableSize进行调整
- java6中默认大小为1009
- java7中的默认大小为60013
- java8中可选的最小大小为1009
StringTable在jvm的逻辑内存地址也在发生变化
- jdk6中它在方法区里
- jdk7/8中它在堆中
放到字符串常量池并不是指向字符串常量池,每个字符串对象都指向它真实的字符串地址,字符串常量池就是维护对象名(key)和它指向的真实字符串的地址(value)
public class StringTest {
public static void main(String[] args) {
String str1 = "helloworld";
}
}
对应的字节码文件
0 ldc #2 <helloworld>
2 astore_1
3 return
从字节码文件里可以看到使用了ldc字节码指令获取到了字符串helloworld,这说明这个helloworld已经在内存中了,
其实这里是因为字符串属于常量信息,无论是类字段还是局部变量,字符串都属于一种常量信息,所以常量信息会被编译到常量池中,在类加载的链接阶段加载到内存并分配好了内存空间以及赋值
在链接阶段就会创建好一个字符串对象,并且放到字符串常量池中


具体加载过程使用此图进行说明
String的底层实现是一个char数组或者后来的byte数组,
因此声明一个String变量
- 首先会生成这个变量的hash值,去字符串数组里看看是否已经存在,如果存在,就直接指向已存在的对象
- 如果不存在会创建String对象和一个数组对象,数组对象存值,String对象指向这个数组对象
- 后将这个字符串放到字符串常量池中
public class StringTest {
public static void main(String[] args) {
String a = "hello" + "word";
}
}
对应的字节码文件如下
0 ldc #2 <helloword>
2 astore_1
3 return
从字节码文件里可以看出来jvm底层进行了优化,把两个字符串合并了
public class StringTest {
public static void main(String[] args) {
String a = "hello" ;
String b = "word";
String c = a+b;
System.out.println(c.intern()=="helloword");
}
对应的字节码文件如下:
0 ldc #2 <hello>
2 astore_1
3 ldc #3 <word>
5 astore_2
6 new #4 <java/lang/StringBuilder>
9 dup
10 invokespecial #5 <java/lang/StringBuilder.<init> : ()V>
13 aload_1
14 invokevirtual #6 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;>
17 aload_2
18 invokevirtual #6 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;>
21 invokevirtual #7 <java/lang/StringBuilder.toString : ()Ljava/lang/String;>
24 astore_3
25 return
从字节码文件可以看出执行了如下过程 (new StringBuilder()).append(a).append(b).toString();
所以字符变量拼接底层实际上是使用了StringBuilder对象的append方法,因此如果进行循环多次字符变量拼接过程中会反复创建StringBuilder对象,因此推荐显示使用StringBuilder完成字符拼接操作
此外toString()操作底层实现就是返回了一个new String(“字符串”)对象
public class StringTest {
public static void main(String[] args) {
String abc = new String("abc");
String abcdef = new String("abc")+ new String("def");
}
}
对应的字节码文件
0 new #2 <java/lang/String>
3 dup
4 ldc #3 <abc>
6 invokespecial #4 <java/lang/String.<init> : (Ljava/lang/String;)V>
9 astore_1
10 new #5 <java/lang/StringBuilder>
13 dup
14 invokespecial #6 <java/lang/StringBuilder.<init> : ()V>
17 new #2 <java/lang/String>
20 dup
21 ldc #3 <abc>
23 invokespecial #4 <java/lang/String.<init> : (Ljava/lang/String;)V>
26 invokevirtual #7 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;>
29 new #2 <java/lang/String>
32 dup
33 ldc #8 <def>
35 invokespecial #4 <java/lang/String.<init> : (Ljava/lang/String;)V>
38 invokevirtual #7 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;>
41 invokevirtual #9 <java/lang/StringBuilder.toString : ()Ljava/lang/String;>
44 astore_2
45 return
可以看出无论是声明单个字符,还是拼接字符,new String()用于初始化的字符串都是被加载到了内存中的字符串常量池中的
因此new String过程创建了两个对象一个是new String对象,一个是初始化的字符串对象
具体实现过程说明如下:
1.首先会对赋值的字符串进行对象创建,创建过程就是上面的字符串创建流程。
2.创建完成之后进行new String的操作,创建一个对象
3.对象的char对象指向之前创建出来的字符地址
public class StringTest {
public static void main(String[] args) {
String abcdef = new String("abc")+ new String("def");
String abcd = new StringBuilder("abc").toString();
String abdd = new StringBuilder("abc").append("def").toString();
}
}
对应的字节码文件
0 new #2 <java/lang/StringBuilder>
3 dup
4 invokespecial #3 <java/lang/StringBuilder.<init> : ()V>
7 new #4 <java/lang/String>
10 dup
11 ldc #5 <abc>
13 invokespecial #6 <java/lang/String.<init> : (Ljava/lang/String;)V>
16 invokevirtual #7 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;>
19 new #4 <java/lang/String>
22 dup
23 ldc #8 <def>
25 invokespecial #6 <java/lang/String.<init> : (Ljava/lang/String;)V>
28 invokevirtual #7 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;>
31 invokevirtual #9 <java/lang/StringBuilder.toString : ()Ljava/lang/String;>
34 astore_1
35 new #2 <java/lang/StringBuilder>
38 dup
39 ldc #5 <abc>
41 invokespecial #10 <java/lang/StringBuilder.<init> : (Ljava/lang/String;)V>
44 invokevirtual #9 <java/lang/StringBuilder.toString : ()Ljava/lang/String;>
47 astore_2
48 new #2 <java/lang/StringBuilder>
51 dup
52 ldc #5 <abc>
54 invokespecial #10 <java/lang/StringBuilder.<init> : (Ljava/lang/String;)V>
57 ldc #8 <def>
59 invokevirtual #7 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;>
62 invokevirtual #9 <java/lang/StringBuilder.toString : ()Ljava/lang/String;>
65 astore_3
66 return
从字节码文件可以看出StringBuilder的初始参数字符串和append()方法里的字符串参数是被加载到字符串常量池的,但是合并后的字符串是没有放到字符串常量池的,上面已经说过StringBuilder是用来进行字符串拼接的,通过toString方法返回拼接后的字符对象。因此通过字节可以看出通过toString返回拼接后的对象,该对象的字符串其实并没有在字符串常量池中创建对应的字符串
总结:
- new String()创建字符串的时候,
它的初始化常量会查询一遍字符串常量池,没有的话会放进去,所以new String的字符永远是已经被加载到字符串常量池了,所以返回的是堆中的一个对象,这个对象没有放到常池中,但是它指向的字符串是被记录在字符串常量池中的- new StringBuilder().append()创建字符串的时候,也是同理,它的各个参数对应的字符串也是永远都被已经加载到字符串常量池了,
但是合并后的字符串却没有加载到字符串常量池,因为是动态创建的,所以没通过字符串常量池进行查询,因此这个最终通过toString返回的对象对应的字符串记录是没有在字符串常量池中进行记录的。
字符拼接的具体过程如下:
注意:有地方说 把下面的 s改成 String s = “aa”+new String("bb") 创建对象会变少,而我经过调试发现并没有变少还是4个

在程序中创建一个字符串的时候,如果同过类来创建,往往会声明一个变量获得对应的对象,在比较的时候如果直接拿这个变量比较的话,其实比较的是这个变量对象在堆中的内存地址,这个地址对于不同对象肯定是不同的。为了比较对象真实的字符串信息,因此需要intern()方法获得对象指向的真正的字符串地址。
- intern方法在字符串常量池中根据这个对象的名字来寻找是否存在该对象,找到了返回地址
- 如果没找到:在jdk6中,会创建一个字符串,然后返回这个字符串地址,但是他创建的这个字符串是把堆空间中的字符串对象复制到了永久代空间里的,因此对象的地址发生了变化。字符串常量池指向的地址和
- 如果没找到:在jdk7/8中,会先查找字符串常量池,然后创建一个字符串,这个字符串指向的是堆中存在的字符串,但是返回的地址是真实对象地址
总的来说jdk67的差异就是应为永久代和元空间的位置不同导致的运行逻辑不同
public class StringTest {
public static void main(String[] args) {
String s1 = "ab";
String a = new String("ab");
System.out.println(a==s1); //false因为a是一个新的对象地址 而s1是ab地址
System.out.println(a.intern()==s1); //true 因为a.intern()去字符串常量表里找他的字符对应的地址,这个地址和s1是同一个
String s = new StringBuilder("a").append("b").toString(); //创建一个新的对象
System.out.println(s1==s); //false 新对象地址和ab不同
System.out.println(s.intern()==s1);
}
}