常量池有三种:class文件常量池、运行时常量池、字符串常量池
class文件常量池(constant_pool)
class文件中的一部分,是一个静态的存储结构,常量池中两大类常量
- 字面量:文本字符串(类中所有用双引号引起来的字符串字面量)
- 符号引用:类和接口的全限定名、字段的名称和描述符、方法的名称和描述符
运行时常量池(runtime constant pool)
class文件常量池的运行时表示形式
- 一直在方法区中,HotSpot JDK1.8中,方法区的实现为元空间(Metaspace),存在于本地内存中
全局字符串常量池
HotSpot VM里,记录interned string的一个全局表叫做StringTable(猜测:类似HashMap<hashCode, reference>,HashSet底层也是HashMap)
1. 初始为空,由String类维护
A pool of strings, initially empty, is maintained privately by the class String.——jdk8 String.intern()注释
2. 全局的(运行时常量池是一个类对应一个)
public class StringTest {
public static void main(String[] args) {
System.out.println(new String1().a == new String2().b);//true
}
}
public class String1 {
String a = "123";
}
public class String2 {
String b = "123";
}
3. 字面量相加(+),编译时就直接拼接
// 源码:
public class Test {
public static void main(String[] args) {
String str = "aa" + "bb" + "cc";
}
}
// javap -v Test
public class Test
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #4.#13 // java/lang/Object."<init>":()V
#2 = String #14 // aabbcc
#3 = Class #15 // Test
#4 = Class #16 // java/lang/Object
#5 = Utf8 <init>
#6 = Utf8 ()V
#7 = Utf8 Code
#8 = Utf8 LineNumberTable
#9 = Utf8 main
#10 = Utf8 ([Ljava/lang/String;)V
#11 = Utf8 SourceFile
#12 = Utf8 Test.java
#13 = NameAndType #5:#6 // "<init>":()V
#14 = Utf8 aabbcc
#15 = Utf8 Test
#16 = Utf8 java/lang/Object
{
public Test();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 1: 0
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 aabbcc
2: astore_1
3: return
LineNumberTable:
line 6: 0
line 7: 3
}
SourceFile: "Test.java"
从反编译的class文件中可以看到,只存在“aabbcc”
4. 只要存在new String(…)相加(+),则使用StringBuilder的append()和toString()方法,toString()实现为new String(…)
public class Test {
public static void main(String[] args) {
String str1 = new String("aa") + new String("bb");
String str2 = "bb" + new String("cc");
}
}
public class Test
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #12.#21 // java/lang/Object."<init>":()V
#2 = Class #22 // java/lang/StringBuilder
#3 = Methodref #2.#21 // java/lang/StringBuilder."<init>":()V
#4 = Class #23 // java/lang/String
#5 = String #24 // aa
#6 = Methodref #4.#25 // java/lang/String."<init>":(Ljava/lang/String;)V
#7 = Methodref #2.#26 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#8 = String #27 // bb
#9 = Methodref #2.#28 // java/lang/StringBuilder.toString:()Ljava/lang/String;
#10 = String #29 // cc
#11 = Class #30 // Test
#12 = Class #31 // java/lang/Object
#13 = Utf8 <init>
#14 = Utf8 ()V
#15 = Utf8 Code
#16 = Utf8 LineNumberTable
#17 = Utf8 main
#18 = Utf8 ([Ljava/lang/String;)V
#19 = Utf8 SourceFile
#20 = Utf8 Test.java
#21 = NameAndType #13:#14 // "<init>":()V
#22 = Utf8 java/lang/StringBuilder
#23 = Utf8 java/lang/String
#24 = Utf8 aa
#25 = NameAndType #13:#32 // "<init>":(Ljava/lang/String;)V
#26 = NameAndType #33:#34 // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#27 = Utf8 bb
#28 = NameAndType #35:#36 // toString:()Ljava/lang/String;
#29 = Utf8 cc
#30 = Utf8 Test
#31 = Utf8 java/lang/Object
#32 = Utf8 (Ljava/lang/String;)V
#33 = Utf8 append
#34 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuilder;
#35 = Utf8 toString
#36 = Utf8 ()Ljava/lang/String;
{
public Test();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 1: 0
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=4, locals=3, args_size=1
0: new #2 // class java/lang/StringBuilder
3: dup
4: invokespecial #3 // Method java/lang/StringBuilder."<init>":()V
7: new #4 // class java/lang/String
10: dup
11: ldc #5 // String aa
13: invokespecial #6 // Method java/lang/String."<init>":(Ljava/lang/String;)V
16: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
19: new #4 // class java/lang/String
22: dup
23: ldc #8 // String bb
25: invokespecial #6 // Method java/lang/String."<init>":(Ljava/lang/String;)V
28: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
31: invokevirtual #9 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
34: astore_1
35: new #2 // class java/lang/StringBuilder
38: dup
39: invokespecial #3 // Method java/lang/StringBuilder."<init>":()V
42: ldc #8 // String bb
44: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
47: new #4 // class java/lang/String
50: dup
51: ldc #10 // String cc
53: invokespecial #6 // Method java/lang/String."<init>":(Ljava/lang/String;)V
56: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
59: invokevirtual #9 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
62: astore_2
63: return
LineNumberTable:
line 5: 0
line 6: 35
line 7: 63
}
SourceFile: "Test.java"
- 对于
String str1 = new String("aa") + new String("bb");
,字节码指令为0~34,0先new StringBuilder对象,28再调用appender方法,31最后调用toString()创建String对象- 对于
String str2 = "bb" + new String("cc");
,字节码指令为35~62,32 new StringBuilder对象,44 append 字符串“bb”,56 append字符串“cc”,59 toString创建String对象
5. 运行时生产的字符串不会进入字符串常量池,除非调用String.intern()方法
public class StringTest {
public static void main(String[] args) {
String str1 = new StringBuilder("abc").append("def").toString();
System.out.println(str1.intern() == str1); //true
// 下面的代码用于对比测试
String str2 = new StringBuilder("defdef").toString();
System.out.println(str2.intern() == str2); //false
// 下面的代码用于对比测试(特殊字符串)
String str3 = new StringBuilder("ja").append("va").toString();
System.out.println(str3.intern() == str3); //false
}
}
- 第一行代码,解析时,在堆中new了三个对象,“abc”、“def"和"abcdef”,并把"abc"、"def"引用放入字符串常量池,但字符串"abcdef"是运行时产生的,jvm并不会把它放入池中,调用intern()方法时,jvm把str1这个引用放入池中,所以str1.intern() == str1为true
- 第三行代码,堆中new了两个"defdef"对象,一个地址赋值给str2,一个地址存入池中,调用intern()方法时,是把池中的地址返回,故str2.intern() == str2为false
- 第五行代码,是由于"java"字符串在jvm启动加载的类中使用到,已经在池中了
6. 这是个纯运行时的结构,而且是惰性(lazy)维护的
这个惰性是由于类加载时的解析阶段造成的,HotSpot VM在遇到操作符号引用的字节码指令(如ldc)时,会先对指令操作的对象进行解析,把它从符号引用解析为直接引用
备注:类字段在类初始化时,会进行解析
7. 所有具有相同字符序列的字符串对象,引用的都是同一个字符序列
public class StringTest {
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
String str1 = new String("aaa");
String str2 = new String("aaa");
String str3 = "aaa";
Field field = String.class.getDeclaredField("value");
field.setAccessible(true);
char[] value = (char[]) field.get(str1);
value[2] = 'c';
System.out.println(str1);//aac
System.out.println(str2);//aac
System.out.println(str3);//aac
}
}
也就是说,对象里的字段引用的是同一个地址
private final char value[]
8. 各版本String.intern()的区别
public class StringTest {
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
String str = new StringBuilder("计算机").append("软件").toString();
System.out.println(str.intern() == str);//jdk<=1.6为false,jdk>=1.7为true
}
}
- jdk6及以前,将堆中str对象复制一份到方法区的字符串常量池中
- jdk7及以后,将str引用存入堆中字符串常量池中,不会再创建对象
9. 常见面试题
String str = new String("aaa")
创建几个对象?
首先,创建"aaa"对象,并把引用存入字符串常量池,然后,创建str对象,所以池中的引用并不是str