Java中使用的常量池,是JVM提供的缓存机制。
一. 如何将String对象存储到常量池中:
- 直接使用双引号声明出来的
String
对象会直接存储在常量池中。 - 使用intern方法,若常量池中不存在,则置入。
- 利用编译器的自动优化(eg. String str = “Hey” + “Man” //将HeyMan置入String pool);
二. String常量池的底层实现
String Pool是一个固定大小的Hashtable,默认长度为1009,若Hash冲突严重,导致链表过长,性能大幅下降,且不可自动扩容(使用jni调用C++的 StringTable::intern 方法)。
JDK7+可通过配置-XX: StringTableSize=, 设置初始长度。
三. Perm Space的变更
-
从一个经典面试题入手:
String s = new String("abc")
创建了几个对象?分别创建了两个对象,第一个对象“abc”存储在常量池中,第二个对象在Java Heap中的String对象。 -
JDK7后将字符串常量池从Perm区移到heap区域,导致了intern方法,发生巨大变化,如果存在堆中的对象,会直接保存对象的引用,而不会重新创建对象。
-
从以下例子,可以清晰看出变化。
/** * 字符串常量池在JDK6/7中的区别 * output: * JDK6: false; * JDK7: true; */ public void stringPoolDiff() { // 等价于 String Hello = "Hello"; String world = "World"; String s1 = Hello + world; // 会被编译成 s1 = new StringBuilder().append(new String("Hello")).append(new String()).toString(); String s1 = new String("Hello") + new String("World"); s1.intern(); //jdk7 string pool中存储s1的引用 String s2 = "HelloWorld"; System.out.println(s1 == s2); }
-
可以从图中看出,JDK7中,intern()方法,往String Pool存储的是一个堆中对象的引用。
四. String中易陷入的坑(JDK7中的内存模型)
- new String(“Hello”),创建两个对象,分别存在String Pool和Heap中,intern()时,如果String Pool中已存在,则返回该引用。
/**
* 字符串常量池与堆空间
* output:
* s1 storeAdr: 73c6c3b2
* s2 storeAdr: 48533e64
* s1_stringPool store address: 48533e64
*/
@Test
public void poolAndHeapStore() {
// string pool未存在“Hello”,先将“Hello”置入String Pool,再进行对象创建
String s1 = new String("Hello");
System.out.println("s1 storeAdr: " + Integer.toHexString(System.identityHashCode(s1)));
String s2 = "Hello"; // String Pool已存在“Hello”,直接引用
System.out.println("s2 storeAdr: " + Integer.toHexString(System.identityHashCode(s2)));
String s1_stringPool = s1.intern();
System.out.println("s1_stringPool store address: " + Integer.toHexString(System.identityHashCode(s1_stringPool)));
}
- 编译器的自动优化
/**
* 编译器自动优化字符字面量
* output:
* true
* true
* false
*/
@Test
public void compileAutoOptimize() {
// 编译器自动优化,“Hello”和“World”不存入String Pool,直接置入“HelloWorld”
String s1 = "Hello" + "World";
String s2 = "HelloWorld";
System.out.println(s1 == s2);
// 验证"Hello"并未置入String Pool
String s3 = new String("Hel") + new String("lo");
s3.intern();
String s4 = "Hello";
System.out.println(s3 == s4);
// 只有字符常量拼接,编译器才进行优化
String s5 = "Hello".concat("World");
System.out.println(s2 == s5);
}
- String Pool只在编译期放入
/**
* 常量字符串编译期才放入string pool
* output:
* false
* true
*/
@Test
public void poolAndHeapStore6() {
// 常量字符串在编译期才放入string pool,s2通过s1而得到,属于运行时赋值,存于堆中;
String s1 = "Hello";
String s2 = s1 + "";
String s3 = "Hello" + "";
System.out.println(s1 == s2);
System.out.println(s1 == s3);
}
五. String的不可变(immutable)性的探究:
在《Core Java》中对不可变字符串的描述是这样:“不可变字符串却有一个优点:编译器可以让字符串共享。Java的设计者认为共享带来的高效率远远胜过于提取、拼接字符串所带来的低效率。”
关于String的不可变性,是由于API中未对外提供相应的更改方法,使value的值恒定不可变。但我们可通过Java的动态反射机制,打破这种不可变性。
/**
* 通过动态反射改变String的value值
*
* @throws NoSuchFieldException
* @throws IllegalAccessException
*/
private static void modifyStringValue() throws NoSuchFieldException, IllegalAccessException {
String oriStr = new String("Hello");
System.out.println("oriStr store address: " + Integer.toHexString(System.identityHashCode(oriStr)));
System.out.println(oriStr);
Field field = String.class.getDeclaredField("value");
field.setAccessible(true);
field.set(oriStr, "World".toCharArray());
System.out.println("newStr store address: " + Integer.toHexString(System.identityHashCode(oriStr)));
System.out.println(oriStr);
}
祝近安
ooooor