简介
java 中说到常量池,一般有下面三种:
class文件常量池
例如 Hello.java 代码如下:1
2
3
4
5public 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
24Constant 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 | public class Hello { |
- 该代码在编译后的 class文件常量池中相关类型有 CONSTANT_String 和 CONSTANT_Utf8
- 在类加载的时候,CONSTANT_Utf8 类型对应的 Symbol 类型的 C++ 对象就已经创建,内容是跟 Class 文件同样格式的UTF-8编码的字符串”hello”
- 而 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 | public class Hello { |
通过 javap -v Hello
查看 main
方法相关部分如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18public 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;
- 该代码在编译后的 class文件常量池中相关类型有 CONSTANT_String 和 CONSTANT_Utf8
- 在类加载的时候,CONSTANT_Utf8 类型对应的 Symbol 类型的 C++ 对象就已经创建,内容是跟 Class 文件同样格式的UTF-8编码的字符串 “hello”
- 在运行时,执行到
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
28private 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
28private 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
7public 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” 对象,并保存引用到字符串常量池中了。