轉(zhuǎn)帖地址:http://congmo.github.com/blog/2012/02/16/1-translationofstring/(蔥末)
規(guī)范化的字符串可節(jié)省空間,代價(jià)就是花費(fèi)更多的CPU時(shí)間來檢測存儲(chǔ)在RAM中的字符串和替換字符串副本。規(guī)范化之后的字符串,不論存在多少引用,僅在RAM中存在一份。由于String是不可變的,所以比如兩個(gè)不同的方法碰巧使用同一個(gè)字符串,那么它們就可以使用同一個(gè)String的副本。不論字符串的意思在不同的語境下是否相同,依舊可以共享,就好比sin可以是人名,當(dāng)然也是三角函數(shù)中的函數(shù)名。共享重復(fù)字符串的過程就叫做規(guī)范化。String.intern()返回規(guī)范化的主字符串的引用。規(guī)范化之后的字符串可以直接使用==(比較引用)來比較兩個(gè)字符串是否相等。由于String不可變的特性,可以節(jié)省很多空間。比如”pot”在”hippopotamus”中出現(xiàn),那么就不需要?jiǎng)?chuàng)建新的字符串,額外分配新的空間(堆中的空間),返回一個(gè)相應(yīng)指向”hippopotamus”的引用即可。
為何要規(guī)范化
規(guī)范化字符串有如下兩個(gè)原因:
就比如,將文件中用逗號(hào)分隔的20000人的黨籍讀入HashMap,那么內(nèi)存中就需要20000左右個(gè)字符串用來記錄這些黨籍。如果將字符串進(jìn)行規(guī)范化,那么幾十個(gè)就足矣。
規(guī)范化與String.substring()
使用String.substring()時(shí),JVM只在棧中分配一個(gè)String類型的引用,指向原始字符串的字面值。substring不需要額外分配空間,也不需要拷貝字符。String.substring不會(huì)對(duì)結(jié)果進(jìn)行規(guī)范化。
注意:只要還有”活動(dòng)”的子串指向原始字符串,那么垃圾回收期就沒法回收它。
String.subString產(chǎn)生的空串也不用自動(dòng)規(guī)范化,因此,空串也會(huì)導(dǎo)致長長的原始字符串沒法被回收。
1 public static void main(String args[]){
2 String s = "a very long string";
3 // create an empty substring
4 String e1 = s.substring( 0,0);
5 // make sure the empty string is canonical
6 String e2 = ( e1.length() ==0) ? "" :e1;
7 System.out.println( e1=="" );// always prints false
8 System.out.println( e2=="" );// always prints true
9 }
規(guī)范化與void字符串
想要避免空串致使原始字符串不能被回收,就不要使用任何void字符串指向原始字符串。void字符串有3中:””,” “,null。
1 public final static String possiblyEmpty( StringpString)
2 {
3 if( pString==null) return"";
4 pString=pString.trim();
5 if( pString.length() ==0) return"";
6 returnpString;
7 }
8 public final staticStringpossiblyNull( StringpString)
9 {
10 if( pString==null) returnnull;
11 pString=pString.trim();
12 if( pString.length() ==0) returnnull;
13 returnpString;
14 }
15 public final staticStringneverNull( StringpString)
16 {
17 /* if pString is null, Java will throw an NullPointerException */
18 pString=pString.trim();
19 /* if pString is empty or blank, throw a NullPointerException */
20 if( pString.length() ==0) throw newNullPointerException();
21 returnpString;
22 }
規(guī)范化疑難雜癥
所有字符串在編譯期間被規(guī)范化,那么程序運(yùn)行時(shí)產(chǎn)生的字符串就不能被規(guī)范化。這樣比較惡心的一點(diǎn)是在大多數(shù)情況下程序可以正常運(yùn)行,但是在特殊的情況下就會(huì)出錯(cuò)。就比如,使用==替代equals來比較兩個(gè)字符串是否相等,絕大多數(shù)是可行的,因?yàn)樽址畷?huì)被規(guī)范化,但是不排除特例,比如運(yùn)行期間產(chǎn)生的字符串。
規(guī)范化與new String( String )
新手喜歡用String s = new String( “hello” );代替String s = “hello”;
這與規(guī)范化正好相反,這樣創(chuàng)建了一個(gè)全新的”hello”字符串,雖然有相同的字面值,但是不會(huì)被規(guī)范化。在兩種場景下適合使用new來創(chuàng)建字符串:
使用new String( String )就一定會(huì)創(chuàng)建一個(gè)全新的字符串嗎?答案是肯定的。你可能以為JVM很智能,會(huì)將新創(chuàng)建的字符串規(guī)范化,然后返回指向母串的引用。但是語言規(guī)范中指出,new String()一定會(huì)創(chuàng)建一個(gè)全新的字符串,盡管JVM在理論上可以同String.substring(0)和String.intern.substring(0)一樣進(jìn)行規(guī)范化,防止出現(xiàn)多個(gè)拷貝。
這就引申出另外一個(gè)問題,s == s.substring(0)總是返回false嗎?答案也是肯定的。
還有一個(gè)適合用new來創(chuàng)建字符串的地方,如下:
1 String password = new String( jpassword.getPassWord() );
getPassWord方法返回一個(gè)字符數(shù)組(char),這么做并不愚蠢,在高安全性的場景下,就可以將char數(shù)組清空。
看下這段代碼:String s = new String( “Hello” ); 當(dāng)變量s所在的類被加載的時(shí)候,字面值”Hello”會(huì)被規(guī)范化,但是愚蠢的使用new String,會(huì)在堆中重新創(chuàng)建一份字面值”Hello”,地址與規(guī)范化后的”Hello”不同。在Sun的JVM中,規(guī)范化的字符串被存儲(chǔ)在一個(gè)叫perm gen的特殊RAM區(qū)域,這個(gè)區(qū)域中JVM也加載類和存儲(chǔ)本地編譯后的代碼,而且規(guī)范化后的字符串與存儲(chǔ)在堆中的普通對(duì)象一樣。如果這樣寫:String s = “Hello”,就不會(huì)重新創(chuàng)建”Hello”的副本,而是直接指向規(guī)范化后的字符串。
規(guī)范化與垃圾回收
在JDK的早起版本中,由于JVM要持有存儲(chǔ)規(guī)范化后字符串的HashTable的引用,以便檢查新創(chuàng)建的字符串是否已在共享池中存在,這樣就導(dǎo)致了規(guī)范化后的字符串沒法被垃圾回收器回收。隨著1.2版本中引入了弱引用之后,無用的規(guī)范化字符串就可以被回收了。
JDK1.2版本之后,規(guī)范化字符串在沒用引用指向它時(shí),可以被回收,而且規(guī)范化不是只發(fā)生在編譯期。這樣以編碼的方式重新創(chuàng)建、規(guī)范化字符串時(shí),新創(chuàng)建的字符串對(duì)象會(huì)變成唯一的原始字符串。這樣做不會(huì)帶來實(shí)際的問題,使用==來比較兩個(gè)字符串包含的字符是否相等同樣奏效。(這里理解的不是很好,我覺得應(yīng)該是這樣的:同一個(gè)字面值規(guī)范化后,之前的那個(gè)字面值的地址會(huì)被新地址替換掉)
溢出
java.lang.OutOfMemoryError: String intern table overflow 表示規(guī)范化字符串太多。一些低版本的JVM規(guī)定規(guī)范化字符串不能超過64K(大約50000個(gè))。IBM的Java1.1.8 JRE就有這樣的限制。它是Error,不是Exception,如果想捕獲它,可以這樣做:
1 public class InternTest
2 {
3 public static final intn=80000;
4 public static void main( String[] args)
5 {
6 String[] hold = new String[n];
7 // build list of interned strings
8 for( inti=0;i<n;i++)
9 {
10 try
11 {
12 hold[i] =Integer.toString(i).intern();
13 }
14 catch( Throwablee)
15 {
16 System.out.println( "intern exploded at " +i);
17 System.exit( 1);
18 }
19 }
20 // make sure they were really interned.
21 for( inti=0;i<n;i++)
22 {
23 if( hold[i] !=Integer.toString(i).intern() )
24 {
25 System.out.println( "intern failed at " +i);
26 System.exit( 1);
27 }
28 }
29 System.out.println( "intern good for at least " +n+" Strings." );
30 }
依舊要注意規(guī)范化會(huì)”不利”垃圾回收。
底層
這里只講底層規(guī)范化如何起作用的最簡單形式。JVM內(nèi)部在堆中存儲(chǔ)對(duì)象,包括規(guī)范化與普通的String對(duì)象(這個(gè)說法貌似不是很嚴(yán)謹(jǐn))。而且規(guī)范化的String被放在一個(gè)”弱”HashMap中。
HashMap中的String集合,也叫字符串常量池。他們和堆中其他普通對(duì)象沒什么兩樣,只是因?yàn)榻?jīng)過優(yōu)化后,生存期要長一些。String對(duì)象在堆中,而指向它的引用存在HashMap中,所以規(guī)范化字符串有自己的共享池。
當(dāng)字符串被規(guī)范化時(shí),先在HashMap中檢查是否已存在,如果存在則返回指向主字符串的引用,通常這個(gè)引用優(yōu)先自身的引用,而自身的副本就很快被垃圾回收器回收。如果沒有,則將其引用添加到HashMap中,并注冊(cè)為主字符串。規(guī)范化的過程不會(huì)再生成字符串的副本,只是持有主字符串的唯一引用。
規(guī)范化與非規(guī)范化的字符串都存儲(chǔ)在堆中。由于規(guī)范化時(shí)產(chǎn)生的是弱引用,所以當(dāng)除了HashMap中的弱引用再無其他引用指向主字符串時(shí),該主字符串就可以被回收了。
new String時(shí),不會(huì)自動(dòng)規(guī)范化,因此在堆中會(huì)有同一個(gè)字符串的多個(gè)副本。隨后調(diào)用該字符串的intern方法,這些副本也不會(huì)被清除。
我總感覺這里貌似有問題,干脆不翻譯了,把原文貼上吧。
This is a simplified version of how interning works under the hood. Inside the JVM is the heap where all allocated Objects reside. This includes Strings both interned and ordinary. (In Sun’s JVM, the interned Strings (which includes String literals) are stored in a special pool of RAM called the perm gen, where the JVM also loads classes and stores natively compiled code. However, the intered Strings behave no differently than had they been stored in the ordinary object heap.) In addition, interned Strings are registered in a weak HashMap.The collection of Strings registered in this HashMap is sometimes called the String pool. However, they are ordinary Objects and live on the heap just like any other (perhaps in an optimised way since interned Strings tend to be long lived). The String Object lives on the heap and a reference to it lives in the HashMap. There is so separate pool of interned String objects. Whenever a String is interned, it is looked up in the HashMap to see if it exists already. If so the user gets passed a reference to the master copy. Normally he will use that copy in preference to his. His duplicate copy then will likely soon have no references to it and will be eventually garbage collected. If the String has never been seen before, a reference to it will be added to the HashMap and intern will hand him a reference to his own String, now registered as the unique master. Note that the intern process does not make a copy of the String, it just keeps a reference to the unique master copies. All the Strings, interned and ordinary live on the heap. When there are no references left to a String except the intern HashMap registry reference, it will be garbage collected since intern keeps only a weak reference to it. When you say new String, it is not automatically interned. Thus there may then be duplicates on the heap. If you later use intern on that String, those duplicates won’t be cleaned up. Only when you intern all copies of a String, and discard references to the uninterned versions do you maintain but a single copy.
手動(dòng)規(guī)范化
規(guī)范化最大的問題就是知道程序結(jié)束才能銷毀占用RAM的空間,盡管再?zèng)]有引用指向主字符串,也不能被垃圾回收器回收(早期版本)。如果想使用一個(gè)臨時(shí)的規(guī)范化字符串,可以使用手動(dòng)規(guī)范化。
然而,現(xiàn)在主流的JVM中的規(guī)范化字符串共享池都是采用弱引用實(shí)現(xiàn)的,所以只要沒有強(qiáng)引用指向主字符串,則可被垃圾回收器回收。你可以像JVM一樣,自己實(shí)現(xiàn)規(guī)范化的過程。
比如假設(shè)從文件中讀取以逗號(hào)分隔的人名與地址,并以某種順序存入集合,由于很多人居住在同一城市,所以RAM中就會(huì)充滿了同一個(gè)城市的副本。
那么創(chuàng)建一個(gè)HashMap(不是HashSet),用于存儲(chǔ)每個(gè)城市名稱的主字符串。每次獲取城市時(shí),先從HashMap中查找,如果存在則用主字符串的引用替換自身的引用。自身String對(duì)象的副本很快就會(huì)被垃圾回收器回收。如果不存在增加城市到HashMap。
當(dāng)讀完城市后,就可以講HashMap拋棄,而放入到HashMap中的主字符串,除了沒有其他引用指向的主字符串被垃圾回收器回收掉之外,還是一樣存在,一樣擁有唯一的引用,而且與規(guī)范化后的字符串一樣。
原文地址:http://mindprod.com/jgloss/interned.html
這里就算翻譯完了,不過有些地方覺得怪怪的。還有由于個(gè)人水平實(shí)在是有限,難免有地方粗糙。另外,如果你說研究這個(gè)實(shí)在是沒有意思類似的話,那拜托你憋在心里吧,謝謝了。
因?yàn)樵诳磈dk源碼,看到String中最后一行的intern是個(gè)native方法,于是就到處查資料,還在OSChina上提出一個(gè)問題:http://www.oschina.net/question/129471_38493,討論中就提到了撒加在javaeye上的一個(gè)帖子的回答:http://www.iteye.com/topic/1112592,于是我做了如下的測試,又畫了3張圖。
測試環(huán)境:
java version "1.6.0_17" Java(TM) SE Runtime Environment (build 1.6.0_17-b04) Java HotSpot(TM) Client VM (build 14.3-b01, mixed mode, sharing)
1 public static void main(String[] args) {
2 String str0 = "congmo.github.com";
3 System.out.println(str0.intern() == str0);
4 }
輸出結(jié)果:true
1 public static void main(String[] args) {
2 String str0 = args[0];
3 System.out.println(str0.intern() == str0);
4 }
同樣在命令行輸入:congmo.github.com
輸出結(jié)果:
false
1 public static void main(String[] args) {
2
3 String str0 = "congmo.github.com";
4 System.out.println(str0.intern() == str0);
5
6 String str1 = new String( args[0] );
7 System.out.println(str1.intern() == str1);
8
9 System.out.println(str0 == str1.intern());
10 }
輸出結(jié)果:
true
false
true
從前面兩段代碼中可以看出,使用命令行的方式同樣輸入?yún)?shù)”congmo.github.com”,將args0賦值給str0,然后str0.intern()==str0的結(jié)果竟然是false,難道真如javaeye那篇帖子中有人懷疑的那樣,JVM將args0提前就規(guī)范化了?按道理應(yīng)該不會(huì)啊。
測試環(huán)境:
java version "1.7.0_02" Java(TM) SE Runtime Environment (build 1.7.0_02-b13) Java HotSpot(TM) Client VM (build 22.0-b10, mixed mode, sharing)
1 public static void main(String[] args) {
2 String str0 = args[0];
3 System.out.println(str0.intern() == str0);
4 }
輸出結(jié)果: true
1 public static void main(String[] args) {
2 String str0 = new String( args[0] );
3 System.out.println(str0.intern() == str0);
4 }
輸出結(jié)果: true
1 public static void main(String[] args) {
2 String str0 = args[0];
3 System.out.println(str0.intern() == str0);
4 String str = new String( args[0] );
5 System.out.println(str.intern() == str);
6 System.out.println(str.intern() == str0);
7 }
輸出結(jié)果:
true
false
true
但是從1.7版本執(zhí)行的結(jié)果看來,貌似可以確定JVM沒有對(duì)args0規(guī)范化,但是從javaeye帖子討論中可以知曉1.7版本后perm gen這個(gè)內(nèi)存區(qū)被干掉了,所以規(guī)范化之后的字符串也存儲(chǔ)在堆中,所以無論args0有沒有提前被規(guī)范化,str0始終都會(huì)指向堆中那個(gè)引用。按照我的理解,如下圖所示:

所以現(xiàn)在看來還是個(gè)未知數(shù)。
另外,我按照自己的理解針對(duì)3中情況畫了3張圖,都是用于說明JVM的內(nèi)存分配的。 注:jdk1.6或之前版本,1.7之后方法區(qū)被砍掉。


這兩張圖都是描述使用new創(chuàng)建String,然后再調(diào)用自身的intern方法后內(nèi)存以及引用的變化。