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