深入淺出之JVM GC篇

dawn009發表於2015-04-30

一個優秀的Java程式設計師必須瞭解GC的工作原理、如何最佳化GC的效能、如何與GC進行有限的互動,因為有一些應用程式對效能要求較高,例如嵌入式系統、 實時系統等,只有全面提升記憶體的管理效率 ,才能提高整個應用程式的效能。本篇文章首先簡單介紹GC的工作原理之後,然後再對GC的幾個關鍵問題進行深入探討,最後提出一些Java程式設計建議, 從GC角度提高Java程式的效能。

一、垃圾回收機制(GC)

1. JVM GC的概述

GC即垃圾收集機制是指JVM用於釋放那些不再使用的物件所佔用的記憶體。Java語言並不要求JVM有GC,也沒有規定GC如何工作。不過常用的JVM都有GC,而且大多數GC都使用類似的演算法管理記憶體和執行收集操作。

2. GC基本原理

Java的記憶體管理實際上就是物件的管理,其中包括物件的分配和釋放。 對於程式設計師來說,分配物件使用new關鍵字;釋放物件時,只要將物件所有引用賦值為null,讓程式不能夠再訪問到這個物件,我們稱該物件為"不可達 的".GC將負責回收所有"不可達"物件的記憶體空間。
對於GC來說,當程式設計師建立物件時,GC就開始監控這個物件的地址、大小以及使用情況。通常,GC採用有向圖的方式記錄和管理堆(heap)中的所有對 象。透過這種方式確定哪些物件是"可達的",哪些物件是"不可達的".當GC確定一些物件為"不可達"時,GC就有責任回收這些記憶體空間。但是,為了保證 GC能夠在不同平臺實現的問題,Java規範對GC的很多行為都沒有進行嚴格的規定。例如,對於採用什麼型別的回收演算法、什麼時候進行回收等重要問題都沒 有明確的規定。因此,不同的JVM的實現者往往有不同的實現演算法。這也給Java程式設計師的開發帶來行多不確定性。

在充分理解了垃圾收集演算法和執行過程後,才能有效的最佳化它的效能。有些垃圾收集專用於特殊的應用程式。比如,實時應用程式主要是為了避免垃圾收集中斷,而 大多數OLTP應用程式則注重整體效率。理解了應用程式的工作負荷和JVM支援的垃圾收集演算法,便可以進行最佳化配置垃圾收集器。
垃圾收集的目的在於清除不再使用的物件。GC透過確定物件是否被活動物件引用來確定是否收集該物件。GC首先要判斷該物件是否是時候可以收集。兩種常用的方法是引用計數和物件引用遍歷。

3. GC 常用機制

3.1 引用計數

引用計數儲存對特定物件的所有引用數,也就是說,當應用程式建立引用以及引用超出範圍時,JVM必須適當增減引用數。當某物件的引用數為0時,便可以進行垃圾收集。

3.2 物件引用遍歷

早期的JVM使用引用計數,現在大多數JVM採用物件引用遍歷。物件引用遍歷從一組物件開始,沿著整個物件圖上的每條連結,遞迴確定可到達 (reachable)的物件。如果某物件不能從這些根物件的一個(至少一個)到達,則將它作為垃圾收集。在物件遍歷階段,GC必須記住哪些物件可以到 達,以便刪除不可到達的物件,這稱為標記(marking)物件。
下一步,GC要刪除不可到達的物件。刪除時,有些GC只是簡單的掃描堆疊,刪除已標記的物件,並釋放它們的記憶體以生成新的物件,這叫做清除 (sweeping)。這種方法的問題在於記憶體會分成好多小段,而它們不足以用於新的物件,但是組合起來卻很大。因此,許多GC可以重新組織記憶體中的對 象,並進行壓縮(compact),形成可利用的空間。
為此,GC需要停止其他的活動。這種方法意味著所有與應用程式相關的工作停止,只有GC執行。結果,在響應期間增減了許多混雜請求。另外,更復雜的GC不斷增加或同時執行以減少或者清除應用程式的中斷。有的GC使用單執行緒完成這項工作,有的則採用多執行緒以增加效率。

3.3 標記-清除收集器

這種收集器首先遍歷物件圖並標記可到達的物件,然後掃描堆疊以尋找未標記物件並釋放它們的記憶體。這種收集器一般使用單執行緒工作並停止其他操作。

3.4 標記-壓縮收集器

有時也叫標記-清除-壓縮收集器,與標記-清除收集器有相同的標記階段。在第二階段,則把標記物件複製到堆疊的新域中以便壓縮堆疊。這種收集器也停止其他操作。

3.5 複製收集器

這種收集器將堆疊分為兩個域,常稱為半空間。每次僅使用一半的空間,JVM生成的新物件則放在另一半空間中。GC執行時,它把可到達物件複製到另一半空間,從而壓縮了堆疊。這種方法適用於短生存期的物件,持續複製長生存期的物件則導致效率降低。

3.6 增量收集器

增量收集器把堆疊分為多個域,每次僅從一個域收集垃圾。這會造成較小的應用程式中斷。

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執行只回收一定比例的 不可達物件,保證程式的順暢執行。

3.7 分代收集器

這種收集器把堆疊分為兩個或多個域,用以存放不同壽命的物件。JVM生成的新物件一般放在其中的某個域中。過一段時間,繼續存在的物件將獲得使用期並轉入更長壽命的域中。分代收集器對不同的域使用不同的演算法以最佳化效能。

3.8 併發收集器

併發收集器與應用程式同時執行。這些收集器在某點上(比如壓縮時)一般都不得不停止其他操作以完成特定的任務,但是因為其他應用程式可進行其他的後臺操作,所以中斷其他處理的實際時間大大降低。

3.9 並行收集器

並行收集器使用某種傳統的演算法並使用多執行緒並行的執行它們的工作。在多CPU機器上使用多執行緒技術可以顯著的提高Java應用程式的可擴充套件性。

二、詳解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只可能執行一次。

class MyObject{
Test main; //記錄Test物件,在finalize中時用於恢復可達性
public MyObject(Test t)
{
main=t; //儲存Test 物件
}
protected void finalize()
{
main.ref=this;// 恢復本物件,讓本物件可達
System.out.println("This is finalize");//用於測試finalize只執行一次
}
}
class Test {
MyObject ref;
public static void main(String[] args) {
Test test=new Test();
test.ref=new MyObject(test);
test.ref=null; //MyObject物件為不可達物件,finalize將被呼叫
System.gc();
if (test.ref!=null) System.out.println("My Object還活著");
}
}
執行結果:
This is finalize
MyObject還活著

此例子中,需要注意的是雖然MyObject物件在finalize中變成可達物件,但是下次回收時候,finalize卻不再被呼叫,因為 finalize函式最多隻呼叫一次。

三、垃圾回收只與記憶體有關

也就是說,使用垃圾回收器的唯一原因是為了回收程式不再使用的記憶體。所以對於與垃圾回收有關的任何行為來說(尤其是finalize()方法),它們也必 須同記憶體及其回收有關。但這是否意味著要是物件中含有其他物件,finalize()就應該明確釋放那些物件呢?不,無論物件是如何建立的,垃圾回收器都 會負責釋放物件佔據的所有記憶體。這就將對finalize()的需求限制到一種特殊情況,即透過某種建立物件方式以外的方式為物件分配了儲存空間。不 過,java中一切皆為物件,那這種特殊情況是怎麼回事呢?由於在分配記憶體時可能採用了類似C語言中的做法,而非java中的通常做法。這種情況主要發生在使用“本地方法”的情況下,本地方法是一種在Java中呼叫非Java程式碼的方式。在非java程式碼中,也許會呼叫C的malloc()函式系列來分配儲存空間,而且從未呼叫free()函式。

四、一些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程式的暫停時間。

出處:http://my.oschina.net/xianggao/blog/86985

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/29119536/viewspace-1613779/,如需轉載,請註明出處,否則將追究法律責任。

相關文章