在JAVA
语言中有8种基本类型和一种比较特殊的类型String
。这些类型为了使他们在运行过程中速度更快,更节省内存,都提供了一种常量池的概念。常量池就类似一个JAVA
系统级别提供的缓存。
8种基本类型的常量池都是系统协调的,String
类型的常量池比较特殊。它的主要使用方法有两种:
- 直接使用双引号声明出来的
String
对象会直接存储在常量池中。 - 如果不是用双引号声明的
String
对象,可以使用String
提供的intern
方法。intern
方法会从字符串常量池中查询当前字符串是否存在,若不存在就会将当前字符串放入常量池中
今天我们主要来学习一下String
中的intern
方法
Java实现
String#intern
方法是一个native
的方法,注释写的非常明了。
“如果常量池中存在当前字符串,就会直接返回当前字符串. 如果常量池中没有此字符串,会将此字符串放入常量池中后,再返回”。
String#intern
方法的具体实现是c++
的代码,这里我们不过多的关注,有兴趣的同学可以去深入解析String#intern中第一节自行查看相关源码。这里我们介绍一下它的大概实现过程:
JAVA
使用jni
调用c++
实现的StringTable
的intern
方法,StringTable
的intern
方法跟Java
中的HashMap
的实现是差不多的,只是不能自动扩容。默认大小是1009。
要注意的是,String
的StringPool
是一个固定大小的Hashtable
,默认值大小长度是1009,如果放进StringPool
的String
非常多,就会造成Hash
冲突严重,从而导致链表会很长,而链表长了后直接会造成的影响就是当调用String.intern
时性能会大幅下降(因为要一个一个找)。
在JDK6
中StringTable
是固定的,就是1009的长度,所以如果常量池中的字符串过多就会导致效率下降很快。在JDK7
中,StringTable
的长度可以通过一个参数指定:
-XX:StringTableSize=99991
JDK 6和JDK 7下intern的区别
相信很多JAVA
程序员都做做类似String s = new String("abc")
这个语句创建了几个对象的题目。
这种题目主要就是为了考察程序员对字符串对象的常量池掌握与否。上述的语句中是创建了2个对象,第一个对象是abc
字符串存储在常量池中,第二个对象在JAVA Heap
中的String
对象。
比如:1
2
3
4
5
6
7
8
9
10
11public 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);
}
打印结果是
- JDK 6下:
false false
- JDK 7下:
false true
具体为什么稍后再解释,然后将s3.intern();
语句下调一行,放到String s4 = "11";
后面。将s.intern(); 放到String s2 = "1";
后面。是什么结果呢?1
2
3
4
5
6
7
8
9
10
11public 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);
}
打印结果为:
- JDK 6下:
false false
- JDK 7下:
false false
JDK 6的解释
注:图中绿色线条代表
string
对象的内容指向。黑色线条代表地址指向。
如上图所示。首先说一下JDK6
中的情况,在JDK6
中上述的所有打印都是false
的,因为JDK6
中的常量池是放在Perm
区中的,Perm
区和正常的JAVA Heap
区域是完全分开的。
上面说过如果是使用引号声明的字符串都是会直接在字符串常量池中生成,而new
出来的String
对象是放在JAVA Heap
区域。
所以拿一个JAVA Heap
区域的对象地址和字符串常量池的对象地址进行比较肯定是不相同的,即使调用String.intern
方法也是没有任何关系的。
JDK 7的解释
再说说JDK7
中的情况。这里要明确一点的是,在JDK6
以及以前的版本中,字符串的常量池是放在堆的Perm
区的,Perm
区是一个类静态的区域,主要存储一些加载类的信息、常量池、方法片段等内容,默认大小只有4M
,一旦常量池中大量使用intern
是会直接产生java.lang.OutOfMemoryError: PermGen space
错误的。
所以在JDK7
的版本中,字符串常量池已经从Perm
区移到正常的Java Heap
区域了。为什么要移动,Perm
区域太小是一个主要原因,现在JDK8
已经直接取消了Perm
区域,而新建立了一个元区域。应该是JDK
开发者认为Perm
区域已经不适合现在JAVA
的发展了。
正式因为字符串常量池移动到JAVA Heap
区域后,再来解释为什么会有上述的打印结果。
- 在第一段代码中,先看
s3
和s4
字符串。String s3 = new String("1") + new String("1");
,这句代码中现在生成了2个对象,是字符串常量池中的“1” 和JAVA Heap
中的s3
引用指向的对象。中间还有2个匿名的new String("1")
我们不去讨论它们。此时s3
引用对象内容是”11”,但此时常量池中是没有 “11”对象的。 - 接下来
s3.intern();
这一句代码,是将s3
中的“11”字符串放入String
常量池中,因为此时常量池中不存在“11”字符串,因此常规做法是跟JDK6
图中表示的那样,在常量池中生成一个 “11” 的对象,关键点是JDK7
中常量池不在Perm
区域了,这块做了调整。常量池中不需要再存储一份对象了,可以直接存储堆中的引用。这份引用指向s3
引用的对象。也就是说引用地址是相同的。 - 最后
String s4 = "11";
这句代码中”11”是显示声明的,因此会直接去常量池中创建,创建的时候发现已经有这个对象了,此时也就是指向s3
引用对象的一个引用。所以s4
引用就指向和s3
一样了。因此最后的比较s3 == s4
是true
。 再看
s
和s2
对象。String s = new String("1");
第一句代码,生成了2个对象。常量池中的“1” 和JAVA Heap
中的字符串对象。s.intern();
这一句是s
对象去常量池中寻找后发现 “1” 已经在常量池里了。接下来
String s2 = "1";
这句代码是生成一个s2
的引用指向常量池中的“1”对象。 结果就是s
和s2
的引用地址明显不同。图中画的很清晰。
- 来看第二段代码,从上边第二幅图中观察。第一段代码和第二段代码的改变就是
s3.intern();
的顺序是放在String s4 = "11";
后了。这样,首先执行String s4 = "11";
声明s4
的时候常量池中是不存在“11”对象的,执行完毕后,“11“对象是s4
声明产生的新对象。然后再执行s3.intern();
时,常量池中“11”对象已经存在了,因此s3
和s4
的引用是不同的。 - 第二段代码中的
s
和s2
代码中,s.intern();
,这一句往后放也不会有什么影响了,因为对象池中在执行第一句代码String s = new String("1");
的时候已经生成“1”对象了。下边的s2
声明都是直接从常量池中取地址引用的。s
和s2
的引用地址是不会相等的。
总结
从上述的例子代码可以看出JDK7
版本对intern
操作和常量池都做了一定的修改。主要包括2点:
- 将
String
常量池从Perm
区移动到了Java Heap
区 String#intern
方法时,如果存在堆中的对象,会直接保存对象的引用,而不会重新创建对象。