String、常量池以及intern方法
.class文件常量池
常量池主要存放两类常量:字面量和符号引用。
字面量指文本字符串等。
符号引用指:
- 类和接口的全限定名
- 字段的名称和描述符
- 方法的名称和描述符
编译期结束,当类加载器加载类时,.class文件常量被加入运行常量池,如果常量已存在则不再加载。
.class文件常量对象(如String)被创建是预定好的,除非已常量已存在则不再加载。
String intern方法
intern
方法和JVM的运行常量池有关。
intern
方法有两个作用:
- 将字符串字面量放入常量池(如果池没有的话)
- 返回这个常量的引用
第1个作用,在JDK1.6和JDK1.7中含义不同。
JDK1.6的运行常量池在堆外内存,调用intern
方法,如果池没有的话将拷贝堆内实例对象到运行常量池,返回的引用自然地址在堆外。滥用该方法,则占用大量内存,抛出异常。
JDK1.7的运行常量池被移入堆内内存,调用intern
方法,如果池没有的话将放入实例对象的引用,并返回实例对象的引用。
下面是段测试代码:
// 运行环境JDK1.8
public class Test {
public static void main(String[] args) {
// 情况1. 待测对象运行期才能确定
String s1 = "hello";
String s2 = ",world";
String s3 = s1 + s2;
System.out.println(s3.intern() == s3); // true
// 情况2. 待测对象编译期已经确定
String s4 = "你好" + ",世界";
System.out.println(s4.intern() == s4); // true
// 情况3. 待测对象运行期才能确定
String s5 = "你好" + s2;
System.out.println(s5.intern() == s5); // true
// 情况4
String s6 = new String("hello");
String s7 = s6.intern();
System.out.println(s6 == s7); // false
}
}
当我们使用javac
命令编译完后,通过javap -v
查看类文件。
Constant pool:
...
#30 = Utf8 hello
#31 = Utf8 ,world
...
#40 = Utf8 你好,世界
#41 = Utf8 你好
...
常量池中已存在"hello"
,",world"
,"你好,世界"
,"你好"
四个常量。
运行期时,上面四个常量进入运行常量池。
-
情况1
堆上创建一个对象(称为A),s3
指向此对象。当A调用intern
方法时,运行常量池不存在"hello,world"
,intern
方法会在运行常量池中保存引用,并返回此引用,此引用指向对象A。故结果为true
。 -
情况2
两个字面量直接拼接,编译后只有拼接结果的常量,运行期进入运行常量池,s4
指向常量池,因为常量池存在此常量,故s4.intern()
与s3
指向相同。 -
情况3
堆上创建一个对象(称为B),s5
指向此对象。当B调用intern
方法时,运行常量池不存在"你好,world"
,intern
方法会在运行常量池中保存引用,并返回此引用,此引用指向对象B。故结果为true
。 -
情况4
堆上创建一个对象(称为C),s6
指向此对象。当C调用intern
方法时,运行常量池存在"hello"
,intern
方法会返回一个指向常量池的引用。此引用和s6
不同,故结果为true
。
总结
intern
方法可以在运行期往运行常量池中添加常量。
intern
方法可以避免重复创建相同的String实例,优化内存。
JDK1.7,此方法通过常量池判断是否存在相同的String实例,如果存在,将引用指向同个对象,将其他相同但冗余的对象取消引用,利用内存回收机制回收new
创建的对象。如果不存在,在运行常量池加入该对象或其引用。