几个常量池
- 先看一下虚拟机的大体结构
图自知乎
class常量池
- class 文件结构图自知乎
- java类编译后,生成
.class文件
引自
class文件
中除了包含类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池(constant pool table)
,用于存放编译器生成的各种字面量(Literal)
和符号引用(Symbolic References)
。
字面量:常量概念,如文字符串"abc"
、被声明为final的常量值等。
符号引用:一组符号来描述所引用的目标。符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。(与直接引用的区分:直接引用一般是指向方法区的本地指针,相对偏移量或是一个能间接定位到目标的句柄)。
符号引用一般包括下面三类常量:
- 类和接口的全限定名
- 字段的名称和描述符
- 方法的名称和描述符
Class Data
存在方法区中(1.8方法区的实现为元空间,之前是永久代)。
字符串常量池
字符串常量池
,也叫字符串字面量池(String Literal Pool)
、字符串池(String Pool)
、全局字符串池
。
类加载完成,经过验证,准备阶段之后 在 堆 中生成了字符串对象实例,然后将这些字符串对象实例的引用地址存到字符串池
中。- 在
HotSpot VM
里实现的字符串池
功能的是一个StringTable
类,它是一个哈希表,里面存的是驻留字符串
(用双引号括起来的部分)的引用,而不是驻留字符串实例本身,实例本身还在堆中。所以这些字符串实例被这个StringTable
引用之后就等同被赋予了驻留字符串
的身份。这个StringTable
在每个HotSpot VM
的实例只有一份,被所有的类共享。 - 字符串常量池在堆中(1.7之前在方法区中)
运行时常量池
- 当类加载到内存中后,
jvm
就会将class常量池
中的内容存放到运行时常量池中,即每个类都有一个运行时常量池。class常量池
中的数据会经过解析,加载到运行时常量池中。
class
常量池中存储的是字面量和符号引用,而经过解析之后,也就是把符号引用替换为直接引用。解析的过程会去查询字符串常量池,就是上面的StringTable
,以保证运行时常量池所引用的字符串与全局字符串池中所引用的是一致的。
- 运行时常量池在方法区中。
字符串常量池的一些问题
@Test
public void test() {
String str1 = "abc";
String str2 = new String("def");
String str3 = "abc";
String str4 = str2.intern();
String str5 = "def";
System.out.println(str1 == str3); //true
System.out.println(str2 == str4);//false
System.out.println(str4 == str5);//true
}
"abc"
在堆中的实例在类加载的时候就已存在,直接返回"abc"
在StringTable
中的引用。new String("def")
新申请了内存空间,创建了一个新的字符串实例,值为"def"
,返回该地址。"def"
同样已存在StringTable
中。"abc"
在StringTable
中已经有了,直接返回StringTable
中的引用。intern()
:如果StringTable
中没有该字符串的引用,则加入其中。返回"def"
的引用。"def"
在StringTable
中已经有了,直接返回StringTable
中的引用。
@Test
public void test() {
//此行代码执行的底层执行过程是
//创建临时StringBuilder的append方法将"2"和"2"拼接在一块,然后调用toString方法new出"22"
//在StingTable中会有"22"的引用
String str1 = new String("2") + new String("2");
String str2 = "22";
System.out.println(str1 == str2); //false
}
@Test
public void test() {
String str1 = "a";
String str2 = "b";
String str3 = "ab";
//同样通过StringBuilder append,new String("ab"),会在StringTable中添加引用
String str4 = str1 + str2;
//编译时常量,在编译后直接能计算出拼接后的字符串值,不需要再运行时创建临时StringBuilder对象。
//返回StringTable中的"ab"引用
String str5 = "a" + "b";
System.out.println(str3 == str4); // false
System.out.println(str3 == str4.intern()); // true
System.out.println(str3 == str5);// true
}
特殊情况
public static final String A; // 常量A
public static final String B; // 常量B
static {
A = "ab";
B = "cd";
}
public static void tes() {
// 将两个常量用+连接对s进行初始化
String s = A + B;
String t = "abcd";
System.out.println(s == t);//false
}
A和B虽然被定义为常量,但是它们都没有马上被赋值。在运算出s的值之前,他们何时被赋值,以及被赋予什么样的值,都是个变数。因此A和B在被赋值之前,性质类似于一个变量。那么s就不能在编译期被确定,而只能在运行时被创建了。