常量池及字符串对象的创建

简介

java 中说到常量池,一般有下面三种:

  1. class文件常量池
  2. 运行时常量池
  3. 字符串常量池

class文件常量池

例如 Hello.java 代码如下:

1
2
3
4
5
public class Hello {
public static void main(String[] args) {
String s = "hello";
}
}

在编译后通过 javap -v Hello 查看常量池部分如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Constant pool:
#1 = Methodref #4.#20 // java/lang/Object."<init>":()V
#2 = String #21 // hello
#3 = Class #22 // base/Hello
#4 = Class #23 // java/lang/Object
#5 = Utf8 <init>
#6 = Utf8 ()V
#7 = Utf8 Code
#8 = Utf8 LineNumberTable
#9 = Utf8 LocalVariableTable
#10 = Utf8 this
#11 = Utf8 Lbase/Hello;
#12 = Utf8 main
#13 = Utf8 ([Ljava/lang/String;)V
#14 = Utf8 args
#15 = Utf8 [Ljava/lang/String;
#16 = Utf8 s
#17 = Utf8 Ljava/lang/String;
#18 = Utf8 SourceFile
#19 = Utf8 Hello.java
#20 = NameAndType #5:#6 // "<init>":()V
#21 = Utf8 hello
#22 = Utf8 base/Hello
#23 = Utf8 java/lang/Object

其中 #2 为 CONSTANT_String 类型,持有一个 #21 的 index,指向一个 CONSTANT_Utf8 类型的常量 “hello”

运行时常量池

  • 运行时常量池以前位于永久代,jdk1.8 移除了永久代,取而代之的是元空间,位于 native memory。
  • 在类加载的时候,class文件常量池中的大部分数据都会进入到运行时常量池
  • CONSTANT_Utf8 类型对应的是一个 Symbol 类型的 C++ 对象,内容是跟 Class 文件同样格式的UTF-8编码的字符串
  • CONSTANT_String 类型对应的是一个实际的 Java 对象的引用,C++ 类型是 oop
  • CONSTANT_Utf8 类型对应的 Symbol 对象在类加载时候就已经创建了
  • CONSTANT_String 则是 lazy resolve 的,例如说在第一次引用该项的 ldc 指令被第一次执行到的时候才会 resolve。
    那么在尚未 resolve 的时候,HotSpot VM 把它的类型叫做 JVM_CONSTANT_UnresolvedString,内容跟Class文件里一样只是一个index;
    等到 resolve 过后这个项的常量类型就会变成最终的 JVM_CONSTANT_String,而内容则变成实际的那个 oop。

字符串常量池

  • jdk1.7 已经把字符串常量池从永久代中移到了堆中。
  • 字符串常量池中存的是引用,引用指向的字符串对象还是存储在堆中

以字面量形式”hello”创建

创建了几个对象?

2个对象。一个 String 对象和一个数组对象

什么时候创建对象?什么时候存入字符串常量池?

1
2
3
4
5
public class Hello {
public static void main(String[] args) {
String s = "hello";
}
}
  1. 该代码在编译后的 class文件常量池中相关类型有 CONSTANT_String 和 CONSTANT_Utf8
  2. 在类加载的时候,CONSTANT_Utf8 类型对应的 Symbol 类型的 C++ 对象就已经创建,内容是跟 Class 文件同样格式的UTF-8编码的字符串”hello”
  3. 而 CONSTANT_String 类型的解析是延迟的,具体在第一次引用该项的 ldc 指令被第一次执行到的时候。
    解析 CONSTANT_String 时(该代码也就是执行到 Strings = "hello"; 的时候),根据 index 去运行时常量池查找 CONSTANT_UTF8,然后找到对应的 Symbol 对象,
    去到 StringTable,StringTable 支持以 Symbol 为 key 来查询是否已经有内容匹配的项存在与否,
    存在则直接返回匹配项,不存在则创建出内容匹配的java.lang.String 对象,然后将其引用放入 StringTable
  • 注:这里的 StringTable 是字符串常量池的实现方式。

以 new String(“hello”) 形式创建

创建了几个对象?

2个对象。两个 String 对象和一个共享的数组对象

什么时候创建对象?什么时候存入字符串常量池?

1
2
3
4
5
public class Hello {
public static void main(String[] args) {
String s = new String("hello");
}
}

通过 javap -v Hello 查看 main 方法相关部分如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=2, args_size=1
0: new #2 // class java/lang/String
3: dup
4: ldc #3 // String hello
6: invokespecial #4 // Method java/lang/String."<init>":(Ljava/lang/String;)V
9: astore_1
10: return
LineNumberTable:
line 10: 0
line 11: 10
LocalVariableTable:
Start Length Slot Name Signature
0 11 0 args [Ljava/lang/String;
10 1 1 s Ljava/lang/String;

  1. 该代码在编译后的 class文件常量池中相关类型有 CONSTANT_String 和 CONSTANT_Utf8
  2. 在类加载的时候,CONSTANT_Utf8 类型对应的 Symbol 类型的 C++ 对象就已经创建,内容是跟 Class 文件同样格式的UTF-8编码的字符串 “hello”
  3. 在运行时,执行到 String s = new String("hello"); 的时候,会执行一个引用该 CONSTANT_String 的 ldc 指令。也就是触发 CONSTANT_String 的解析。
    解析过程如上所述,因为是第一次,所以会在堆中创建一个内容为 “hello” 的 String 对象,然后将引用放到字符串常量池。随后才会通过 invokespecial 指令
    再在堆中创建一个 String 对象,该 String 对象引用存储在栈上。

String.intern()

jdk1.7 以后,该方法会查看字符串常量池中是否有引用指向该 String 对象,没有则添加该 String 对象的引用到字符串常量池。有则直接返回引用。

代码测试

  • 注:以下代码注释说法不考虑数组对象
    测试代码1如下:
    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
    private static void testStringIntern1() {

    // 堆上创建两个对象:hello 和 hello1。字符串常量池中引用 s 指向 hello 对象,栈上引用 s1 指向 hello1 对象
    String s1 = new String("hello");

    // 去常量池中寻找后发现字符串常量池中存在引用s,该引用指向的对象 hello 和栈上引用 s1 指向的 hello1 对象内容相同,直接返回(无变量接收)
    s1.intern();

    // 理解为没有创建对象,因为字符串常量池上存在引用 s 指向的 hello 对象的内容为 "hello",直接返回,此时栈上引用 s2 也指向引用 s 指向的 hello 对象
    String s2 = "hello";

    // s1 指向的对象为 hello1,s2 指向的对象为 hello,虽然两个对象的内容都是 "hello",但不是同一个对象
    System.out.println(s1 == s2); // false

    // 堆上创建一个对象:helloWorld。栈上引用 s3 指向 helloWorld 对象。字符串常量池中不存在该对象引用。
    // 至于 "hello" 和 "World" 我们不去讨论它们。
    String s3 = new String("hello") + new String("World");

    // 去常量池中寻找后未发现存在引用指向的对象的内容为 "helloWorld",但是jdk1.8(jdk1.7及以后版本)的字符串常量池可以直接存储引用,
    // 所以这里在字符串常量池中创建了一个引用 ss,该引用指向 s3 引用指向的堆上的 helloWorld 对象
    s3.intern();

    // 理解为没有创建对象,因为字符串常量池上存在引用 ss 指向的对象的内容为 "helloWorld",直接返回,此时栈上引用 s4 也指向 helloWorld 对象
    String s4 = "helloWorld";

    // 所以 s3 和 s4 指向同一个对象,也就是位于堆上的 helloWorld 对象
    System.out.println(s3 == s4); // true
    }

测试代码2如下:

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
private static void testStringIntern2() {

// 堆上创建两个对象:hello 和 hello1。字符串常量池中引用 s 指向 hello 对象,栈上引用 s1 指向 hello1 对象
String s1 = new String("hello");

// 理解为没有创建对象,因为字符串常量池上存在引用 s 指向的 hello 对象的内容为 "hello",直接返回,此时栈上引用 s2 也指向引用 s 指向的 hello 对象
String s2 = "hello";

// 去常量池中寻找后发现字符串常量池中存在引用s,该引用指向的对象 hello 和栈上引用 s1 指向的 hello1 对象内容相同,直接返回(无变量接收)
s1.intern();

// s1 指向的对象为 hello1,s2 指向的对象为 hello,虽然两个对象的内容都是 "hello",但不是同一个对象
System.out.println(s1 == s2); // false

// 堆上创建一个对象:helloWorld。栈上引用 s3 指向 helloWorld 对象。字符串常量池中不存在该对象引用。
// 至于 "hello" 和 "World" 我们不去讨论它们。
String s3 = new String("hello") + new String("World");

// 因为此时常量池上不存在引用指向的对象内容为 "helloWorld"
// 所以在堆上创建一个字符串对象 helloWorld2,添加其引用 ss 到字符串常量池,同时栈上引用 s4 也指向 helloWorld2 对象
String s4 = "helloWorld";

// 去字符串常量池查找发现存在引用 ss 指向的对象的内容为 "helloWorld",直接返回(无变量接收)
s3.intern();

// s3 指向堆上的对象 helloWorld,而 s4 指向对象的对象 helloWorld2,所以不是同一个对象
System.out.println(s3 == s4); // false
}

测试代码3如下:

1
2
3
4
5
6
7
public static void main(String[] args) {
String str1 = new StringBuilder("he").append("llo").toString();
System.out.println(str1.intern() == str1);

String str2 = new StringBuilder("ja").append("va").toString();
System.out.println(str2.intern() == str2);
}

打印结果为true,false。
str1.intern() == str1 等于 true 很好理解。
但是 str2.intern() 不指向引用 str2 指向的对象?说明之前字符串常量池中已经有了引用指向的对象内容为”java”?
的确如此,因为 sun.misc.Version 类中定义了一个常量:

1
private static final String launcher_name = "java";

该类会在 JDK 类库的初始化过程中被加载和初始化。初始化时就创建了 “java” 对象,并保存引用到字符串常量池中了。