概述 隨著Java的廣泛應用,越來越多的關(guān)鍵企業(yè)系統(tǒng)也使用Java構(gòu)建。作為Java核心運行環(huán)境的Java虛擬機JVM被廣泛地部署在各種系統(tǒng)平臺上。對Java應用的性能優(yōu)化也越來越受到關(guān)注;談到Java應用的性能問題就不得不涉及到兩個方面:一是Java應用的構(gòu)造是否是最優(yōu)化的;二是對JVM的微調(diào)。本文將從一般意義上對Java性能的優(yōu)化做一些總結(jié)。 Java性能優(yōu)化的策略 一談到性能優(yōu)化,往往會被認為是應用開發(fā)和部署過程中或之后的事情,其實不然。如果想要構(gòu)建一個最優(yōu)化的系統(tǒng),我們必須從該系統(tǒng)的需求分析和業(yè)務模型設(shè)計之初就要考慮到性能的最優(yōu)化問題;當然對于一個已經(jīng)構(gòu)造好的系統(tǒng)來講,我們能做的只是在不改變系統(tǒng)代碼的前提下,盡量地在該系統(tǒng)的部署方案和運行環(huán)境上下功夫。由此,我們得出一個結(jié)論就是:所謂最優(yōu)化是一個相對的概念,一個系統(tǒng)是否是最優(yōu)化的,必須基于某個大前提來進行評判。因此,在進行優(yōu)化分析之前一定要把握好前提條件是什么。 如上圖所示,可以看出,對系統(tǒng)性能提高貢獻最大、最明顯的是從業(yè)務層面和架構(gòu)層面所作的分析和優(yōu)化;最不明顯的是對系統(tǒng)平臺和硬件層面以及網(wǎng)絡層面的優(yōu)化。因此在著手對目標系統(tǒng)進行優(yōu)化分析之前,我們一定要從優(yōu)化最明顯、貢獻最大的方面著手。這樣有助于我們在最大程度上去提高系統(tǒng)性能。 以下我們將針對Java系統(tǒng)的性能優(yōu)化,從代碼編寫和JVM兩個角度著手,總結(jié)一下常見的方法和思路。 編寫性能高效的Java代碼 根據(jù)GC的工作原理,我們可以通過一些技巧和方式,讓GC運行更加有效率,更加符合應用程序的要求。以下就是一些程序設(shè)計的幾點建議: 1)避免對象創(chuàng)建和GC 只要有可能,應該避免創(chuàng)建對象,防止調(diào)用構(gòu)造函數(shù)帶來的相關(guān)性能成本,以及在對象結(jié)束其生命周期時進行垃圾收集所帶來的成本??紤]以下這些準則: 只要有可能,就使用基本變量類型,而不使用對象類型。例如,使用 int,而不使用 Integer; 緩存那些頻繁使用的壽命短的對象,避免一遍又一遍地重復創(chuàng)建相同的對象,并因此加重垃圾收集的負擔; 在處理字符串時,使用 StringBuffer 而不使用字符串String進行連接操作,因為字符串對象具有不可變的特性,并且需要創(chuàng)建額外的字符串對象以完成相應的操作,而這些對象最終必須經(jīng)歷 GC; 避免過度地進行 Java 控制臺的寫操作,降低字符串對象處理、文本格式化和輸出帶來的成本; 實現(xiàn)數(shù)據(jù)庫連接池,重用連接對象,而不是重復地打開和關(guān)閉連接; 使用線程池(thread pooling),避免不停地創(chuàng)建和刪除線程對象,特別是在大量使用線程的時候; 避免在代碼中調(diào)用GC。GC是一個“停止所有處理(stop the world)”的事件,它意味著除了 GC 線程自身外,其他所有執(zhí)行線程都將處于掛起狀態(tài)。如果必須調(diào)用 GC,那么可以在非緊急階段或空閑階段實現(xiàn)它; 避免在循環(huán)內(nèi)分配對象。 盡早釋放無用對象的引用。大多數(shù)程序員在使用臨時變量的時候,都是讓引用變量在退出活動域(scope)后,自動設(shè)置為null。我們在使用這種方式時候,必須特別注意一些復雜的對象,例如數(shù)組,隊列,樹,圖等,這些對象之間的相互引用關(guān)系較為復雜。對于這類對象,GC回收它們一般效率較低。如果程序允許,盡早將不再使用的引用對象賦為null。這樣可以加速GC的工作。 如果有經(jīng)常使用的圖片,可以使用soft引用類型。它可以盡可能將圖片保存在內(nèi)存中,供程序調(diào)用,而不引起Out Of Memory。 注意一些全局的變量,以及一些靜態(tài)變量。這些變量往往容易引起懸掛對象(dangling reference),造成內(nèi)存浪費。 2)Java Native Interface(JNI) 使用本機代碼編寫應用程序的一部分,特別是頻繁使用的部分,并將之與 Java 鏈接,這樣做通常是為了提高性能。不過,JVM 與本機代碼之間的通信通常很慢,因此,太多的 JNI 調(diào)用可能會降低性能。只要有可能就應該將本機操作集合在一起,以減少 JNI 調(diào)用的數(shù)量。使用 JNI 代碼本地處理異常,盡管有時不可避免,但會導致性能下降。在這種情況下,應該使用 ExceptionCheck() 函數(shù),因為與 ExceptionOccurred() 相比較,它帶來的計算開銷更少一些。后者必須創(chuàng)建一個將引用的對象,以及一個本地引用。 3)同步 為了減少 JVM 和操作系統(tǒng)中的爭用,應該只在可行的情況下才使用同步方法。不要將同步方法放到循環(huán)結(jié)構(gòu)中。 4)數(shù)據(jù)結(jié)構(gòu) 作為一條通用規(guī)則,在更簡單的數(shù)據(jù)結(jié)構(gòu)能滿足需要的地方,應該避免使用更復雜的數(shù)據(jù)結(jié)構(gòu)。例如,在可以使用數(shù)組的地方不要使用向量。使用最有效的方法搜索元素,并將元素插入數(shù)據(jù)結(jié)構(gòu)中,比如說,在向量的結(jié)尾處添加和刪除元素,以便獲得更好的性能。 5)盡可能使用堆棧變量 如果您頻繁存取變量,就需要考慮從何處存取這些變量。變量是static變量,還是堆棧變量,或者是類的實例變量? 變量的存儲位置對存取它的代碼的性能有明顯的影響。JVM 是一種基于堆棧的虛擬機,因此優(yōu)化了對堆棧數(shù)據(jù)的存取和處理。所有局部變量都存儲在一個局部變量表中,在 Java 操作數(shù)堆棧中進行處理,并可被高效地存取。存取 static 變量和實例變量成本更高,因為 JVM 必須使用代價更高的操作碼,并從常數(shù)存儲池中存取它們。(常數(shù)存儲池保存一個類型所使用的所有類型、字段和方法的符號引用。)通常,在第一次從常數(shù)存儲池中訪問 static 變量或?qū)嵗兞恳院螅琂VM 將動態(tài)更改字節(jié)碼以使用效率更高的操作碼。盡管有這種優(yōu)化,堆棧變量的存取仍然更快。 考慮到這些事實,在構(gòu)建代碼時就可以考慮通過存取堆棧變量而不是實例變量或 static 變量,使操作更高效。如果必須使用,可以考慮將實例變量或 static 變量復制到局部堆棧變量中。當變量的處理完成以后,其值又被復制回實例變量或 static 變量中。這并不表示您應該避免使用 static 變量或?qū)嵗兞?。您應該使用對您的設(shè)計有意義的存儲機制。例如,如果您在一個循環(huán)中存取 static 變量或?qū)嵗兞浚瑒t您可以臨時將它們存儲在一個局部堆棧變量中,這樣就可以明顯地提高代碼的性能。這將提供最高效的字節(jié)碼指令序列供 JVM 執(zhí)行。 6)finalize函數(shù) finalize是位于Object類的一個方法,該方法的訪問修飾符為protected,由于所有類為Object的子類,因此用戶類很容易訪問到這個方法。由于,finalize函數(shù)沒有自動實現(xiàn)鏈式調(diào)用,我們必須手動的實現(xiàn),因此finalize函數(shù)的最后一個語句通常是super.finalize()。通過這種方式,我們可以從下到上實現(xiàn)finalize的調(diào)用,即先釋放自己的資源,然后再釋放父類的資源。 根據(jù)Java語言規(guī)范,JVM保證調(diào)用finalize函數(shù)之前,這個對象是不可達的,但是JVM不保證這個函數(shù)一定會被調(diào)用。另外,規(guī)范還保證finalize函數(shù)最多運行一次。 很多Java初學者會認為這個方法類似于C++中的析構(gòu)函數(shù),將很多對象、資源的釋放都放在這一函數(shù)里面。其實,這不是一種很好的方式。原因有三,其一,GC為了能夠支持finalize函數(shù),要對覆蓋這個函數(shù)的對象作很多附加的工作。其二,在finalize運行完成之后,該對象可能變成可達的,GC還要再檢查一次該對象是否是可達的。因此,使用finalize會降低GC的運行性能。其三,由于GC調(diào)用finalize的時間是不確定的,因此通過這種方式釋放資源也是不確定的。 通常,finalize用于一些不容易控制、并且非常重要的資源的釋放,例如一些I/O的操作,數(shù)據(jù)的連接。這些資源的釋放對整個應用程序是非常關(guān)鍵的。在這種情況下,程序員應該以通過程序本身管理(包括釋放)這些資源為主,以finalize函數(shù)釋放資源方式為輔,形成一種雙保險的管理機制,而不應該僅僅依靠finalize來釋放資源。 7)異常的開銷很大 是的,異常開銷很大。那么,這是不是就意味著您不該使用異常?當然不是。但是,何時應該使用異常,何時又不應該使用異常呢?不幸的是,答案不是一下子就說得清的。我們要說的是,您不必放棄已經(jīng)學到的好的 try-catch 編程習慣,但是使用異常時可能會遇到麻煩,創(chuàng)建異常就是一個例子。當創(chuàng)建一個異常時,需要收集一個棧跟蹤(stack track),這個棧跟蹤用于描述異常是在何處創(chuàng)建的。構(gòu)建這些棧跟蹤時需要為運行時棧做一份快照,正是這一部分開銷很大。運行時棧不是為有效的異常創(chuàng)建而設(shè)計的,而是設(shè)計用來讓運行時盡可能快且沒有任何不必要的延遲。但是,當需要創(chuàng)建一個 Exception 時,JVM 不得不說:“先別動,我想就您現(xiàn)在的樣子存一份快照,所以暫時停止入棧和出棧操作?!睏8櫜恢话\行時棧中的一兩個元素,而是包含這個棧中的每一個元素,從棧頂?shù)綏5?,還有行號和一切應有的東西。如果在一個深度為20的棧中創(chuàng)建了異常,那么就別指望只記錄頂部的幾個棧元素了——得完完整整地記錄下所有20個元素。從 main 或 Thread.run (在棧底)到棧頂,記錄整個棧。 因此,創(chuàng)建異常這一部分開銷很大。從技術(shù)上講,棧跟蹤快照是在本地方法 Throwable.fillInStackTrace() 中發(fā)生的,這個方法又是從 Throwable constructor 那里調(diào)用的。但是這并沒有什么影響——如果您創(chuàng)建一個 Exception ,就得付出代價。好在捕獲異常開銷不大,因此可以使用 try-catch 將核心內(nèi)容包起來。從技術(shù)上講,您甚至可以隨意地拋出異常,而不用花費很大的代價。招致性能損失的并不是 throw 操作——盡管在沒有預先創(chuàng)建異常的情況下就拋出異常是有點不尋常。真正要花代價的是創(chuàng)建異常。幸運的是,好的編程習慣已教會我們,不應該不管三七二十一就拋出異常。異常是為異常的情況而設(shè)計的,使用時也應該牢記這一原則。 8)避免非常大的分配 有時候問題不是由當時的堆狀態(tài)造成的,而是因為分配失敗造成的。分配的內(nèi)存塊都必須是連續(xù)的,而隨著堆越來越滿,找到較大的連續(xù)塊越來越困難。這不僅僅是 Java 的問題,使用 C 中的 malloc 也會遇到這個問題。JVM 在壓縮階段通過重新分配引用來減少碎片,但其代價是要凍結(jié)應用程序較長的時間。 |
|
來自: 昵稱27831725 > 《編寫高效代碼》