String 對象是咱們使用最頻繁的一個對象類型,但它的性能問題倒是最容易被忽略的。String 對象做爲 Java 語言中重要的數據類型,是內存中佔用空間最大的一個對象,高效地使用字符串,能夠提高系統的總體性能,好比百M內存輕鬆存儲幾十G數據。git
若是不正確對待 String 對象,則可能致使一些問題的發生,好比由於使用了正則表達式對字符串進行匹配,從而致使併發瓶頸。正則表達式
接下來咱們就從 String 對象的實現、特性以及實際使用中的優化三方面入手,深刻了解。編程
在開始以前,先思考一個問題:經過三種不一樣的方式建立了三個對象,再依次兩兩匹配,每組被匹配的兩個對象是否相等?數組
String str1 = "abc"; String str2 = new String("abc"); String str3 = str2.intern(); System.out.println(str1 == str2); System.out.println(str2 == str3); System.out.println(str1 == str3);
對於上面的問題,你能夠先思考下答案,以及這樣思考的緣由。緩存
如今咱們回到正題來:String 對象是如何實現的?安全
在Java語言中,Sun 公司的工程師們對String對象作了大量的優化,來節約內存空間,提高 String 對象在系統中的性能。以下圖: 性能優化
1.在 Java6 以及以前的版本中,String 對象是對 char 數組進行了封裝實現的對象,主要有4個成員變量: char 數組、偏移量 offset、字符數量 count、哈希值 hash。服務器
String 對象是經過 offset 和 count 兩個屬性來定位 char[] 數組,獲取字符串。這麼作能夠高效、快速地共享數組對象,同時節省內存空間,但這種方式頗有可能會致使內存泄漏。多線程
2.從 Java7 版本開始到 Java8 版本,Java 對 String 類作了一些改變。String 類中再也不有 offset 和 count 兩個變量了。這樣的好處是 String 對象佔用的內存稍微少了些,同時 String.substring 方法也再也不共享 char[],從而解決了使用該方法可能致使的內存泄露問題。併發
3.從 Java9 版本開始,工程師將 char[] 字段改成了 byte[] 字段,又維護了一個新的屬性 coder,它是一個編碼格式的標識。
工程師爲何這樣修改呢?
咱們知道一個 char 字符佔16位,2個字節。這個狀況下,存儲單字節編碼內的字符(佔一個字節的字符)就顯得很是浪費。JDK1.9 的 String 類爲了節約內存空間,因而使用了佔8位,1個字節的 byte 數組來存放字符串。
而新屬性 coder 的做用是,在計算字符串長度或者使用 indexOf() 函數時,咱們須要根據這個字段,判斷如何計算字符串長度。coder 屬性默認有 0 和 1 兩個值,0 表明 Latin-1(單字節編碼),1 表明 UTF-16。若是 String 判斷字符串只包含了 latin-1,而 coder 屬性值爲 0, 反之則爲 1。
在實現代碼中 String 類被 final 關鍵字修飾了,並且變量 char 數組也被 final修飾了。咱們知道類被 final 修飾表明該類不可繼承,而 char[] 被 final+private 修飾,表明了 String 對象不可被更改。Java實現的這個特性叫做 String 對象的不可變性,即 String 對象一旦建立成功,就不能再對它進行改變。
Java 這樣作的好處在哪裏呢?
1)保證 String對象的安全性。假設 String 對象是可變的,那麼 String 對象將可能被惡意修改。
2)保證 hash 屬性值不會頻繁變動,確保了惟一性,使得類型 HashMap 容器才能實現相應的 key-value 緩存功能。
3)能夠實現字符串常量池。在 Java 中,一般有兩種建立字符串對象的方式,一種是經過字符串常量的方式建立,如 String str = "abc";另外一種是字符串變量經過 new 形式的建立,如 String str = new String("abc")。
當代碼中使用第一種方式建立字符串對象時,JVM 首先會檢查該對象是否在字符串常量池中,若是在,就返回該對象引用,不然新的字符串將在常量池中被建立。這種方式能夠減小同一個值的字符串對象的重複建立,節約內存。
String str = new String("abc")這種方式,首先在編譯類文件時,「abc」常量字符串將會放入到常量結構中,在類加載時,「abc」將會在常量池中建立;其次,在調用 new 時,JVM 命令將會調用 String 的構造函數,同時引用常量池中的 「abc」 字符串,在堆內存中建立一個 String 對象;最後, str 將引用 String 對象。
說到這裏,將講述一個特殊例子:日常編程時,對一個 String 對象 str 賦值 」hello「,而後又讓 str 賦值爲 」world「,這個時候 str 的值變成了 」world「,那麼 str 值確實改變了,爲何還說 String 對象不可變呢?
在這裏要說明對象和對象引用的區別,在 Java 中要比較兩個對象是否相等,每每要用 == ,而要判斷兩個對象的值是否相等,則須要用 equals 方法來判斷。
上面的 str 只是 String 對象的引用,並非對象自己。對象在內存中是有一塊內存地址,str 則是一個指向該內存的引用。因此在前面例子中,第一次賦值的時候,建立了一個 」hello「對象, str 引用指向 」hello「 地址;第二次賦值的時候,又從新建立了一個對象 」world「,str 引用指向了 」world「,但 「hello」 對象依然存在於內存中。
也就是說 str 並非對象,而只是一個對象引用。真正的對象依然在內存中,沒有被改變。
編程過程當中,字符串的拼接很常見。前面講過 String 對象是不可變的,若是使用 String 對象相加,拼接想要的字符串,是否是就會產生多個對象呢?例以下面代碼:
String str = "ab" + "cd" + "ef";
分析代碼可知:首先會生成 ab 對象,再生成 abcd 對象,最後生成 abcdef 對象,從理論上來講,這段代碼是低效的。
但實際運行中,咱們發現只有一個對象生成,這是爲何呢?咱們來看看編譯後的代碼,你會發現編譯器自動優化了這段代碼,以下:
String str = "abcdef";
上面講的是字符串常量的累計,下面看字符串變量的累計:
String str = "abcdef"; for(int i = 0; i < 100; i++){ str = str + i; }
上面的代碼編譯後,能夠看到編譯器一樣對這段代碼進行了優化,Java 在進行字符串的拼接時,偏向使用 StringBuilder,這樣能夠提升程序的效率。
String str = "abcdef"; for(int i = 0; i < 100; i++){ str = (new StringBuilder(String.valueOf(str))).append(i).toString(); }
綜上已知:即便使用 + 號做爲字符串的拼接,也同樣能夠被編譯器優化成 StringBuilder 的方式。但再細緻些,你會發如今編譯器優化的代碼中,每次循環都會生成一個新的 StringBuilder 實例,一樣也會下降系統的性能。
因此平時作字符串的拼接時,建議顯示地使用 StringBuilder 來提高系統性能。
若是在多線程編程中, String 對象的拼接涉及到線程安全,可使用 StringBuffer,可是因爲 StringBuffer 是線程安全的,涉及到鎖競爭,因此從性能上來講,要比 StringBuilder 差一些。
說完了構建字符串,接下來講下 String 對象的存儲問題。先看下面一個案例:
Twitter 每次發佈消息狀態的時候,都會產生一個地址信息,以當時 Twitter 用戶的規模預估,服務器須要 32G 的內存來存儲地址信息。
public class Location{ private String city; private String region ; private String countryCode; private double longitude; private double latitude; }
考慮到其中又不少用戶在地址信息上是有重合的,好比:國家、省份、城市等,這時能夠將這部分信息單獨列出一個類,以減小重複。
public class ShareLocation{ private String city; private String region ; private String countryCode; } public class Location{ private ShareLocation shareLocation; private double longitude; private double latitude; }
經過優化,數據存儲大小減小到了 20G 左右,但對於內存存儲這個數據來講,依然很大,怎麼辦?
這是能夠經過使用 String.intern 來節省內存空間,從而優化 String 對象的存儲。
具體作法就是:在每次賦值的時候使用 String 的 intern 方法,若是常量池有相同值,就會重複使用該對象,返回對象引用,這樣一開始的對象就能夠被回收掉。這種方式可使重複性很是高的地址信息大小從 20G 降到幾百兆。
ShareLocation shareLocation = new ShareLocation(); shareLocation.setCity(messageInfo.getCity().intern()); shareLocation.setRegion(messageInfo.getRegion().intern()); shareLocation.setCountryCode(messageInfo.getCountryCode().intern()): Location location = new Location(); location.set(shareLocation); location.set(messageInfo.getLongitude()); location.set(messageInfo.getLatitude());
爲了更好的理解,下面講述一個簡單的例子:
String a = new String("abc").intern(); String b = new String("abc").intern(); if(a == b){ System.out.println("a == b"); } 運行結果: a == b
在字符串常量池中,默認會將對象放入常量池;在字符串變量中,對象是會在堆中建立,同時也會在常量池中建立一個字符串對象,String 對象中的 char 數組將會引用常量池中的 char 數組,並返回堆內存對象引用。
若是調用 intern 方法,會去查看字符串常量池中是否有等於該對象的字符串的引用,若是沒有,在 JDK1.6 版本中去複製堆中的字符串到常量池中,並返回該字符串引用,堆內存中原有的字符串因爲沒有引用指向它,將會經過垃圾回收器回收。
在 JDK1.7 版本之後,因爲常量池合併到了堆中,因此不會再複製具體字符串了,只是會把首次遇到的字符串的引用添加到常量池中;若是有,就返回常量池的字符串引用。
如今再來看上面的例子,在一開始字符串 「abc」 會在加載類時,在常量池中建立一個字符串對象。
建立 a 變量時,調用 new String() 會在堆中建立一個 String 對象,String 對象中的 char 數組將會引用常量池中字符串,調用 intern 方法以後,會去常量池中查找是否有等於該字符串對象的引用,有就返回引用。
建立 b 變量時,調用 new String() 會在堆中建立一個 String 對象,String 對象中的 char 數組將會引用常量池中字符串,調用 intern 方法以後,會去常量池中查找是否有等於該字符串對象的引用,有就返回引用。
而在堆內存中的兩個對象,因爲沒有引用指向它,將會被垃圾回收。因此 a 和 b 引用的是同一個對象。
若是在運行時,建立字符串對象,將會直接在堆內存中建立,不會在常量池中建立。因此動態建立的字符串對象,調用 intern 方法,在 JDK1.6 版本中會去常量池中建立運行時常量以及返回字符串引用,在 JDK1.7 版本以後,會將堆中的字符串常量的引用放入到常量池中,當其餘堆中的字符串對象經過 intern 方法獲取字符串對象時,則會去常量池中判斷是否有相同值的字符串的引用,此時有,則返回該常量池中字符串引用,跟以前的字符串指向同一地址的字符串對象。
以一張圖來總結 String 字符串的建立分配內存地址狀況:
使用 intern 方法須要注意的一點是,必定要結合實際場景,由於常量池的實現是相似於一個 HashTable 的實現方式,HashTable 存儲的數據越大,遍歷的時間複雜度就會增長。若是數據過大,會增長整個字符串常量池的負擔。
Split() 方法使用了正則表達式實現了其強大的分割功能,而正則表達式的性能是很是不穩定的,使用不恰當會引發回溯問題,極可能致使 CPU 高居不下。
因此應該慎重使用 split() 方法,能夠用 String.indexOf() 方法代替 split() 方法完成字符串的分割。若是實在沒法知足需求,在使用 split() 方法時,對回溯問題須要加以重視。
1)作好 String 字符串性能優化,能夠提升系統的總體性能。在這個理論基礎上,Java 版本在迭代中經過不斷地更改爲員變量,節約內存空間,對 String 對象優化。
2)String 對象的不可變性的特性實現了字符串常量池,經過減小同一個值的字符串對象的重複建立,進一步節約內存。
也是由於這個特性,咱們在作長字符串拼接時,須要顯示使用 StringBuilder,以提升字符串的拼接性能。
3)使用 intern 方法,讓變量字符串對象重複使用常量池中相同值的對象,進而節約內存。