字符串是开发中常用的类型,在开发的过程中为了节省内存,JVM开辟了一块区域用于缓存常量,这块区域被叫做运行时常量池,它的主要来源有Class文件中Constant pool和运行时动态加入。

String中intern()方法在运行时会去检查运行时常量池是否有对应的引用,如果有则返回该引用,如果没有则将当前实例放入常量池并返回常量池中当前实例的引用。

下面是一个《深入理解JVM》书中的测试例子

public class RuntimeConstantPoolOOM{
    public static void main(String[] args){
        String str1 = new StringBuilder("计算机").append("计算机").append("软件").toString();
        System.out.println(str1.intern() == str1);

        String str2 = new StringBuilder("ja").append("va").toString();
        System.out.println(str2.intern() == str2);
    }
}

在上面的例子中如果是在JDK1.6中运行会得到两个false,如果在JDK1.7及以上中运行会得到一个true和一个false.为了说明这个差异的原因先来说说HotSpot虚拟机在这几个版本的变更。

在JDK1.6中HotSpot虚拟机将方法区这一规范实现为永久代,而在JDK1.7中开始逐步去除永久代,在JDK1.8中永久代已被移除,由元空间取代,原本永久代中加载的类信息在元空间存放,原本永久代中的运行时常量池在堆中存放。

所以产生上面代码问题的原因是:
1. 在jdk6中,str1.intern()方法由于”计算机软件”这一字符串在永久代中还不存在,所以这个方法会将这一字符串放入永久代,并返回对应的引用,而str1是StringBuilder类的一个实例在堆中创建,所以两个引用地址并不相等。str2也类似。
2. 在jdk7及以上版本,运行时常量池被放到了堆中,对于intern()方法的实现不会再进行复制,而是在常量池中记录堆中第一次出现这个实例的引用。对于”计算机软件”这个字符串来说,str1是首次出现的实例,而str1.intern()方法区常量池中获取首次出现实例的引用获得的也是相同的引用地址,所以两者相同为true。但对于”java”这个字符串来说,JVM在启动后就将整个实例加入的堆中并记录在了运行时常量池中(可能是因为这个字符串过于常用),所以str2对象已经不是”java”这个字符串第一次出现的实例了,而str2.intern()返回的地址还是第一次出现实例的地址,所以两者不等,为false。

下面是几个说明图

jdk6中的实现

jdk7中的实现

总结

  • 运行时常量池在jdk7后做了修改,从永久代移到了堆中
  • String的intern()方法在jdk7后运行时常量池只存引用,不用再复制一份,
  • String的intern()方法可以帮助我们减少创建对象从而减少空间

参考

最后修改日期: 2019年9月15日

作者

留言

撰写回覆或留言

发布留言必须填写的电子邮件地址不会公开。