作者:蔡曉建
原文地址:
http://mccxj.github.io/blog/20130615_java-string-constant-pool.html
String印象
String是java中的無處不在的類姚糊,使用也很簡單稀蟋。初學(xué)java阔加,就已經(jīng)有字符串是不可變的蓋棺定論旭斥,解釋通常是:它是final的篡石。
不過阎毅,String是有字面量這一說法的塔橡,這是其他類型所沒有的特性(除原生類型)佳镜。另外,java中也有字符串常量池這個說法姊氓,用來存儲字符串字面量丐怯,不是在堆上,而是在方法區(qū)里邊存在的翔横。
字面量和常量池初探
字符串對象內(nèi)部是用字符數(shù)組存儲的读跷,那么看下面的例子:
String m = "hello,world";
String n = "hello,world";
String u = new String(m);
String v = new String("hello,world");
這些語句會發(fā)生什么事情? 大概是這樣的:
- 會分配一個11長度的char數(shù)組,并在常量池分配一個由這個char數(shù)組組成的字符串禾唁,然后由m去引用這個字符串效览。
- 用n去引用常量池里邊的字符串,所以和n引用的是同一個對象荡短。
- 生成一個新的字符串丐枉,但內(nèi)部的字符數(shù)組引用著m內(nèi)部的字符數(shù)組。
- 同樣會生成一個新的字符串掘托,但內(nèi)部的字符數(shù)組引用常量池里邊的字符串內(nèi)部的字符數(shù)組瘦锹,意思是和u是同樣的字符數(shù)組。
如果我們使用一個圖來表示的話闪盔,情況就大概是這樣的(使用虛線只是表示兩者其實沒什么特別的關(guān)系):
結(jié)論就是,m和n是同一個對象弯院,但m,u,v都是不同的對象,但都使用了同樣的字符數(shù)組锭沟,并且用equal判斷的話也會返回true抽兆。
我們可以使用反射修改字符數(shù)組來驗證一下效果,可以試試下面的測試代碼:
@Test
public void test1() throws Exception {
String m = "hello,world";
String n = "hello,world";
String u = new String(m);
String v = new String("hello,world");
Field f = m.getClass().getDeclaredField("value");
f.setAccessible(true);
char[] cs = (char[]) f.get(m);
cs[0] = 'H';
String p = "Hello,world";
Assert.assertEquals(p, m);
Assert.assertEquals(p, n);
Assert.assertEquals(p, u);
Assert.assertEquals(p, v);
}
從上面的例子可以看到族淮,經(jīng)常說的字符串是不可變的辫红,其實和其他的final
類還是沒什么區(qū)別,還是引用不可變的意思祝辣。 雖然String
類不開放value贴妻,但同樣是可以通過反射進(jìn)行修改,只是通常沒人這么做而已蝙斜。 即使是涉及”修改”的方法名惩,都是通過產(chǎn)生一個新的字符串對象來實現(xiàn)的,例如replace孕荠、toLower娩鹉、concat等。 這樣做的好處就是讓字符串是一個狀態(tài)不可變類稚伍,在多線程操作時沒有后顧之憂弯予。
當(dāng)然,在字符串修改的時候个曙,會產(chǎn)生一個新的對象锈嫩,如果執(zhí)行很頻繁,就會導(dǎo)致大量對象的創(chuàng)建,性能問題也就隨之而來了呼寸。 為了應(yīng)付這個問題艳汽,通常我們會采用StringBuffer
或StringBuilder
類來處理。
另外对雪,字符串常量通常是在編譯的時候就確定好的河狐,定義在類的方法區(qū)里邊,也就是說慌植,不同的類甚牲,即使用了同樣的字符串, 還是屬于不同的對象蝶柿。所以才需要通過引用字符串常量來減少相同的字符串的數(shù)量丈钙。可以通過下面的代碼來測試一下:
class A {
public void print() {
System.out.println("hello");
}
}
class B {
public void print() {
String s = "hello";
// 修改s的第一個字符為H
System.out.println("hello"); // 輸出Hello
new A().print(); // 輸出hello
}
}
字符串操作細(xì)節(jié)
String
類內(nèi)部處理有個字符數(shù)組之外交汤,還使用偏移位置offset和長度count雏赦, 通過offset和count來確定字符數(shù)組的一部分,這部分才是這個字符串的真正的內(nèi)容芙扎。 例如星岗,有substring
這個常用方法,看下面的例子:
String m = "hello,world";
String u = m.substring(2,10);
String v = u.substring(4,7);
按照上面的說法戒洼,m,n的數(shù)據(jù)結(jié)構(gòu)就如下圖所示:
可以發(fā)現(xiàn)俏橘,m,n,v是三個不同的字符串對象,但引用的value數(shù)組其實是同一個圈浇。 同樣可以通過上述反射的代碼進(jìn)行驗證寥掐,這里就不詳述了。
但字符串操作時磷蜀,可能需要修改原來的字符串?dāng)?shù)組內(nèi)容或者原數(shù)組沒法容納的時候召耘,就會使用另外一個新的數(shù)組,例如replace,concat,+等操作褐隆。另外污它,oracle的JDK實現(xiàn)中,String的構(gòu)造方法庶弃,對于字符串參數(shù)只是引用部分字符數(shù)組的情況(count小于字符數(shù)組長度)衫贬,采用的是拷貝新數(shù)組的方式,是比較特別的歇攻,不過這個構(gòu)造方法也沒什么機(jī)會使用到固惯。
例如下面的代碼:
String m = "hello,";
String u = m.concat("world");
String v = new String(m.substring(0,2));
得到的結(jié)構(gòu)圖如下:
可以發(fā)現(xiàn),m,u,v內(nèi)部的字符數(shù)組并不是同一個掉伏,有興趣可以試驗一下缝呕。
常量池中字符串的產(chǎn)生
常量池中的字符串通常是通過字面量的方式產(chǎn)生的,就像上述m語句那樣斧散。 并且他們是在編譯的時候就準(zhǔn)備好了供常,類加載的時候,順便就在常量池生成鸡捐。
可以通過javap
命令檢查一下class的字節(jié)碼栈暇,可以發(fā)現(xiàn)下面的高亮部分(以上面代碼為例):
javap -v StringTest
Compiled from "StringTest.java"
public class com.github.mccxj.StringTest extends java.lang.Object
SourceFile: "StringTest.java"
minor version: 0
major version: 50
Constant pool:
const #1 = Method #9.#28; // java/lang/Object."<init>":()V
+ const #2 = String #29; // hello,
+ const #3 = String #30; // world
...
+ const #46 = Asciz hello,;
+ const #47 = Asciz world;
...
大家不知有沒有發(fā)現(xiàn),上面的圖中箍镜,u和v的字符數(shù)組沒有被常量池里邊的字符串引用到源祈。 原因就是這些字符串(字符數(shù)組)都是運行時生成的,而常量池里邊的字符串和字符數(shù)組是完整對應(yīng)上的(count等于數(shù)組長度)色迂。
即使是字符串的內(nèi)容是一樣的香缺,都不能保證是同一個字符串?dāng)?shù)組。例如下面的代碼:
String m = "hello,world";
String u = m + ".";
String v = "hello,world.";
u和v雖然是一樣內(nèi)容的字符串歇僧,但內(nèi)部的字符數(shù)組不是同一個图张。畫成圖的話就是這樣的:
另外有一點,如果讓m聲明為final诈悍,你就會發(fā)現(xiàn)u和v變成是同一個對象祸轮。畫成圖的話就是這樣的:
這應(yīng)該怎么解釋的?這其實都是編譯器搞的鬼侥钳,因為m是final的适袜, u直接被編譯成”hello,world.”了,如果使用javap查看的話舷夺,會發(fā)現(xiàn)下面一段邏輯:
const #2 = String #25; // hello,world
const #3 = String #26; // hello,world.
...
public void test1() throws java.lang.Exception;
Code:
Stack=1, Locals=4, Args_size=1
0: ldc #2; //String hello,world
2: astore_1
3: ldc #3; //String hello,world.
5: astore_2
6: ldc #3; //String hello,world.
8: astore_3
9: return
那么苦酱,如何讓運行時產(chǎn)生的字符串放到常量池里邊呢? 可以借助String
類的intern
方法。 例如下面的用法:
String m = "hello,world";
String u = m.substring(0,2);
String v = u.intern();
上面我們已經(jīng)知道m(xù),n使用的是同一個字符數(shù)組冕房,但intern
方法會到常量池里邊去尋找字符串”he”,如果找到的話躏啰,就直接返回該字符串, 否則就在常量池里邊創(chuàng)建一個并返回耙册,所以v使用的字符數(shù)組和m,n不是同一個给僵。畫成圖的話就是這樣的:
字符串的內(nèi)存釋放問題
像字面量字符串,因為存放在常量池里邊详拙,被常量池引用著帝际,是沒法被GC的。例如下面的代碼:
String m = "hello,world";
String n = m.substring(0,2);
m = null;
n = null;
經(jīng)過上述的操作饶辙,畫成圖的話就是這樣的:
而經(jīng)過上面的分析蹲诀,我們知道像substring
、split
等方法得到的結(jié)果都是引用原字符數(shù)組的弃揽。 如果某字符串很大脯爪,而且不是在常量池里存在的则北,當(dāng)你采用substring
等方法拿到一小部分新字符串之后,長期保存的話(例如用于緩存等)痕慢, 會造成原來的大字符數(shù)組意外無法被GC的問題尚揣。
關(guān)于這個問題,常見的解決辦法就是使用new String(String original)
或java.io.StreamTokenizer
類掖举。并且在網(wǎng)上已經(jīng)有比較廣泛的討論快骗,大家可以去閱讀一下:
結(jié)論
- 任何時候,比較字符串內(nèi)容都應(yīng)該使用
equals
方法塔次; - 修改字符串操作方篮,應(yīng)該使用
StringBuffer
,StringBuilder
励负; - 可以使用
intern
方法讓運行時產(chǎn)生字符串的復(fù)用常量池中的字符串藕溅; - 字符串操作可能會復(fù)用原字符數(shù)組,在某些情況可能造成內(nèi)存泄露的問題继榆。