Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
运行时常量池相对于Class文件常量池的另外一个重要特征是具备动态性,Java语言并不要求常量一定只有编译期才能产生,也就是并非预置入Class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用比较多的就是String类的intern()方法。
在JDK1.6及以前,运行时常量池是方法区的一部分。
在JDK1.7及以后,运行时常量池在Java 堆(Heap)中。
运行时和class常量池一样,运行时常量池也是每个类都有一个。但是字符串常量池全只有一个
class常量池(静态常量池)、运行时常量池、字符串常量池区别:
实际上,为了提高匹配速度,即更快的查找某个字符串是否存在于常量池,Java在设计字符串常量池的时候,还搞了一张stringtable, stringtable 有点类似于我们的hashtable,里面保存了字符串的引用。
在jdk6中 StringTable 的长度是固定的,就是1009,因此如果放入String Pool中的String非常多,就会造成hash冲突,导致链表过长。此时当调用 String.intern() 时会需要到链表上一个一个找,从而导致性能大幅度下降;
在jdk7+, StringTable 的长度可以通过一个参数指定:
上面我们已经稍微了解过字符串常量池了,它是java为了节省空间而设计的一个内存区域,java中所有的类共享一个字符串常量池。
public class Test {
public void test() {
String str1 = "abc";
String str2 = new String("abc");
System.out.println(str1 == str2);
String str3 = new String("abc");
System.out.println(str3 == str2);
String str4 = "a" + "b";
System.out.println(str4 == "ab");
final String s = "a";
String str5 = s + "b";
System.out.println(str5 == "ab");
String s1 = "a"; String s2 = "b";
String str6 = s1 + s2;
System.out.println(str6 == "ab");
String str7 = "abc".substring(0, 2);
System.out.println(str7 == "ab");
String str8 = "abc".toUpperCase();
System.out.println(str8 == "ABC");
String s3 = "ab";
String s4 = "ab" + getString();
System.out.println(s3 == s4);
String s5 = "a";
String s6 = "abc";
String s7 = s5 + "bc";
System.out.println(s6 == s7.intern());
}
private String getString(){
return "c";
}
}
String str1 = "abc";
System.out.println(str1 == "abc");
String str2 = new String("abc");
System.out.println(str2 == "abc");
String str2 = new String("abc");
String str3 = new String("abc");
System.out.println(str3 == str2);
String str4 = "a" + "b";
System.out.println(str4 == "ab");
String s1 = "a";
String s2 = "b";
String str6 = s1 + s2;
System.out.println(str6 == "ab");
String str7 = "abc".substring(0, 2);
System.out.println(str7 == "ab");
String str8 = "abc".toUpperCase();
System.out.println(str8 == "ABC");
intern的作用是把new出来的字符串的引用添加到stringtable中,java会先计算string的hashcode,查找stringtable中是否已经有string对应的引用了,如果有返回引用(地址),然后没有把字符串的地址放到stringtable中,并返回字符串的引用(地址)。
String a = new String("haha");
System.out.println(a.intern() == a);//false
因为有双引号括起来的字符串,所以会把ldc命令,即"haha"会被我们添加到字符串常量池,它的引用是string的char数组的地址,会被我们添加到stringtable中。所以a.intern的时候,返回的其实是string中的char数组的地址,和a的string实例化地址肯定是不一样的。
String e = new String("jo") + new String("hn");
System.out.println(e.intern() == e);//true
new String(“jo”) + new String(“hn”)实际上会转为stringbuffer的append 然后tosring()出来,实际上是new 一个新的string出来。在这个过程中,并没有双引号括起john,也就是说并不会执行ldc然后把john的引用添加到stringtable中,所以intern的时候实际就是把新的string地址(即e的地址)添加到stringtable中并且返回回来。
String f = new String("ja") + new String("va");
System.out.println(f.intern() == f);//false
或许很多朋友感觉很奇怪,这跟上面的例子2基本一模一样,但是却是false呢?这是因为java在启动的时候,会把一部分的字符串添加到字符串常量池中,而这个“java”就是其中之一。所以intern回来的引用是早就添加到字符串常量池中的”java“的引用,所以肯定跟f的原地址不一样。
Jdk6中字符串常量池位于PermGen(永久代)中,PermGen是一块主要用于存放已加载的类信息和 字符串池的大小固定的区域。
执行intern()方法时,若常量池中不存在等值的字符串,JVM就会在常量池中创建一个等值的字符串,然后返回该字符串的引用。除此以外,JVM 会自动在常量池中保存一份之前已使用过的字符串集合。
Jdk6中使用intern()方法的主要问题就在于字符串常量池被保存在PermGen中:
Jdk7将常量池从PermGen区移到了Java堆区。堆区的大小一般不受限,所以将常量池从PremGen区移到堆区使得常量池的使用不再受限于固定大小。可以使用 -XX:StringTableSize 虚拟机参数设置字符串池的map大小。
执行intern操作时,如果常量池已经存在该字符串,则直接返回字符串引用,否则复制该字符串对象的引用到常量池中并返回。
除此之外,位于堆区的常量池中的对象可以被垃圾回收。当常量池中的字符串不再存在指向它的引用时,JVM就会回收该字符串。
public static void main(String[] args) {
String s = new String("1");
s.intern();
String s2 = "1";
System.out.println(s == s2);
String s3 = new String("1") + new String("1");
s3.intern();
String s4 = "11";
System.out.println(s3 == s4);
}
打印结
public static void main(String[] args) {
String s = new String("1");
String s2 = "1";
s.intern();
System.out.println(s == s2);
String s3 = new String("1") + new String("1");
String s4 = "11";
s3.intern();
System.out.println(s3 == s4);
}
打印结
如上图所示。在 jdk6中上述的所有打印都是 false 的,因为 jdk6中的常量池是放在 Perm 区中的,Perm区和正常的 JAVA Heap 区域是完全分开的。上面说过如果是使用引号声明的字符串都是会直接在字符串常量池中生成,而 new 出来的 String 对象是放在 JAVA Heap 区域。所以拿一个 JAVAHeap 区域的对象地址和字符串常量池的对象地址进行比较肯定是不相同的,即使调用 String.intern方法也是没有任何关系的。
在 Jdk6 以及以前的版本中,字符串的常量池是放在堆的Perm区的,Perm区是一个类静态的区域,主要存储一些加载类的信息,常量池,方法片段等内容,默认大小只有4m,一旦常量池中大量使用intern 是会直接产生 java.lang.OutOfMemoryError:PermGen space 错误的。
在 jdk7 的版本中,字符串常量池已经从Perm区移到正常的Java Heap区域了。为什么要移动,Perm区域太小是一个主要原因,而且jdk8已经直接取消了Perm区域,而新建立了一个元区域。应该是jdk开发者认为Perm区域已经不适合现在 JAVA 的发展了。正式因为字符串常量池移动到JAVA Heap区域后,再来解释为什么会有上述的打印结果。
小结
从上述的例子代码可以看出 jdk7 版本对 intern 操作和常量池都做了一定的修改。主要包括2点
如果在字符串拼接中,有一个参数是非字面量,而是一个变量的话,整个拼接操作会被编译成StringBuilder.append,这种情况编译器是无法知道其确定值的。只有在运行期才能确定。
String s3 = new String("1") + new String("1");
那么,有了这个特性了,intern就有用武之地了。那就是很多时候,我们在程序中得到的字符串是只有在运行期才能确定的,在编译期是无法确定的,那么也就没办法在编译期被加入到常量池中。
这时候,对于那种可能经常使用的字符串,使用intern进行定义,每次JVM运行到这段代码的时候,就
会直接把常量池中该字面值的引用返回,这样就可以减少大量字符串对象的创建了。
static final int MAX = 1000 * 10000;
static final String[] arr = new String[MAX];
public static void main(String[] args) throws Exception {
Integer[] DB_DATA = new Integer[10];
Random random = new Random(10 * 10000);
for (int i = 0; i < DB_DATA.length; i++) {
DB_DATA[i] = random.nextInt();
}
long t = System.currentTimeMillis();
for (int i = 0; i < MAX; i++) {
arr[i] = new String(String.valueOf(DB_DATA[i % DB_DATA.length])).intern();
}
System.out.println((System.currentTimeMillis() - t) + "ms");
System.gc();
}
以上程序会有很多重复的相同的字符串产生,但是这些字符串的值都是只有在运行期才能确定的。所以,只能我们通过intern显示的将其加入常量池,这样可以减少很多字符串的重复创建。
Jdk6 中常量池位于PremGen区,大小受限,不建议使用String.intern()方法,不过Jdk7 将常量池移到了Java堆区,大小可控,可以重新考虑使用String.intern()方法,但是由对比测试可知,使用该方法的耗时不容忽视,所以需要慎重考虑该方法的使用。
String.intern() 方法主要适用于程序中需要保存有限个会被反复使用的值的场景,这样可以减少内存消耗,同时在进行比较操作时减少时耗,提高程序性能。