字符串池与常量池是完全不同的两个东西,但是很多地方都喜欢把它们混为一谈,很容易让初学者产生误解,在这里我想好好讨论一下它们。
字符串池也可以被称为字符串常量池,我认为这个名称就是产生误解的根源,有些人说着说着就把字符串三个字省略了,只剩下了常量池... 所以为了避免误解,我建议在指代字符串对象的缓存池的时候,就直接称之为字符串池
1 常量池
常量池分为两个类型,一是.class文件中静态的常量池,二是.class文件中的静态常量池被加载到JVM中而形成的运行时常量池。
1.1 静态常量池
.class文件中的常量池可以看作一个数组,数组中存储了一些常量,当需要在字节码指令中用到这个常量的时候,就通过数组的索引来访问它。
看下面的代码:
String m = "hellohellohellohellohello";
String n = "hellohellohellohellohello";
它在字节码中将会是这种形式:
// 常量池:
#1 hellohellohellohellohello
#2 ...
...
----------------------------
String m = #1;
String n = #1;
当然,这只是一个简化的版本,实际上要更加复杂 (实际的版本可以看文章末尾参考资料部分里面贴出的那个回答,目前可以先只考虑简化的版本)
注意,在这个里面存储的字符串常量只是一个简单的UTF8编码的字节序列,而不是Java的字符串对象,它就和你在一个txt文本中存储的字符串一样,我们用UTF8格式来打开一个.class文件,可以看到hellohellohellohellohello
是可以被解析的:
1.2 运行时常量池
理解了静态的常量池之后,运行时常量池就很容易想明白了。简单来说,运行时常量池就是.class文件中的静态常量池在JVM中的运行时表示,每一个.class文件的静态常量池都会生成一个对应的运行时常量池。等到JVM在解释String m = #1
这条指令时,它可以去这个类的运行时常量池中查找#1的定义。
2 字符串池
字符串池是Java为了重用String
对象而设置的一个缓存池,Java1.7之前设置在方法区上,保存的是String对象;Java1.7之后设置在堆上,保存的是String
对象的引用,String
对象本身存在于堆上的其他位置。下文中以Java1.7之后的情况为标准。
继续上面的例子。当JVM在解释String m = #1时,它已经从运行时常量池拿到了相应的UTF8序列,接下来,它会在字符串池中寻找和这个UTF8序列对应的String对象,并把这个对象的引用赋值给m。你可能会好奇这个String被创建的时机,根据R大的这篇文章,在这条语句所在的类被加载时,如果字符串池中已经存在对应的对象了,那么就什么都不做,如果不存在,就会创建一个对应的String对象,并把其引用放入池中。
除了字符串池,Integer
、Long
等Wrapper类型也有自己的缓存池,比如Integer
会缓存从-128~127的Integer
对象,当使用字面量赋值或者Integer.valueOf()
时,如果池中存在相应的对象,就会返回池中的对象,只有当池中没有时才会在堆上创建新对象。
不过,和字符串池不同的时,这些Wrapper池不会像字符串池一样可以增长,也就是池中的对象数目是固定的,Integer池中只会有-128~127。
3 涉及String的其他内容
在这里我收集了一些关于String的一些有趣和需要注意的信息,欢迎大家补充
强调,下面的代码只在JDK 11的环境下进行了测试,不同的JDK中可能会有不同的行为,同样欢迎大家补充
3.1 在Java 7之后String.intern()
的行为
考虑下面的代码:
String a = new String("hello");
a.intern();
a.intern()
会在字符串池中查找是否有一个字符串引用所指向的对象的值是"hello",如果有,就直接返回池中的引用;如果没有,就把a指向的对象的引用放入池中,在返回该引用。所以:
String a = new String("hello");
return a == a.intern();
// 返回false.
// a调用intern方法时,字符串池中已经存在表示hello这个字符序列的String对象引用了(因为"hello"字面量的存在),所以a.intern返回的引用和a肯定不同
------------------------------------------------------------------------------
String a = new String(new char[] {'h', 'e', 'l', 'l', 'o'});
return a == a.intern();
// 返回true
// new String(new char[] {'h', 'e', 'l', 'l', 'o'})因为没有出现"hello"字面量,所以池中没有表示hello的对象引用,那么a.intern会将a放入池中再返回,返回结果自然和a相等
3.2 两个字符串相加时创建的新对象(的引用)会被放入字符串池中吗:
结论:除非是两个字面量直接相加,否则都不会被放入字符串池中
String m = "ja" + "va";
String n = new String(new char[]{'j', 'a', 'v', 'a'});
System.out.println(n.intern() == n);
// 输出:false
// 分析:
// 根据n.intern() != n 可以得出相加后的"java"对象被放入了字符串池。实际上,编译器会将"ja" + "va"直接连起来变成"java",所以字节码中是m = "java",这样一来在解释器遇到这一行时,一个"java"对象就会在堆上被创建,并且它的引用会被放入字符串池中
---------------------------------------------------------------------------
String m1 = "ja";
String m2 = "va";
String m = m1 + m2;
String n = new String(new char[]{'j', 'a', 'v', 'a'});
System.out.println(n.intern() == n);
// 输出:true
// 分析:
// 根据n.intern() == n 可以得出相加后的"java"对象没有被放入字符串池。
// 可以自己测试当m1 = new String("ja"), m2 = new String("va")的情况,得到的结果和本例是一样的
4 总结
运行时常量池中的字符串是UTF8序列,字符串池中存储的是Java的String对象(的引用),不要把两者弄混了。
5 参考资料
更详细的内容比如字面量对应的String对象被创建的时机、对字节码的分析可以看 @木女孩大大的这篇回答:
Java 中new String("字面量") 中 "字面量" 是何时进入字符串常量池的?www.zhihu.com其他参考:
String pool vs Constant poolstackoverflow.com有帮助的话麻烦点个赞奥~ 另外也欢迎关注我的专栏!