概述
字符串常量池 是 JVM 为了提升性能和减少内存消耗针对字符串(String 类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建。
java
// 在堆中创建字符串对象"ab"
// 将字符串对象"ab"的引用保存在字符串常量池中
String aa = "ab";
// 直接返回字符串常量池中字符串对象"ab"的引用
String bb = "ab";
System.out.println(aa==bb);// true
HotSpot虚拟机中,字符串常量池是通过StringTable来实现的,可以理解为一个固定大小的HashTable,保存的是字符串(字面量?)(key)和 字符串对象的引用(value)的映射关系,字符串对象的引用指向堆中的字符串对象。
下面将通过详细的分析,说清楚字符串对象的创建过程。
字符串的创建方式
参考文章:流程图详解 new String("abc") 创建了几个字符串对象 个人感觉这个带图解会比较清晰。
new方式创建
String s1 = new String("abc")
会创建 1 或 2 个字符串对象。
1、如果字符串常量池中不存在字符串对象"abc"的引用,那么它会在堆上创建两个字符串对象,其中一个字符串对象的引用会被保存在字符串常量池中。
示例代码(JDK 1.8):
ini
String s1 = new String("abc");
对应的字节码:

ldc
命令用于判断字符串常量池中是否保存了对应的字符串对象的引用,如果保存了的话直接返回,如果没有保存的话,会在堆中创建对应的字符串对象并将该字符串对象的引用保存到字符串常量池中(注意,是在堆中创建了两个对象,一个赋值给变量str1,另一个字符串对象的地址存在字符串常量池)。
代码执行结果如下

2、如果字符串常量池中已存在字符串对象"abc"的引用,则只会在堆中创建 1 个字符串对象"abc"。
示例代码(JDK 1.8):
java
// 字符串常量池中已存在字符串对象"abc"的引用
String s1 = "abc";
// 下面这段代码只会在堆中创建 1 个字符串对象"abc"
String s2 = new String("abc");
对应的字节码:

这里就不对上面的字节码进行详细注释了,7 这个位置的 ldc
命令不会在堆中创建新的字符串对象"abc",这是因为 0 这个位置已经执行了一次 ldc
命令,已经在堆中创建过一次字符串对象"abc"了。7 这个位置执行 ldc
命令会直接返回字符串常量池中字符串对象"abc"对应的引用。
字面量创建
实际使用中,大部分是通过字面量的方式创建字符串,JVM虚拟机对这种创建字符串的方式做了优化,以减少内存占用。
大概流程
java
String str1 = "abc";
上述代码执行有两种情况:
-
若字符串常量池没有"abc",会在字符串常量池创建一个字符串对象,并将其引用赋值给str1(注意:这种方式也是在堆中创建对象后,将堆中的对象引用赋值给str1)
-
若字符串常量池已有"abc",则不再创建对象,直接将字符串常量池中的对象引用赋值给str1
上述代码执行结果,在jvm中如下:

stack区中建立了String类型的str1变量,指向了堆中的字符串
StringTable的key是"abc"这个字面量,value是堆区字符串对象地址,也指向了堆区的字符数组。
所以以下代码的运行结果是:
详解字面量方式创建字符串对象的流程
严格地说,字面量在代码运行到它所在语句之前,它还不是字符串对象
- 在上面的 java 代码被编译为 class 文件后,
"abc"
存储于【类文件常量池】中
less
Constant pool: // 常量池
#1 = Methodref #19.#41 // java/lang/Object."<init>":()V
#2 = String #42 // ab
...
- 当 class 完成类加载之后,
"abc"
这个字面量被存储于【运行时常量池】(归属于方法区)中,其中 #1 #2 都会被翻译为运行时真正的内存地址
再看一下 class 中 main 方法的字节码
arduino
public static void main(java.lang.String[]); // 字节码指令
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=2, args_size=1
0: ldc #2 // String abc
2: astore_1
3: return
...
- main 方法被调用时,就会执行里面的字节码指令
yaml
0: ldc #2 // String abc
2: astore_1
3: return
ldc #2
就是到运行时常量池中找到 #2 的内存地址,找到 "abc"
这个字面量,先判断字符串常量池是否保存了对应的字符串对象的引用,如果有的话直接返回,否则根据它在常量池中创建一个 String 对象。(懒加载)
另外说需要说明:同一个类中的值相同的字面量,只有一份

StringTable
HotSpot 虚拟机中字符串常量池的实现是 src/hotspot/share/classfile/stringTable.cpp
,StringTable
可以简单理解为一个固定大小的HashTable
,容量为 StringTableSize
(可以通过 -XX:StringTableSize
参数来设置),保存的是字符串字面量(key)和 字符串对象引用(value)的映射关系,字符串对象的引用指向堆中的字符串对象。
-
字面量方式创建的字符串,会放入 StringTable 中,StringTable 管理的字符串,才具有不重复的特性。
-
而 char[],byte[],int[],String,以及 + 方式本质上都是使用 new 来创建,它们都是在堆中创建新的字符串对象,不会考虑字符串重不重复。
String的intern方法
字符串提供了 intern 方法来实现去重,让通过new创建的字符串对象有机会受到 StringTable 的管理
arduino
public native String intern();
调用intern方法,有以下两种情况:
如果字符串常量池中保存了对应的字符串对象的引用,就直接返回该引用。
java
String x = ...;
String s = x.intern();
则会直接返回StringTable中的字符串对象地址给s。
例子
ini
String x = new String(new char[]{'a', 'b', 'c'}); // 野生的
String y = "abc"; // 将 "abc" 加入 StringTable
String z = x.intern(); // 已有,返回 StringTable 中 "abc",即 y
System.out.println(z == y); true
System.out.println(z == x); false
如果字符串常量池中没有保存对应的字符串对象的引用,那就在常量池中创建一个指向该字符串对象的引用并返回。(1.7 以上 JDK 的做法)
例子
ini
String x = new String(new char[]{'a', 'b', 'c'}); // 野生的
String z = x.intern(); // 野生的 x 加入 StringTable,StringTable 中有了 "abc"
String y = "abc"; // 已有,不会产生新的对象,用的是 StringTable 中 "abc"
System.out.println(z == x); true
System.out.println(z == y); true
StringTable的位置
StringTable 的位置(1.6)

StringTable 的位置(1.8)

如何证明
- 1.6 不断将字符串用 intern 加入 StringTable,最后撑爆的是永久代内存,为了让错误快速出现,将永久代内存设置的小一些:
-XX:MaxPermSize=10m
,最终会出现java.lang.OutOfMemoryError: PermGen space
- 1.8 不断将字符串用 intern 加入 StringTable,最后撑爆的是堆内存,为了让错误快速出现,将堆内存设置的小一些:
-Xmx10m -XX:-UseGCOverheadLimit
后一个虚拟机参数是避免 GC 频繁引起其他错误而不是我们期望的java.lang.OutOfMemoryError: Java heap space
题目实战
String str1 = "abc"; String str2 = new String("abc");分别创建了几个对象
看上面的内容即可
计算输出结果1
java
// 在堆中创建字符串对象"Java" ???
// 将字符串对象"Java"的引用保存在字符串常量池中
String s1 = "Java";
// 直接返回字符串常量池中字符串对象"Java"对应的引用
String s2 = s1.intern();
// 会在堆中在单独创建一个字符串对象
String s3 = new String("Java");
// 直接返回字符串常量池中字符串对象"Java"对应的引用 ???
String s4 = s3.intern();
// s1 和 s2 指向的是堆中的同一个对象
System.out.println(s1 == s2); // true
// s3 和 s4 指向的是堆中不同的对象
System.out.println(s3 == s4); // false
// s1 和 s4 指向的是堆中的同一个对象
System.out.println(s1 == s4); //true
计算输出结果2
java
public static void main(String[] args) {
String s1 = new String("abc");
String s2 = "abc";
String s3 = "abc";
String s4 = new String("abc");
System.out.println(s1 == s2);
System.out.println(s2 == s3);
System.out.println(s1 == s4);
System.out.println(System.identityHashCode(s1));
System.out.println(System.identityHashCode(s2));
System.out.println(System.identityHashCode(s3));
System.out.println(System.identityHashCode(s4));
}
上述代码的输出
false
true
false
460141958
1163157884
1163157884
1956725890

分析下面代码的执行结果
java
public static void main(String[] args) {
String str1 = "abc";
String str2 = "ab" + "c";
String str3 = new String("ab") + "c";
String str4 = str3.intern();
System.out.println(str1 == str2); true
System.out.println(str1 == str3); fasle
System.out.println(str1 == str4); true
}