03. Java字符串常量池
建议先了解完JVM的相关知识,再看本文章,文中涉及到JVM特定内存区域描述,不清楚该区域作用,将难以理解。
常量池概述
java源码文件经过编译后,会生成 .class 字节码文件,打开任何一个字节码文件,可以看到 cafe babe 魔数等二进制数据。

第一行二进制描述了如下信息:
- cafe babe:魔数
- 0000:次版本
- 0034:主版本(十进制52,JDK1.8)
- 002a:常量池计数器
- 0a00...:常量池数据区和代码区
常量池可以看成存放Java代码,主要存放两类数据:字面量、符号引用
字面量:字面量是由字母、数字等构成的字符串或数值常量。字面量只在右值出现(等号右边的值),如下所示,等号右边的值都是字面量:
int a = 1;
Integer b = 2;
String c = "abcda";
符号引用:上面的a、b、c就是符号名称,即符号引用。符号引用可以是:
- 类和接口的全限定名
com.xxx.User - 字段的名称和描述符
name - 方法的名称和描述符
set()
静态常量池、运行时常量池和字符串常量池
静态常量池:静态的、未加载的 .class文件; 运行时常量池:经 JVM 将 .class 文件装入内存、加载到方法区;常量池内的符号引用在程序加载或运行时会被转变为被加载到方法区的代码的直接引用,在 JVM 调用这个方法时,就能根据直接引用找到这个方法在方法区的位置,从而执行方法。 字符串常量池:属于运行时常量池的一小部分,根据JDK版本的不同,从属关系也不同。
字符串常量池: 为何要将字符串常量池独立出来呢?
这是因为字符串的分配,是需要耗费时间和空间的,而作为最基础的数据类型,被程序在各个地方大量使用,这将大量频繁的创建字符串,极大地影响了程序的性能。
JVM 为了提高性能和减少内存开销,在实例化字符串常量时进行了优化:为字符串开辟一个字符串常量池,类似与缓存区,在创建字符串常量时作用如下:
- 检查字符串常量池是否存在该字符串
- 存在该字符串,返回引用实例
- 不存在,实例化该字符串放入常量池内,再把此引用实例返回
- 调用时,直接从字符串常量池内取值
字符串常量池底层是 hotspot 的 C++实现的,类似一个哈希表的 K-V 结构,保存的本质是字符串对象的引用。
String创建比较
不同的String创建方式,导致在比较两个字符串时,得到的结果也不同,以下是相关实例:
示例一:
String s0 = "ab";
Stirng s1 = "ab";
String s2 = "a" + "b";
System.out.println( s0 == s1 ); // true
System.out.println( s0 == s2 ); // true
s0 和 s1 都是字符串常量,所以两者相等。
s2 由两个字符串相加,JVM将在编译期把字符串的 + 连接优化为连接后的值 ab,所以 s0 等于 s2。
示例二:
String s0 = "ab"
String s1 = new String("ab");
String s2 = "a" + new String("b");
System.out.println( s0 == s1 ); // false
System.out.println( s0 == s2 ); // false
System.out.println( s1 == s2 ); // false
s0 指向常量池的 ab,s1 指向堆中的 ab,所以不相等。
s2 后半部分存在 new String("b") 在JVM编译期无法确定,相当于 new String("ab"),值需要在程序运行期来动态分配再将新地址返回给 s2,所以 s2 与其他都不相等。
示例三:
String s1 = new String("ab");
String s2 = new String("ab");
System.out.println( s1 == s2 ); // false
这里创建了三个对象,两个符号引用对象 s1 和 s2 ,一个字符串常量对象 ab。
new 的方式始终会创建一个对象,不管字符串的内容是否已经存在,而双引号 "" 的方式会重复利用字符串常量池中已经存在的对象。
其他常量池
java 中基本乐行的包装类大部分都实现了常量池,这些类是 Byte、Short、Integer、Long、Character、Boolean,浮点类型则没有实现。
这些的包装类型也只是值在 [-128, 127] 之间才使用常量池,因为较小数字用到的概率更大一些。