对于 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
-
-