轉帖地址:http://congmo.github.com/blog/2012/02/16/1-translationofstring/(蔥末)
規(guī)范化的字符串可節(jié)省空間,代價就是花費更多的CPU時間來檢測存儲在RAM中的字符串和替換字符串副本。規(guī)范化之后的字符串,不論存在多少引用,僅在RAM中存在一份。由于String是不可變的,所以比如兩個不同的方法碰巧使用同一個字符串,那么它們就可以使用同一個String的副本。不論字符串的意思在不同的語境下是否相同,依舊可以共享,就好比sin可以是人名,當然也是三角函數中的函數名。共享重復字符串的過程就叫做規(guī)范化。String.intern()返回規(guī)范化的主字符串的引用。規(guī)范化之后的字符串可以直接使用==(比較引用)來比較兩個字符串是否相等。由于String不可變的特性,可以節(jié)省很多空間。比如”pot”在”hippopotamus”中出現,那么就不需要創(chuàng)建新的字符串,額外分配新的空間(堆中的空間),返回一個相應指向”hippopotamus”的引用即可。
為何要規(guī)范化
規(guī)范化字符串有如下兩個原因:
就比如,將文件中用逗號分隔的20000人的黨籍讀入HashMap,那么內存中就需要20000左右個字符串用來記錄這些黨籍。如果將字符串進行規(guī)范化,那么幾十個就足矣。
規(guī)范化與String.substring()
使用String.substring()時,JVM只在棧中分配一個String類型的引用,指向原始字符串的字面值。substring不需要額外分配空間,也不需要拷貝字符。String.substring不會對結果進行規(guī)范化。
注意:只要還有”活動”的子串指向原始字符串,那么垃圾回收期就沒法回收它。
String.subString產生的空串也不用自動規(guī)范化,因此,空串也會導致長長的原始字符串沒法被回收。
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ī)范化,那么程序運行時產生的字符串就不能被規(guī)范化。這樣比較惡心的一點是在大多數情況下程序可以正常運行,但是在特殊的情況下就會出錯。就比如,使用==替代equals來比較兩個字符串是否相等,絕大多數是可行的,因為字符串會被規(guī)范化,但是不排除特例,比如運行期間產生的字符串。
規(guī)范化與new String( String )
新手喜歡用String s = new String( “hello” );代替String s = “hello”;
這與規(guī)范化正好相反,這樣創(chuàng)建了一個全新的”hello”字符串,雖然有相同的字面值,但是不會被規(guī)范化。在兩種場景下適合使用new來創(chuàng)建字符串:
使用new String( String )就一定會創(chuàng)建一個全新的字符串嗎?答案是肯定的。你可能以為JVM很智能,會將新創(chuàng)建的字符串規(guī)范化,然后返回指向母串的引用。但是語言規(guī)范中指出,new String()一定會創(chuàng)建一個全新的字符串,盡管JVM在理論上可以同String.substring(0)和String.intern.substring(0)一樣進行規(guī)范化,防止出現多個拷貝。
這就引申出另外一個問題,s == s.substring(0)總是返回false嗎?答案也是肯定的。
還有一個適合用new來創(chuàng)建字符串的地方,如下:
1 String password = new String( jpassword.getPassWord() );
getPassWord方法返回一個字符數組(char),這么做并不愚蠢,在高安全性的場景下,就可以將char數組清空。
看下這段代碼:String s = new String( “Hello” ); 當變量s所在的類被加載的時候,字面值”Hello”會被規(guī)范化,但是愚蠢的使用new String,會在堆中重新創(chuàng)建一份字面值”Hello”,地址與規(guī)范化后的”Hello”不同。在Sun的JVM中,規(guī)范化的字符串被存儲在一個叫perm gen的特殊RAM區(qū)域,這個區(qū)域中JVM也加載類和存儲本地編譯后的代碼,而且規(guī)范化后的字符串與存儲在堆中的普通對象一樣。如果這樣寫:String s = “Hello”,就不會重新創(chuàng)建”Hello”的副本,而是直接指向規(guī)范化后的字符串。
規(guī)范化與垃圾回收
在JDK的早起版本中,由于JVM要持有存儲規(guī)范化后字符串的HashTable的引用,以便檢查新創(chuàng)建的字符串是否已在共享池中存在,這樣就導致了規(guī)范化后的字符串沒法被垃圾回收器回收。隨著1.2版本中引入了弱引用之后,無用的規(guī)范化字符串就可以被回收了。
JDK1.2版本之后,規(guī)范化字符串在沒用引用指向它時,可以被回收,而且規(guī)范化不是只發(fā)生在編譯期。這樣以編碼的方式重新創(chuàng)建、規(guī)范化字符串時,新創(chuàng)建的字符串對象會變成唯一的原始字符串。這樣做不會帶來實際的問題,使用==來比較兩個字符串包含的字符是否相等同樣奏效。(這里理解的不是很好,我覺得應該是這樣的:同一個字面值規(guī)范化后,之前的那個字面值的地址會被新地址替換掉)
溢出
java.lang.OutOfMemoryError: String intern table overflow 表示規(guī)范化字符串太多。一些低版本的JVM規(guī)定規(guī)范化字符串不能超過64K(大約50000個)。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ī)范化會”不利”垃圾回收。
底層
這里只講底層規(guī)范化如何起作用的最簡單形式。JVM內部在堆中存儲對象,包括規(guī)范化與普通的String對象(這個說法貌似不是很嚴謹)。而且規(guī)范化的String被放在一個”弱”HashMap中。
HashMap中的String集合,也叫字符串常量池。他們和堆中其他普通對象沒什么兩樣,只是因為經過優(yōu)化后,生存期要長一些。String對象在堆中,而指向它的引用存在HashMap中,所以規(guī)范化字符串有自己的共享池。
當字符串被規(guī)范化時,先在HashMap中檢查是否已存在,如果存在則返回指向主字符串的引用,通常這個引用優(yōu)先自身的引用,而自身的副本就很快被垃圾回收器回收。如果沒有,則將其引用添加到HashMap中,并注冊為主字符串。規(guī)范化的過程不會再生成字符串的副本,只是持有主字符串的唯一引用。
規(guī)范化與非規(guī)范化的字符串都存儲在堆中。由于規(guī)范化時產生的是弱引用,所以當除了HashMap中的弱引用再無其他引用指向主字符串時,該主字符串就可以被回收了。
new String時,不會自動規(guī)范化,因此在堆中會有同一個字符串的多個副本。隨后調用該字符串的intern方法,這些副本也不會被清除。
我總感覺這里貌似有問題,干脆不翻譯了,把原文貼上吧。
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.
手動規(guī)范化
規(guī)范化最大的問題就是知道程序結束才能銷毀占用RAM的空間,盡管再沒有引用指向主字符串,也不能被垃圾回收器回收(早期版本)。如果想使用一個臨時的規(guī)范化字符串,可以使用手動規(guī)范化。
然而,現在主流的JVM中的規(guī)范化字符串共享池都是采用弱引用實現的,所以只要沒有強引用指向主字符串,則可被垃圾回收器回收。你可以像JVM一樣,自己實現規(guī)范化的過程。
比如假設從文件中讀取以逗號分隔的人名與地址,并以某種順序存入集合,由于很多人居住在同一城市,所以RAM中就會充滿了同一個城市的副本。
那么創(chuàng)建一個HashMap(不是HashSet),用于存儲每個城市名稱的主字符串。每次獲取城市時,先從HashMap中查找,如果存在則用主字符串的引用替換自身的引用。自身String對象的副本很快就會被垃圾回收器回收。如果不存在增加城市到HashMap。
當讀完城市后,就可以講HashMap拋棄,而放入到HashMap中的主字符串,除了沒有其他引用指向的主字符串被垃圾回收器回收掉之外,還是一樣存在,一樣擁有唯一的引用,而且與規(guī)范化后的字符串一樣。
原文地址:http://mindprod.com/jgloss/interned.html
這里就算翻譯完了,不過有些地方覺得怪怪的。還有由于個人水平實在是有限,難免有地方粗糙。另外,如果你說研究這個實在是沒有意思類似的話,那拜托你憋在心里吧,謝謝了。
因為在看jdk源碼,看到String中最后一行的intern是個native方法,于是就到處查資料,還在OSChina上提出一個問題:http://www.oschina.net/question/129471_38493,討論中就提到了撒加在javaeye上的一個帖子的回答: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 }
輸出結果:true
1 public static void main(String[] args) {
2 String str0 = args[0];
3 System.out.println(str0.intern() == str0);
4 }
同樣在命令行輸入:congmo.github.com
輸出結果:
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 }
輸出結果:
true
false
true
從前面兩段代碼中可以看出,使用命令行的方式同樣輸入參數”congmo.github.com”,將args0賦值給str0,然后str0.intern()==str0的結果竟然是false,難道真如javaeye那篇帖子中有人懷疑的那樣,JVM將args0提前就規(guī)范化了?按道理應該不會啊。
測試環(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 }
輸出結果: true
1 public static void main(String[] args) {
2 String str0 = new String( args[0] );
3 System.out.println(str0.intern() == str0);
4 }
輸出結果: 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 }
輸出結果:
true
false
true
但是從1.7版本執(zhí)行的結果看來,貌似可以確定JVM沒有對args0規(guī)范化,但是從javaeye帖子討論中可以知曉1.7版本后perm gen這個內存區(qū)被干掉了,所以規(guī)范化之后的字符串也存儲在堆中,所以無論args0有沒有提前被規(guī)范化,str0始終都會指向堆中那個引用。按照我的理解,如下圖所示:

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


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