大部分内容来源这篇美团的文章:https://tech.meituan.com/2014/03/06/in-depth-understanding-string-intern.html,原文非常有深度,这里只捡了一些比较浅的东西出来
JDK6到7的字符串常量池出现了很大变化,为了防止绕晕,这里只拿了7以后的情况出来
-
直接用双引号声明的
String
对象直接存到常量池 -
用
intern
方法会从字符串常量池查询当前字符串是否存在,若不存在则将当前字符串放入常量池 -
new
一个字符串出来最多会生成两个对象- 字符串常量池中的引用对象
- Java Heap 中的
String
对象
当然,如果常量池中原本已经存在了被引用对象,则只会多生成一个对象
字符串常量池
字符串常量池的主体是一个 StringTable
。
HotSpot VM中使用全局表StringTable
记录interned string
,StringTable
的 intern
方法类似 HashMap
,本质是HashSet<String>
,但不能自动扩容,(JDK6)默认大小 1009,(JDK7)通过-XX:StringTableSize=99991
参数指定,默认大小 60013。StringTable
只存储对String
实例的引用。
- 加载字节码文件时,常量(
""
圈起来的就是字符串常量)会被加载到运行时常量池 (Runtime Constant Pool) 中,字符串此时只有CONSTANT_Utf8
,采用Lazy-Init
,因此未成为 Java 对象,运行到对应位置再进行对象加载。
类加载阶段
HotSpot VM 在类加载阶段,JVM会创建常量池中的字符串对象实例,但这一过程是惰性的,加载时仅将字面量放入类的运行时常量池,即 CONSTANT_Utf8
,而 CONSTANT_String
还没有,也不会进入字符串常量池。(当然静态变量会直接被指定初始值,也就会创建字符串对象,同时保存引用到字符串常量池)
intern()
JDK7中,intern()
方法将在字符串池中保存堆中的引用并返回(如果已经有则直接返回该引用)。对执行方法的对象本身不会造成任何影响,该指哪指哪。以下为例:
String s3 = new String("1") + new String("1");
s3.intern();
String s4 = "11";
System.out.println(s3 == s4);//true
s3
实际上在 Heap 上 new 出了一个"11"
,且不会保存到常量池中。s3.intern()
将这一对象直接保存到了字符串常量池中,s3
仍指向这一对象,只是对象本身被保存到了常量池,因此s4
显式创建"11"
会直接拿常量池中的对象(引用)来用,也就是s3
。
String s = new String("1");
s.intern();
String s2 = "1";
System.out.println(s == s2);//false
这一段则有很大不同,new String("1")
进行了两步操作——在字符串常量池建了"1"
,并且还在 Heap 上建了一个额外的 String
对象并赋给 s
,所以s.intern()
不会造成任何改变——常量池已经有"1"
,而s
也还是指向 Heap 上的 String
。因此s2
直接指向常量池中的"1"
,s
间接指向1
,两者自然不是一个引用。
综合案例
class NewTest1{
public static String s1="static"; // 第一句
public static void main(String[] args) {
String s1=new String("he")+new String("llo"); //第二句
s1.intern(); // 第三句
String s2="hello"; //第四句
System.out.println(s1==s2);//第五句,输出是true。
}
}
如图,类初始阶段会初始化 s1
,所以在 main()
运行之前字符串常量池里已经有 "static"
。局部变量 s1
由两个字符串拼接而成,拼接过程实际是 StringBuilder
的 append
,最后会通过 toString()
方法new一个 String
对象,且不会放入字符串常量池。在intern()
时才会被放入字符串常量池。s2
显式创建,发现字符串常量池中已有,因此直接使用,两者就是同一对象,s1==s2
。
Oracle JDK 8u20 后的新特性
Oracle JDK 8u20 后出现了 StringDuplication
特性,默认关闭。允许 G1 GC 下的字符串排重,在 JVM 底层允许相同数据的字符串指向同一数据。这一功能下,G1 会在垃圾收集过程中会把新创建的字符串对象放入队列中,在 Young GC 后,并发地将内部数据(JDK 9 以后就是 byte[]
)一致地字符串进行排重。虽然可以节省内存空间,但也会占用额外 CPU,拖慢 Young GC 的作用。