性能調優 -- Java編程中的性能優化

  String做爲咱們使用最頻繁的一種對象類型,其性能問題是最容易被忽略的。做爲Java中重要的數據類型,是內存中佔據空間比較大的一個對象。如何高效地使用字符串,能夠幫助咱們提高系統的總體性能。正則表達式

  如今,咱們就從String對象的實現、特性以及實際使用中的優化這幾方面來入手,深刻理解如下String的性能優化。編程

  在這以前,首先看一個問題。經過三種方式建立三個對象,而後依次兩兩匹配,得出的結果是什麼?答案留到最後揭曉。數組

1 String str1 = "abc";
2 String str2 = new String("abc");
3 String str3 = str2.intern();
4 System.out.println(str1 == str2);
5 System.out.println(str2 == str3);
6 System.out.println(str1 == str3);

  

  String對象是如何實現的?緩存

  Java中對String對象作了大量的優化,以此來節約內存空間,提高String對象的性能。下圖是Java6 -> Java9 String對象屬性的變化:安全

  

  能夠看到,String的屬性有了如下的變化:性能優化

  • 在Java6及之前的版本中,String對象是對char數組進行了封裝實現的對象,主要有:char數組、偏移量offset、字符數量count、哈希值hash。String對象經過offset和count屬性來定位char數組,獲取字符串。這樣作能夠高效快速地共享數組對象,能節省內存空間,但容易出現內存泄漏。
  • 從Java7到Java8版本,Java對String作了一些改變。String類中再也不有offset和count兩個屬性了。這樣作可使String對象佔用的內存減小,而且String.substring方法也再也不共享char[],解決了可能出現的內存泄漏的問題。
  • 從Java9版本開始,將char[]改爲了byte[],並增長了新屬性coder,coder是一個編碼格式的標識。

 

  爲何要這麼改呢?多線程

  咱們知道,一個char字符佔16位,2個字節。這種狀況下存儲單字節的字符就很容易浪費了。JDK1.9的String類爲了節省內存空間,就使用了佔8位,1個字節的byte數組來存儲字符串。app

  coder屬性的做用是:在計算字符串長度或者使用indexOf()時,須要根據這個字段,判斷如何計算字符串的長度。coder屬性值默認有0和1兩個值,0表明Latin-1(單字節編碼),1表明UTF-16。若是String判斷字符串只包含Latin-1,則coder值取0,反之爲1。函數

 

  String對象的不可變性性能

  若是看過String的源碼,就會發現,String類是被final關鍵字修飾的,且變量char數組也被final修飾。

  一個類被final修飾表明着該類不可繼承,char[]被private和final修飾着,表明String對象不可被更改。這就叫作String對象的不可變性。即若是String對象一旦建立成功了,就不能再對它進行改變。

 

  這樣作的好處在哪裏?  

  第1、保證了String對象的安全性。假設String對象是可變的,那麼String對象就會被惡意修改。

  第二.、保證hash屬性值不會頻繁變動,確保了惟一性。使得相似HashMap容器才能實現相應的key-value緩存功能。

  第3、能夠實現字符串常量池。Java中,一般有2種建立字符串對象的方式,一種是經過字符串常量的方式建立,如String str = "abc";另外一種是字符串常量經過new形式的建立,如String str = new String("abc")。

  當代碼中使用第一種方式建立字符串對象時,JVM首先檢查該對象是否在字符串常量池中,若是在就返回該對象的引用,不然新的字符串將在常量池中建立。這種方式能夠減小同一個值的字符串對象的重複建立,節約內存。

  第二種方式,首先在編譯類文件時,"abc"常量字符串將會放入到常量結構中,在類加載時,"abc"會在常量池中建立;而後調用new時,JVM命令將會調用String的構造函數,同時引用常量池的"abc"字符串,在堆內存中建立一個String對象,最後str引用String對象。

  

  String對象的優化

  1.如何構建超大字符串

  編程過程當中字符串的拼接很常見。若是使用String對象相加,拼接咱們想要的字符串,會不會產生多個對象呢?好比說如下代碼:

String str = "ab" + "cd" + "ef";

  分析代碼可知:首先會生成ab對象,再生成abcd對象,最後生成abcdef對象。理論上說,代碼很低效。

  但實際上,會發現只有一個對象生成,這是爲何呢?編譯時編譯器會自動幫咱們優化代碼,使得最後只得出一個對象「abcdef」。

  再來看看,若是進行字符串常量的累計,又會出現什麼結果?

String str = "abcdef";
for (int i = 0; i < 100; i++) {
     str = str + i;
 }

  上面的代碼編譯後,編譯器一樣對代碼進行了優化,在進行字符串拼接時,偏向使用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差些。

  2.如何使用String.intern節省內存?

  對於一些數據,數據量很是大,但同時又有大部分重合的,該如何處理呢?

  具體作法是,每次賦值的時候使用String的intern方法,若是常量池中有相同值,就會重複使用該對象,返回對象的引用,這樣一開始的對象就能夠被回收掉了,這樣的話數據量就會大幅度下降了。

  咱們再來看一個例子:

String a = new String("abc").intern();
String b = new String("abc").intern(); 
if (a == b) {
     System.out.println("a == b");
}

  輸出結果是: a == b

  在字符串常量池中,默認會將對象放入常量池;在字符串變量中,對象老是建立在堆內存的,同時也會在常量池中建立一個字符串對象,複製到堆內存對象中,並返回堆內存對象引用。

  若是調用intern方法,會去查看字符串常量池中是否有等於該對象的字符串,若是沒有,就會在常量池中新增該對象,並返回該對象引用;若是有則返回常量池中的字符串引用。堆內存中原有的對象因爲沒有引用指向它,將會經過垃圾回收器回收。

  3.如何使用字符串的分割方法?

  spilt()方法使用了正則表達式實現了強大的分割功能,而正則表達式的性能是很是不穩定的,使用不當會引發回溯問題,極可能致使CPU居高不下。

  因此要慎重使用spilt方法,咱們能夠用String.indexOf()方法代替spilt()方法完成字符串的分割,若是實在沒法知足需求,就在使用spilt方法時,對回溯問題加以重視就能夠了。

 

  總結

  經過上面的敘述,咱們認識到了作好String字符串性能的優化,能夠提高整個系統的性能。在這個理論基礎上,Java版本在迭代中不斷更改爲員變量,節約內存空間,對String性能進行了優化。

  咱們還提到了String對象的不可變性,正是這個特性實現了字符串常量池,經過減小同一個值的字符串對象的重複建立,進一步節約內存。也是由於這個特性,咱們在作長字符串的拼接時,須要顯示使用StringBuilder,以提高字符串的拼接性能。最後在優化方面,咱們還可使用intern方法,讓變量字符串對象重複使用常量池中相同值的對象,進而節約內存。

  最後,公佈上面那道題的結果:

  false、false、true。

  其中, String str1 = 「abc」;經過字面量的方式建立,abc存儲於字符串常量池中;

  String str2 = new String("abc");經過new對象的方式建立字符串對象,引用地址存放在堆內存中,abc則存放在字符串常量池中,因此爲false;

  String str3 = str2.intern();因爲調用了intern()方法,會返回常量池中的數據,str3此時就指向常量池中的abc,和str1的方式同樣,因此爲true;

相關文章
相關標籤/搜索