1. 程式人生 > 程式設計 >Java程式設計中的效能優化如何實現

Java程式設計中的效能優化如何實現

  String作為我們使用最頻繁的一種物件型別,其效能問題是最容易被忽略的。作為Java中重要的資料型別,是記憶體中佔據空間比較大的一個物件。如何高效地使用字串,可以幫助我們提升系統的整體效能。

  現在,我們就從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中對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陣列來儲存字串。

  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物件一旦建立成功了,就不能再對它進行改變。

  這樣做的好處在哪裡?  

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

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

  第三、可以實現字串常量池。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;

以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支援我們。