1、引言git
String 對象是咱們使用最頻繁的一個對象類型,但它的性能問題倒是最容易被忽略的。String 對象做爲 Java 語言中重要的數據類型,是內存中佔據空間最大的一個對象。高效地使用字符串,能夠提高系統的總體性能。正則表達式
2、String 對象的實現編程
在 Java 語言中,Sun 公司的工程師們對 String 對象作了大量的優化,來節約內存空間,提高 String 對象在系統中的性能。數組
1. 在 Java6 以及以前的版本中,String 對象是對 char 數組進行了封裝實現的對象,主要有四個成員變量: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。併發
3、String 對象的不可變性原由
瞭解了 String 對象的實現後,你有沒有發如今實現代碼中 String 類被 final 關鍵字修飾了,並且變量 char 數組也被 final 修飾了。
咱們知道類被 final 修飾表明該類不可繼承,而 char[] 被 final+private 修飾,表明了 String 對象不可被更改。Java 實現的這個特性叫做 String 對象的不可變性,即 String 對象一旦建立成功,就不能再對它進行改變。
4、String 對象的不可變性好處
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 對象。
5、String 對象的優化
1. 如何構建超大字符串?
編程過程當中,字符串的拼接很常見。前面我講過 String 對象是不可變的,若是咱們使用 String 對象相加,拼接咱們想要的字符串,是否是就會產生多個對象呢?例如如下代碼:
String str= "ab" + "cd" + "ef";
分析代碼可知:首先會生成 ab 對象,再生成 abcd 對象,最後生成 abcdef 對象,從理論上來講,這段代碼是低效的。
但實際運行中,咱們發現只有一個對象生成,這是爲何呢?難道咱們的理論判斷錯了?咱們再來看編譯後的代碼,你會發現編譯器自動優化了這行代碼,以下:
String str= "abcdef";
上面介紹的是字符串常量的累計,再來看看字符串變量的累計又是怎樣的呢?
String str = "abcdef";
for(int i=0; i<1000; i++) {
str = str + i;
}
上面的代碼編譯後,你能夠看到編譯器一樣對這段代碼進行了優化。不難發現,Java 在進行字符串的拼接時,偏向使用 StringBuilder,這樣能夠提升程序的效率。
String str = "abcdef";
for(int i=0; i<1000; i++) {
str = (new StringBuilder(String.valueOf(str))).append(i).toString();
}
綜上已知:即便使用 + 號做爲字符串的拼接,也同樣能夠被編譯器優化成 StringBuilder 的方式。但再細緻些,你會發如今編譯器優化的代碼中,每次循環都會生成一個新的 StringBuilder 實例,一樣也會下降系統的性能。
因此平時作字符串拼接的時候,我建議你仍是要顯示地使用 String Builder 來提高系統性能。
若是在多線程編程中,String 對象的拼接涉及到線程安全,你可使用 StringBuffer。可是要注意,因爲 StringBuffer 是線程安全的,涉及到鎖競爭,因此從性能上來講,要比 StringBuilder 差一些。
2. 如何使用 String.intern 節省內存?
講完了構建字符串,咱們再來討論下 String 對象的存儲問題。先看一個案例。
Twitter 每次發佈消息狀態的時候,都會產生一個地址信息,以當時 Twitter 用戶的規模預估,服務器須要 32G 的內存來存儲地址信息。
public class Location {
private String city;
private String region;
private String countryCode;
private double longitude;
private double latitude;
}
考慮到其中有不少用戶在地址信息上是有重合的,好比,國家、省份、城市等,這時就能夠將這部分信息單獨列出一個類,以減小重複,代碼以下:
public class SharedLocation {
private String city;
private String region;
private String countryCode;
}
public class Location {
private SharedLocation sharedLocation;
double longitude;
double latitude;
}
經過優化,數據存儲大小減到了 20G 左右。但對於內存存儲這個數據來講,依然很大,怎麼辦呢?
這個案例來自一位 Twitter 工程師在 QCon 全球軟件開發大會上的演講,他們想到的解決方法,就是使用 String.intern 來節省內存空間,從而優化 String 對象的存儲。
具體作法就是,在每次賦值的時候使用 String 的 intern 方法,若是常量池中有相同值,就會重複使用該對象,返回對象引用,這樣一開始的對象就能夠被回收掉。這種方式可使重複性很是高的地址信息存儲大小從 20G 降到幾百兆。
SharedLocation sharedLocation = new SharedLocation();
sharedLocation.setCity(messageInfo.getCity().intern()); sharedLocation.setCountryCode(messageInfo.getRegion().intern());
sharedLocation.setRegion(messageInfo.getCountryCode().intern());
Location location = new Location();
location.set(sharedLocation);
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.print("a==b");
}
輸出結果:
a==b
在字符串常量中,默認會將對象放入常量池;在字符串變量中,對象是會建立在堆內存中,同時也會在常量池中建立一個字符串對象,複製到堆內存對象中,並返回堆內存對象引用。
若是調用 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 存儲的數據越大,遍歷的時間複雜度就會增長。若是數據過大,會增長整個字符串常量池的負擔。
3. 如何使用字符串的分割方法?
Split() 方法使用了正則表達式實現了其強大的分割功能,而正則表達式的性能是很是不穩定的,使用不恰當會引發回溯問題,極可能致使 CPU 居高不下。
因此咱們應該慎重使用 Split() 方法,咱們能夠用 String.indexOf() 方法代替 Split() 方法完成字符串的分割。若是實在沒法知足需求,你就在使用 Split() 方法時,對回溯問題加以重視就能夠了。
6、總結
咱們認識到作好 String 字符串性能優化,能夠提升系統的總體性能。在這個理論基礎上,Java 版本在迭代中經過不斷地更改爲員變量,節約內存空間,對 String 對象進行優化。
咱們還特別提到了 String 對象的不可變性,正是這個特性實現了字符串常量池,經過減小同一個值的字符串對象的重複建立,進一步節約內存。
但也是由於這個特性,咱們在作長字符串拼接時,須要顯示使用 StringBuilder,以提升字符串的拼接性能。最後,在優化方面,咱們還可使用 intern 方法,讓變量字符串對象重複使用常量池中相同值的對象,進而節約內存。
最後再分享一個我的觀點。那就是千里之堤,潰於蟻穴。平常編程中,咱們每每可能就是對一個小小的字符串瞭解不夠深刻,使用不夠恰當,從而引起線上事故。
好比,在我以前的工做經歷中,就曾由於使用正則表達式對字符串進行匹配,致使併發瓶頸,這裏也能夠將其概括爲字符串使用的性能問題。