String 還能這樣性能調優,我直呼內行

      網友投稿 787 2022-05-30

      STRING 還能優化啥?你是不是框我?

      莫慌,今天給大家見識一下不一樣的 String,從根上拿捏直達 G 點。

      并且碼哥分享一個例子:通過性能調優我們能實現百兆內存輕松存儲幾十 G 數據。

      String對象是我們每天都「摸」的對象類型,但是她的性能問題我們卻總是忽略。

      愛她,不能只會簡單一起玩耍,要深入了解String 的內心深處,做一個「心有猛虎,細嗅薔薇」的暖男。

      通過以下幾點分析,我們一步步揭開她的神秘面紗,讓 String 直接起飛:

      字符串對象的特性;

      String 的不可變性;

      大字符串構建技巧;

      String.intern 節省內存;

      字符串分割技巧;

      String 身體解密

      想要深入了解,就先從基本組成開始……

      「String 締造者」對 String 對象做了大量優化來節省內存,從而提升 String 的性能:

      Java 6 及之前

      數據存儲在 char[]數組中,String通過 offset 和 count兩個屬性定位 char[] 數據獲取字符串。

      這樣可以高效快速的定位并共享數組對象,并且節省內存,但是有可能導致內存泄漏。

      共享 char 數組為啥可能會導致內存泄漏呢?

      String(int?offset,?int?count,?char?value[])?{

      this.value?=?value;

      this.offset?=?offset;

      this.count?=?count;

      }

      public?String?substring(int?beginIndex,?int?endIndex)?{

      //check?boundary

      return??new?String(offset?+?beginIndex,?endIndex?-?beginIndex,?value);

      }

      調用 substring() 的時候雖然創建了新的字符串,但字符串的值 value 仍然指向的是內存中的同一個數組,如下圖所示:

      如果我們僅僅是用 substring 獲取一小段字符,而原始 string字符串非常大的情況下,substring 的對象如果一直被引用。

      此時 String 字符串也無法回收,從而導致內存泄露。

      如果有大量這種通過 substring 獲取超大字符串中一小段字符串的操作,會因為內存泄露而導致內存溢出。

      JDK7、8

      去掉了 offset 和 count兩個變量,減少了 String 對象占用的內存。

      substring 源碼:

      public?String(char?value[],?int?offset,?int?count)?{

      this.value?=?Arrays.copyOfRange(value,?offset,?offset?+?count);

      }

      public?String?substring(int?beginIndex,?int?endIndex)?{

      int?subLen?=?endIndex?-?beginIndex;

      return?new?String(value,?beginIndex,?subLen);

      }

      substring() 通過 new String() 返回了一個新的字符串對象,在創建新的對象時通過 Arrays.copyOfRange() 深度拷貝了一個新的字符數組。

      如下圖所示:

      String.substring 方法不再共享 char[]數組的數據,解決了可能內存泄漏的問題。

      Java 9

      將 char[]字段改為 byte[],新增 coder屬性。

      碼哥,為什么這么改呢?

      一個 char 字符占 2 個字節,16 位。存儲單字節編碼內的字符(占一個字節的字符)就顯得非常浪費。

      為了節約內存空間,于是使用了 1 個字節占 8 位的 byte 數組來存放字符串。

      勤儉節約的女神,誰不愛……

      新屬性 coder 的作用是:在計算字符串長度或者使用 indexOf()方法時,我們需要根據編碼類型來計算字符串長度。

      coder 的值分別表示不同編碼類型:

      0:表示使用 Latin-1 (單字節編碼);

      1:使用UTF-16。

      String 的不可變性

      了解了String 的基本組成之后,發現 String 還有一個比外在更性感的特性,她被 final關鍵字修飾,char 數組也是。

      我們知道類被 final 修飾代表該類不可繼承,而 char[]被 final+private 修飾,代表了 String 對象不可被更改。

      String 對象一旦創建成功,就不能再對它進行改變。

      final 修飾的好處

      安全性

      當你在調用其他方法時,比如調用一些系統級操作指令之前,可能會有一系列校驗。

      如果是可變類的話,可能在你校驗過后,它的內部的值又被改變了,這樣有可能會引起嚴重的系統崩潰問題。

      高性能緩存

      String不可變之后就能保證 hash值得唯一性,使得類似 HashMap容器才能實現相應的 key-value 緩存功能。

      實現字符串常量池

      由于不可變,才得以實現字符串常量池。

      字符串常量池指的是在創建字符串的時候,先去「常量池」查找是否創建過該「字符串」;

      如果有,則不會開辟新空間創建字符串,而是直接把常量池中該字符串的引用返回給此對象。

      創建字符串的兩種方式:

      String str1 = “碼哥字節”;

      String str2 = new String(“碼哥字節”);

      當代碼中使用第一種方式創建字符串對象時,JVM 首先會檢查該對象是否在字符串常量池中,如果在,就返回該對象引用。

      否則新的字符串將在常量池中被創建,并返回該引用。

      這樣可以減少同一個值的字符串對象的重復創建,節約內存。

      第二種方式創建,在編譯類文件時,"碼哥字節" 字符串將會放入到常量結構中,在類加載時,“碼哥字節" 將會在常量池中創建;

      在調用 new 時,JVM 命令將會調用 String 的構造函數,在堆內存中創建一個 String 對象,同時該對象指向「常量池」中的“碼哥字節”字符串,str 指向剛剛在堆上創建的 String 對象;

      如下圖(str1、str2):

      什么是對象和對象引用呀?

      str 屬于方法棧的字面量,它指向堆中的 String 對象,并不是對象本。

      對象在內存中是一塊內存地址,str 則是指向這個內存地址的引用。

      也就是說 str 并不是對象,而只是一個對象引用。

      碼哥,字符串的不可變到底指的是什么呀?

      String?str?=?"Java";

      str?=?"Java,yyds"

      第一次賦值 「Java」,第二次賦值「Java,yyds」,str 值確實改變了,為什么我還說 String 對象不可變呢?

      這是因為 str 只是 String 對象的引用,并不是對象本身。

      真正的對象依然還在內存中,沒有被改變。

      優化實戰

      了解了 String 的對象實現原理和特性,是時候要深入女神內心,結合實際場景,如何更上一層樓優化 String 對象的使用。

      大字符串如何構建

      既然 String 對象是不可變,所以我們在頻繁拼接字符串的時候是否意味著創建多個對象呢?

      String?str?=?"癩蛤蟆撩青蛙"?+?"長的丑"?+?"玩的花";

      是不是以為先生成「癩蛤蟆撩青蛙」對象,再生成「癩蛤蟆撩青蛙長的丑」對象,最后生成「癩蛤蟆撩青蛙長得丑玩的花」對象。

      實際運行中,只有一個對象生成。

      這是為什么呢?

      雖然代碼寫的丑陋,但是編譯器自動優化了代碼。

      再看下面例子:

      String?str?=?"小青蛙";

      for(int?i=0;?i<1000;?i++)?{

      str?+=?i;

      }

      上面的代碼編譯后,你可以看到編譯器同樣對這段代碼進行了優化。

      Java 在進行字符串的拼接時,偏向使用 StringBuilder,這樣可以提高程序的效率。

      String?str?=?"小青蛙";

      for(int?i=0;?i<1000;?i++)?{

      str?=?(new?StringBuilder(String.valueOf(str))).append(i).toString();

      }

      即使如此,還是循環內重復創建 StringBuilder對象。

      敲黑板

      所以做字符串拼接的時候,我建議你還是要顯式地使用 String Builder 來提升系統性能。

      如果在多線程編程中,String 對象的拼接涉及到線程安全,你可以使用 StringBuffer。

      運用 intern 節省內存

      直接看intern() 方法的定義與源碼:

      intern() 是一個本地方法,它的定義中說的是,當調用 intern 方法時,如果字符串常量池中已經包含此字符串,則直接返回此字符串的引用。

      否則將此字符串添加到常量池中,并返回字符串的引用。

      如果不包含此字符串,先將字符串添加到常量池中,再返回此對象的引用。

      什么情況下適合使用 intern() 方法?

      Twitter 工程師曾分享過一個 String.intern() 的使用示例,Twitter 每次發布消息狀態的時候,都會產生一個地址信息,以當時 Twitter 用戶的規模預估,服務器需要 20G 的內存來存儲地址信息。

      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 工程師使用 String.intern() 使重復性非常高的地址信息存儲大小從 20G 降到幾百兆,從而優化了 String 對象的存儲。

      核心代碼如下:

      SharedLocation?sharedLocation?=?new?SharedLocation();

      sharedLocation.setCity(messageInfo.getCity().intern());

      sharedLocation.setCountryCode(messageInfo.getRegion().intern());

      sharedLocation.setRegion(messageInfo.getCountryCode().intern());

      弄個簡單例子方便理解:

      String?a?=new?String("abc").intern();

      String?b?=?new?String("abc").intern();

      System.out.print(a==b);

      輸出結果:true。

      在加載類的時候會在常量池中創建一個字符串對象,內容是「abc」。

      創建局部 a 變量時,調用 new Sting() 會在堆內存中創建一個 String 對象,String 對象中的 char 數組將會引用常量池中字符串。

      在調用 intern 方法之后,會去常量池中查找是否有等于該字符串對象的引用,有就返回引用。

      創建 b 變量時,調用 new Sting() 會在堆內存中創建一個 String 對象,String 對象中的 char 數組將會引用常量池中字符串。

      在調用 intern 方法之后,會去常量池中查找是否有等于該字符串對象的引用,有就返回引用給局部變量。

      而剛在堆內存中的兩個對象,由于沒有引用指向它,將會被垃圾回收。

      所以 a 和 b 引用的是同一個對象。

      字符串分割有妙招

      Split() 方法使用了正則表達式實現了其強大的分割功能,而正則表達式的性能是非常不穩定的。

      使用不恰當會引起回溯問題,很可能導致 CPU 居高不下。

      Java 正則表達式使用的引擎實現是 NFA(Non deterministic Finite Automaton,確定型有窮自動機)自動機,這種正則表達式引擎在進行字符匹配時會發生回溯(backtracking),而一旦發生回溯,那其消耗的時間就會變得很長,有可能是幾分鐘,也有可能是幾個小時,時間長短取決于回溯的次數和復雜度。

      所以我們應該慎重使用 Split() 方法,我們可以用String.indexOf()方法代替 Split() 方法完成字符串的分割。

      總結與思考

      String 還能這樣性能調優,我直呼內行

      我們從 String 進化歷程掌握了她的組成,不斷的改變成員變量節約內存。

      她的不可變性從而實現了字符串常量池,減少同一個字符串的重復創建,節約內存。

      但也是因為這個特性,我們在做長字符串拼接時,需要顯示使用 StringBuilder,以提高字符串的拼接性能。

      最后,在優化方面,我們還可以使用 intern 方法,讓變量字符串對象重復使用常量池中相同值的對象,進而節約內存。

      通過三種不同的方式創建了三個對象,再依次兩兩匹配,每組被匹配的兩個對象是否相等?代碼如下:

      String?str1?=?"abc";

      String?str2?=?new?String("abc");

      String?str3?=?str2.intern();

      assertSame(str1?==?str2);

      assertSame(str2?==?str3);

      assertSame(str1?==?str3)

      跟碼哥一起學習,道路上不迷路。

      Java 應用性能調優

      版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。

      上一篇:Cat.1究竟是如何崛起的?中速率到底有什么用?
      下一篇:tornado中使用異步
      相關文章
      亚洲日本乱码一区二区在线二产线| 亚洲精品色婷婷在线影院| jlzzjlzz亚洲乱熟在线播放| 亚洲va久久久久| 精品久久久久久亚洲精品| 亚洲日本香蕉视频| 亚洲酒色1314狠狠做| 亚洲自偷自拍另类12p| 亚洲AV色香蕉一区二区| 亚洲AV综合色区无码一区| 亚洲精品乱码久久久久66| 亚洲成A人片在线观看WWW| 亚洲国产另类久久久精品小说| 亚洲无av在线中文字幕| 亚洲欧洲精品无码AV| 亚洲精品一品区二品区三品区| 亚洲色精品88色婷婷七月丁香| 国产亚洲色婷婷久久99精品91| 国产亚洲精品免费视频播放 | 亚洲综合色视频在线观看| 日韩亚洲国产二区| 亚洲精品网站在线观看不卡无广告| 亚洲精品成人区在线观看| 国产精品亚洲玖玖玖在线观看 | 亚洲熟妇av一区| 亚洲av永久无码精品三区在线4| 亚洲a∨无码男人的天堂| 亚洲人成色777777老人头| 亚洲国产精品成人AV在线| 国产精品亚洲色婷婷99久久精品| 午夜亚洲av永久无码精品| 亚洲五月午夜免费在线视频| 亚洲一区二区三区香蕉| 亚洲AV永久纯肉无码精品动漫| 久久精品国产精品亚洲毛片| 亚洲人成网站在线观看播放动漫 | 亚洲国产高清美女在线观看| 亚洲欧洲日韩极速播放| 久久精品亚洲日本波多野结衣| 亚洲AV无码一区二三区| 亚洲综合精品香蕉久久网|