这是我参与更文挑战的第2天,活动详情查看: 更文挑战
1 什么是字符串常量池
Java 的String 对象值底层实现为数组,每次创建新的对象时,都要花费昂贵的时间代价去分配内存空间。作为基础的数据类型,大量频繁地创建String 对象,会耗费极大时间和空间代价,影响程序的性能。JVM为了提高时间效率和减少内存开销,在实例化String 对象的时候会进行优化。
- 会开辟一块内存空间存放所有String 对象,这一块空间就称为常量池
- 创建新的String 对象时,先检查常量池中是否存在该字符串,如果存在则返回引用实例,如果不存在则创建新的对象。
- 为了实现字符串常量池,必须要求字符串为不可变,从而避免共享对象出现修改冲突。
2 字符串常量池的实现
字符串常量池在JVM 内部中使用HashTable 的数据结构实现,和平常使用的HashTable 不同的是,JVM 内部这个HashTable 是不可动态扩容的。HashTable 结构示意图如下。
HashTable 的通过数组+拉链法的方式实现,由于HashTable 不可动态扩容,所以存在数组单个bucket存储的链表长度过长进而导致性能降低。在Java 6中,单个bucket默认最大长度为1009;Java 7中这个值提高到6003;也可以通过 -XX:StringTableSize=N 配置这个值。
虽然HashTable 不可动态扩容,但可以进行rehash。在发现散列不均匀的时候进行 rehash。
3 字符串常量池的位置
在Java 6之前,字符串常量池位于方法区中(也就是人们常说的永生代,但实际上两者并不等价,只是由于HotSpot开发者用永生代来实现方法区以方便方法区的垃圾回收,才出现把方法区称为永生代)。可以通过 -XX:MaxPermSize=N 来设置方法区的空间大小,如果方法区内的数据量超过了这个大小,将导致 OutOfMemoryError。
在Java 7中,字符串常量池被移出到堆中,大大增加了字符串常量池的容量上限,从而降低了OutOfMemoryError 发生的概率。在 Java 8之后,又将字符串常量池移到本地内存的元空间中。
4 字符串对象的两种实例化比较
字符串对象可以通过两种不同的方式进行实例化:①通过双引号方式实例化;②通过 new 关键字进行实例化。下面通过代码来看看两种实例化过程有何不一样。
public static void main(String[] args) {
String a = "hello, Java";
String b = "hello, Java";
String c = new String("hello, Java");
String d = new String("hello, Java");
String e = new String(a);
String f = new String("hello, Java").intern();
System.out.println(a == b);
System.out.println(c == d);
System.out.println(a == c);
System.out.println(a == e);
System.out.println(a == f);
}
以上结果为:
System.out.println(a == b); //true
System.out.println(c == d); //false
System.out.println(a == c); //false
System.out.println(a == e); //false
System.out.println(a == f); //true
出现以上的结果差异正是因为字符串常量池的使用。对于 String a = "hello, Java"; 实例化字符串对象时,先查找字符串常量池用是否有 "hello, Java" 对象,如果没有则创建对象并放回对象的引用,如果有则直接返回对象的引用。对于 String c = new String("hello, Java"); 实例化字符串时,还是先查找字符串常量池用是否有 "hello, Java" 对象,然后返回对象的引用;不同的是使用 new 关键字时,总会在堆中创建一个字符串对象,而这个对象存的是字符串常量池相同字符串值对象的引用,而引用 c 指向的是通过 new 关键字创建的字符串对象。String 的 intern() 方法则是强制使用字符串常量池,使用该方法时,通过 new 关键字创建的对象也将直接存入到字符串常量池中。