Memory Model -- 08 -- 不同JDK版本之间 String.intern() 方法的区别

对于 String 类的 intern 方法,平常了解的不是很多,最近在学习的过程中接触到了这个方法,特地来整理记录一下


一、区别

  • JDK6 及之前的版本中

    • 当调用 intern 方法时,如果字符串常量池 (StringTable) 中存在该字符串对象,则返回字符串常量池中该字符串对象的引用

    • 当调用 intern 方法时,如果字符串常量池 (StringTable) 中不存在该字符串对象,则拷贝该字符串对象至字符串常量池,并返回该拷贝的引用

  • JDK7 及之后的版本中

    • 当调用 intern 方法时,如果字符串常量池 (StringTable) 中存在该字符串对象,则返回字符串常量池中该字符串对象的引用

    • 当调用 intern 方法时,如果字符串常量池 (StringTable) 中不存在该字符串对象,则再判断该字符串对象在堆中是否已存在

      • 如果该字符串对象在堆中已存在,则将堆中该字符串的引用添加进字符串常量池,并返回该引用

      • 如果该字符串对象在堆中不存在,则将该字符串对象添加进字符串常量池,并返回字符串常量池中该字符串对象的引用

  • 由此可见,不管在哪个版本中,如果在字符串常量池已经存在该字符串对象,则 intern() 方法都会返回字符串常量池中该字符串对象的引用,这一点是相同的;不同的是,在 JDK7 及之后的版本中,当字符串常量池中不存在该字符串对象时,还会去堆中进行查找和判断


二、重现 PermGen space 异常

  • 在 JDK 6 及之前的版本中,字符串常量池位于永久代中,而永久代的内存极为有限,如果频繁调用 intern() 方法,拷贝字符串对象至字符串常量池,会导致字符串常量池被挤爆 (实现字符串常量池的是一个名为 StringTable 的类,它是一个 Hash 表,默认值长度为 1009),进而会抛出 OutOfMemoryError: PermGen space 异常

  • 从 JDK7 开始,原先位于方法区 (永久代) 中的字符串常量池已被移动到 Java 堆中

  • 现在我们通过代码来重现一下这个异常

    public class PermGenErrorTest {
    
        public static void main(String[] args) {
            for (int i = 0; i < 1000; i++) {
                getRandomString(1000000).intern();
            }
            System.out.println("Mission Complete!");
        }
    
        public static String getRandomString(int length) {
            // 字符串源
            String str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
            Random random = new Random();
            StringBuffer buffer = new StringBuffer();
            for (int i = 0; i < length; i++) {
                int number = random.nextInt(62);
                buffer.append(str.charAt(number));
            }
            return buffer.toString();
        }
    }
    
    • JDK6

      • 测试源码如上所示,此外为了快速复现这个异常,我们需要将永久代的大小设置的小一点,如下所示,将两个参数都设置为了 4M

        • PermSize:永久代初始大小

        • MaxPermSize:永久代最大大小

        在这里插入图片描述

      • 设置好之后,我们再运行程序,就会抛出 OutOfMemoryError: PermGen space 异常了

        在这里插入图片描述

    • JDK7

      • 接着我们将 JDK 版本切换到 7,然后再执行上面的方法,则能够正常执行

        在这里插入图片描述

      • 由此可以说明,在字符串常量池从永久代移动到堆内存之后,OutOfMemoryError: PermGen space 异常就得到了避免

    • JDK8

      • 最后我们将 JDK 版本切换到 8,然后再执行上面的方法,也能够正常执行,但由于 JDK8 中移除了永久代,因此 PermSize、MaxPermSize 这两个参数已经没用了

        在这里插入图片描述


三、intern 在不同 JDK 版本中的表现

  • 最后,我们来看看不同版本下 intern() 方法的具体表现

    public class InternDifference {
    
        public static void main(String[] args) {
            String s1 = new String("a");
            s1.intern();
            String s2 = "a";
            System.out.println(s1 == s2);
    
            String s3 = new String("a") + new String("a");
            s3.intern();
            String s4 = "aa";
            System.out.println(s3 == s4);
        }
    }
    
  • JDK6

    s1 == s2 // false
    s3 == s4 // false
    
    • s1 == s2 为 false

      • 当使用引号显式声明字符串时,会直接在字符串常量池中创建该字符串;而使用 new 关键字,会在堆中创建对应的对象,因此 String s1 = new String("a"); 这行代码创建了两个字符串对象 a,一个存储在堆中,一个存储在字符串常量池中

      • s1.intern(); 这行代码表示调用 intern() 方法,试图将 “a” 字符串的一份拷贝添加至字符串常量池,但此时字符串常量池中已经存在了 “a”,因此添加失败

      • s1 对象的引用指向堆中的 “a” 对象,而 s2 对象的引用指向常量池中的 “a” 对象,两者地址不同,故为 false

    • s3 == s4 为 false

      • String s3 = new String("a") + new String("a"); 这行代码会在堆中创建一个 “aa” 的字符串对象,而使用引号声明的字符串 “a”,在字符串常量池中已存在,故不会再创建

      • s3.intern(); 这行代码表示调用 intern() 方法,试图将 “aa” 字符串的一份拷贝添加至字符串常量池,此时字符串常量池中并没有 “aa” 存在,因此添加成功

      • s3 对象的引用指向堆中的 “aa” 对象,s2 对象的引用指向字符串常量池中的拷贝 “aa”,但是由于放的是拷贝,实际上这两个 “aa” 是两个不同的字符串对象,因此两者地址不同,故为 false

  • JDK7

    s1 == s2 // false
    s3 == s4 // true
    
    • s1 == s2 为 false

      • 当使用引号显式声明字符串时,会直接在字符串常量池中创建该字符串;而使用 new 关键字,会在堆中创建对应的对象,因此 String s1 = new String("a"); 这行代码创建了两个字符串对象 a,一个存储在堆中,一个存储在字符串常量池中

      • s1.intern(); 这行代码表示调用 intern() 方法,试图将 “a” 字符串的一份拷贝添加至字符串常量池,但此时字符串常量池中已经存在了 “a”,因此添加失败

      • s1 对象的引用指向堆中的 “a” 对象,而 s2 对象的引用指向常量池中的 “a” 对象,两者地址不同,故为 false

    • s3 == s4 为 true

      • String s3 = new String("a") + new String("a"); 这行代码会在堆中创建一个 “aa” 的字符串对象,而使用引号声明的字符串 “a”,在字符串常量池中已存在,故不会再创建

      • s3.intern(); 这行代码表示调用 intern() 方法,试图将 “aa” 字符串的引用添加至字符串常量池,此时字符串常量池中并没有 “aa” 存在,因此添加成功

      • s3 对象的引用指向堆中的 “aa” 对象,s2 对象的引用也指向堆中的 “aa” 对象,两者指向的是同一个字符串对象,地址相同,故为 true


四、参考资料

发布了106 篇原创文章 · 获赞 83 · 访问量 10万+

猜你喜欢

转载自blog.csdn.net/Goodbye_Youth/article/details/103518074