1 简介

String.intern()是JDK一早就提供的native方法,不由Java实现,而是底层JVM实现,这让我们对它的窥探提高了难度。特别是在Oracle收购了Sun公司后,源代码不开源了,更无法深入研究了。但我们还是有必要尽量地去探索。

本文将主要讲解一下String.intern()方法的原理、特点,并介绍一个新奇的应用。

2 String的池化

方法intern()的作用就是将String池化,这个池是String的常量池。不同版本的JDK有不同的实现。

2.1 不同实现与不同内存空间

  • JDK 6:intern()方法会把首先遇到的字符串复制一份到永久代中,然后返回永久中的实例引用;如果不是首次,说明常量池中已经有该字符串,直接返回池中的引用。常量池在永久代(PermGen)中。
  • JDK 7:intern()方法首次遇到字符串时,不会复制实例,而是直接把该字符串的引用记录在常量池中,并返回该引用;如果不是首次,则直接返回池中引用。JDK 7常量池在堆中。
  • JDK 8:功能与JDK 7类似。常量池在元空间Metaspace中,元空间不在虚拟机内存中,而是使用本地内存。

2.2 常量池大小差异

这个所谓的String常量池,其实就是一张哈希表,跟HashMap类似,所以也是有大小限制和哈希冲突可能。常量池越大,哈希冲突可能性越小。

  • JDK 6早期版本,池大小为常量1009,后期变得可配置,通过参数-XX:StringTableSize=N指定。大小也会受限于永久代的大小,建议避免使用intern()方法,防止造成PermGen内存溢出。

  • JDK 7将常量池移到堆后,可以存放更多常量,也一样通过参数可配置大小。在Java 7u40后,常量池默认大小增加到了60013。

  • JDK 8默认大小一开始就是60013,依旧支持参数配置。

总的来说,-XX:StringTableSize的默认值在Java 7u40以前为1009,Java 7u40以后改为60013。

3 例子分析

通过例子,来理解一下就更清晰了。JDK 7和8应该表现一致,本文使用JDK 8。

3.1 JDK 8

先演示JDK 8的情况:

例子1

String str1 = new String("pkslow"); System.out.println(str1.intern() == str1);

结果:false

分析:因为使用了字面量,在编译期就会把字符串放到常量池,当使用new String()时,会创建新的对象。所以常量池中的引用与创建的对象引用不同。

例子2

String str1 ="pkslow"; System.out.println(str1.intern() == str1);

结果:true

分析:与上个例子对比,将常量池的地址赋值给了str1变量,所以相等。

例子3

String str1 = new StringBuilder("pk").append("slow").toString(); System.out.println(str1.intern() == str1);  String str2 = new StringBuilder("pk").append("slow").toString(); System.out.println(str2.intern() == str2);

结果:true false

分析:

(1)第一句创建了一个新的字符串对象,str1为其引用,调用str1.intern()时会把它的引用放到常量池中并返回,所以是同一个引用。

(2)在(1)中已经放在常量池了,所以str2.intern()返回的是str1,与str2不相等。

例子4

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

结果:false

分析:按理说与上个例子的(1)一样,应该为true才对。问题在于java它是一个比较特殊的字符串,已经在常量池中存在了,所以不相等。至于为何会存在,我的猜想是有两种可能:其它JDK的Java代码有该常量;JVM代码直接把某些特殊字符串放到了常量池。这个没有深究了。

3.2 JDK 6的不同

当我们知道了原理后,不同表现就可以很容易判断出来了。如下例子: