天天看點

Java 性能優化--進階優化

GC的基本原理

  Java的記憶體管理實際上就是對象的管理,其中包括對象的配置設定和釋放。

  對于程式員來說,配置設定對象使用new關鍵字;釋放對象時,隻要将對象所有引用指派為null,讓程式不能夠再通路到這個對象,我們稱該對象為"不可達的".GC将負責回收所有"不可達"對象的記憶體空間。

  對于GC來說,當程式員建立對象時,GC就開始監控這個對象的位址、大小以及使用情況。通常,GC采用有向圖的方式記錄和管理堆(heap)中的所有對象(詳見

參考資料1

)。通過這種方式确定哪些對象是"可達的",哪些對象是"不可達的".當GC确定一些對象為"不可達"時,GC就有責任回收這些記憶體空間。但是,為了保證GC能夠在不同平台實作的問題,Java規範對GC的很多行為都沒有進行嚴格的規定。例如,對于采用什麼類型的回收算法、什麼時候進行回收等重要問題都沒有明确的規定。是以,不同的JVM的實作者往往有不同的實作算法。這也給Java程式員的開發帶來行多不确定性。本文研究了幾個與GC工作相關的問題,努力減少這種不确定性給Java程式帶來的負面影響。

  增量式GC( Incremental GC )

  GC在JVM中通常是由一個或一組程序來實作的,它本身也和使用者程式一樣占用heap空間,運作時也占用CPU.當GC程序運作時,應用程式停止運作。是以,當GC運作時間較長時,使用者能夠感到Java程式的停頓,另外一方面,如果GC運作時間太短,則可能對象回收率太低,這意味着還有很多應該回收的對象沒有被回收,仍然占用大量記憶體。是以,在設計GC的時候,就必須在停頓時間和回收率之間進行權衡。一個好的GC實作允許使用者定義自己所需要的設定,例如有些記憶體有限有裝置,對記憶體的使用量非常敏感,希望GC能夠準确的回收記憶體,它并不在意程式速度的放慢。另外一些實時網絡遊戲,就不能夠允許程式有長時間的中斷。增量式GC就是通過一定的回收算法,把一個長時間的中斷,劃分為很多個小的中斷,通過這種方式減少GC對使用者程式的影響。雖然,增量式GC在整體性能上可能不如普通GC的效率高,但是它能夠減少程式的最長停頓時間。

  Sun JDK提供的HotSpot JVM就能支援增量式GC.HotSpot

JVM預設GC方式為不使用增量GC,為了啟動增量GC,我們必須在運作Java程式時增加-Xincgc的參數。HotSpot

JVM增量式GC的實作是采用Train

GC算法。它的基本想法就是,将堆中的所有對象按照建立和使用情況進行分組(分層),将使用頻繁高和具有相關性的對象放在一隊中,随着程式的運作,不斷對組進行調整。當GC運作時,它總是先回收最老的(最近很少通路的)的對象,如果整組都為可回收對象,GC将整組回收。這樣,每次GC運作隻回收一定比例的不可達對象,保證程式的順暢運作。

  詳解finalize函數

  finalize是位于Object類的一個方法,該方法的通路修飾符為protected,由于所有類為Object的子類,是以使用者類很容易通路到這個方法。由于,finalize函數沒有自動實作鍊式調用,我們必須手動的實作,是以finalize函數的最後一個語句通常是super.finalize()。通過這種方式,我們可以實作從下到上實作finalize的調用,即先釋放自己的資源,然後再釋放父類的資源。

  根據Java語言規範,JVM保證調用finalize函數之前,這個對象是不可達的,但是JVM不保證這個函數一定會被調用。另外,規範還保證finalize函數最多運作一次。

  很多Java初學者會認為這個方法類似與C++中的析構函數,将很多對象、資源的釋放都放在這一函數裡面。其實,這不是一種很好的方式。原因有三,其一,GC為了能夠支援finalize函數,要對覆寫這個函數的對象作很多附加的工作。其二,在finalize運作完成之後,該對象可能變成可達的,GC還要再檢查一次該對象是否是可達的。是以,使用finalize會降低GC的運作性能。其三,由于GC調用finalize的時間是不确定的,是以通過這種方式釋放資源也是不确定的。

  通常,finalize用于一些不容易控制、并且非常重要資源的釋放,例如一些I/O的操作,資料的連接配接。這些資源的釋放對整個應用程式是非常關鍵的。在這種情況下,程式員應該以通過程式本身管理(包括釋放)這些資源為主,以finalize函數釋放資源方式為輔,形成一種雙保險的管理機制,而不應該僅僅依靠finalize來釋放資源。

  下面給出一個例子說明,finalize函數被調用以後,仍然可能是可達的,同時也可說明一個對象的finalize隻可能運作一次。

  運作結果:

  此例子中,需要注意的是雖然MyObject對象在finalize中變成可達對象,但是下次回收時候,finalize卻不再被調用,因為finalize函數最多隻調用一次。

  程式如何與GC進行互動

  Java2增強了記憶體管理功能,

增加了一個java.lang.ref包,其中定義了三種引用類。這三種引用類分别為SoftReference、WeakReference和PhantomReference.通過使用這些引用類,程式員可以在一定程度與GC進行互動,以便改善GC的工作效率。這些引用類的引用強度介于可達對象和不可達對象之間。

  建立一個引用對象也非常容易,例如如果你需要建立一個Soft

Reference對象,那麼首先建立一個對象,并采用普通引用方式(可達對象);然後再建立一個SoftReference引用該對象;最後将普通引用設定為null.通過這種方式,這個對象就隻有一個Soft

Reference引用。同時,我們稱這個對象為Soft Reference 對象。

  Soft

Reference的主要特點是據有較強的引用功能。隻有當記憶體不夠的時候,才進行回收這類記憶體,是以在記憶體足夠的時候,它們通常不被回收。另外,這些引用對象還能保證在Java抛出OutOfMemory

異常之前,被設定為null.它可以用于實作一些常用圖檔的緩存,實作Cache的功能,保證最大限度的使用記憶體而不引起OutOfMemory.以下給出這種引用類型的使用僞代碼;

  Weak引用對象與Soft引用對象的最大不同就在于:GC在進行回收時,需要通過算法檢查是否回收Soft引用對象,而對于Weak引用對象,GC總是進行回收。Weak引用對象更容易、更快被GC回收。雖然,GC在運作時一定回收Weak對象,但是複雜關系的Weak對象群常常需要好幾次GC的運作才能完成。Weak引用對象常常用于Map結構中,引用資料量較大的對象,一旦該對象的強引用為null時,GC能夠快速地回收該對象空間。

  Phantom引用的用途較少,主要用于輔助finalize函數的使用。Phantom對象指一些對象,它們執行完了finalize函數,并為不可達對象,但是它們還沒有被GC回收。這種對象可以輔助finalize進行一些後期的回收工作,我們通過覆寫Reference的clear()方法,增強資源回收機制的靈活性。

  一些Java編碼的建議

  根據GC的工作原理,我們可以通過一些技巧和方式,讓GC運作更加有效率,更加符合應用程式的要求。以下就是一些程式設計的幾點建議。

  1.最基本的建議就是盡早釋放無用對象的引用。大多數程式員在使用臨時變量的時候,都是讓引用變量在退出活動域(scope)後,自動設定為null.我們在使用這種方式時候,必須特别注意一些複雜的對象圖,例如數組,隊列,樹,圖等,這些對象之間有互相引用關系較為複雜。對于這類對象,GC回收它們一般效率較低。如果程式允許,盡早将不用的引用對象賦為null.這樣可以加速GC的工作。

  2.盡量少用finalize函數。finalize函數是Java提供給程式員一個釋放對象或資源的機會。但是,它會加大GC的工作量,是以盡量少采用finalize方式回收資源。

  3.如果需要使用經常使用的圖檔,可以使用soft應用類型。它可以盡可能将圖檔儲存在記憶體中,供程式調用,而不引起OutOfMemory.

  4.注意集合資料類型,包括數組,樹,圖,連結清單等資料結構,這些資料結構對GC來說,回收更為複雜。另外,注意一些全局的變量,以及一些靜态變量。這些變量往往容易引起懸挂對象(dangling

reference),造成記憶體浪費。

  5.當程式有一定的等待時間,程式員可以手動執行System.gc(),通知GC運作,但是Java語言規範并不保證GC一定會執行。使用增量式GC可以縮短Java程式的暫停時間。

    6. 1)避免對象建立和GC

  隻要有可能,應該避免建立對象,防止調用構造函數帶來的相關性能成本,以及在對象結束其生命周期時進行垃圾收集所帶來的成本。考慮以下這些準則: 隻要有可能,就使用基本變量類型,而不使用對象類型。例如,使用 int,而不使用 Integer;

  緩存那些頻繁使用的壽命短的對象,避免一遍又一遍地重複建立相同的對象,并是以加重垃圾收集的負擔;

  在處理字元串時,使用 StringBuffer 而不使用字元串String進行連接配接操作,因為字元串對象具有不可變的特性,并且需要建立額外的字元串對象以完成相應的操作,而這些對象最終必須經曆 GC;

  避免過度地進行 Java 控制台的寫操作,降低字元串對象處理、文本格式化和輸出帶來的成本;

  實作資料庫連接配接池,重用連接配接對象,而不是重複地打開和關閉連接配接;

  使用線程池(thread pooling),避免不停地建立和删除線程對象,特别是在大量使用線程的時候;

  避免在代碼中調用GC。GC是一個“停止所有處理(stop the world)”的事件,它意味着除了 GC 線程自身外,其他所有執行線程都将處于挂起狀态。如果必須調用 GC,那麼可以在非緊急階段或空閑階段實作它;

  避免在循環内配置設定對象。

  盡早釋放無用對象的引用。大多數程式員在使用臨時變量的時候,都是讓引用變量在退出活動域(scope)後,自動設定為null。我們在使用這種方式時候,必須特别注意一些複雜的對象,例如數組,隊列,樹,圖等,這些對象之間的互相引用關系較為複雜。對于這類對象,GC回收它們一般效率較低。如果程式允許,盡早将不再使用的引用對象賦為null。這樣可以加速GC的工作。

  如果有經常使用的圖檔,可以使用soft引用類型。它可以盡可能将圖檔儲存在記憶體中,供程式調用,而不引起Out Of Memory。

  注意一些全局的變量,以及一些靜态變量。這些變量往往容易引起懸挂對象(dangling reference),造成記憶體浪費。

繼續閱讀