Java 記憶體模型及GC原理
一個優秀Java程式設計師,必須瞭解Java記憶體模型、GC工作原理,以及如何優化GC的效能、與GC進行有限的互動,有一些應用程式對效能要求較高,例如嵌入式系統、實時系統等,只有全面提升記憶體的管理效率,才能提高整個應用程式的效能。
本文將從JVM記憶體模型、GC工作原理,以及GC的幾個關鍵問題進行探討,從GC角度提高Java程式的效能。
一、Java記憶體模型
按照官方的說法:Java 虛擬機器具有一個堆,堆是執行時資料區域,所有類例項和陣列的記憶體均從此處分配。
JVM主要管理兩種型別記憶體:堆和非堆,堆記憶體(Heap Memory)是在 Java 虛擬機器啟動時建立,非堆記憶體(Non-heap Memory)是
簡單來說,堆是Java程式碼可及的記憶體,留給開發人員使用的;非堆是JVM留給自己用的,包含方法區、JVM內部處理或優化所需的記憶體(如 JIT Compiler,Just-in-time Compiler,即時編譯後的程式碼快取)、每個類結構(如執行時常數池、欄位和方法資料)以及方法和構造方法的程式碼。
JVM 記憶體包含如下幾個部分:
- 堆記憶體(Heap Memory): 存放Java物件
- 非堆記憶體(Non-Heap Memory): 存放類載入資訊和其它meta-data
- 其它(Other): 存放JVM 自身程式碼等
在JVM啟動時,就已經保留了固定的記憶體空間給Heap記憶體,這部分記憶體並不一定都會被JVM使用,但是可以確定的是這部分保留的記憶體不會被其他程序使用,這部分記憶體大小由-Xmx
而另一部分記憶體在JVM啟動時就分配給JVM,作為JVM的初始Heap記憶體使用,這部分記憶體是由 -Xms 引數指定。
詳細配置檔案目錄:eclipse/eclipse.ini
預設空餘堆記憶體小於40%時,JVM 就會增大堆直到-Xmx 的最大限制,可以由 -XX:MinHeapFreeRatio指定。
預設空餘堆記憶體大於70%時,JVM 會減少堆直到-Xms的最小限制,可以由 -XX:MaxHeapFreeRatio指定,詳見可以通過 -XX:MaxPermSize
設定Non-Heap大小,詳細參見我的百度部落格
二、Java記憶體分配
Java的記憶體管理實際上就是變數和物件的管理,其中包括物件的分配和釋放。
JVM記憶體申請過程如下:
- JVM 會試圖為相關Java物件在Eden中初始化一塊記憶體區域
- 當Eden空間足夠時,記憶體申請結束;否則到下一步
- JVM 試圖釋放在Eden中所有不活躍的物件(這屬於1或更高階的垃圾回收),釋放後若Eden空間仍然不足以放入新物件,則試圖將部分Eden中活躍物件放入Survivor區
- Survivor區被用來作為Eden及OLD的中間交換區域,當OLD區空間足夠時,Survivor區的物件會被移到Old區,否則會被保留在Survivor區
- 當OLD區空間不夠時,JVM 會在OLD區進行完全的垃圾收集(0級)
- 完全垃圾收集後,若Survivor及OLD區仍然無法存放從Eden複製過來的部分物件,導致JVM無法在Eden區為新物件建立記憶體區域,則出現”out of memory”錯誤
三、GC基本原理
GC(Garbage Collection),是JAVA/.NET中的垃圾收集器。
Java是由C++發展來的,它擯棄了C++中一些繁瑣容易出錯的東西,引入了計數器的概念,其中有一條就是這個GC機制(C#借鑑了JAVA)
程式設計人員容易出現問題的地方,忘記或者錯誤的記憶體回收會導致程式或系統的不穩定甚至崩潰,Java提供的GC功能可以自動監測物件是否超過作用域從而達到自動回收記憶體的目的,Java語言沒有提供釋放已分配記憶體的顯示操作方法。所以,Java的記憶體管理實際上就是物件的管理,其中包括物件的分配和釋放。
對於程式設計師來說,分配物件使用new關鍵字;釋放物件時,只要將物件所有引用賦值為null,讓程式不能夠再訪問到這個物件,我們稱該物件為"不可達的".GC將負責回收所有"不可達"物件的記憶體空間。
對於GC來說,當程式設計師建立物件時,GC就開始監控這個物件的地址、大小以及使用情況。通常,GC採用有向圖的方式記錄和管理堆(heap)中的所有物件。通過這種方式確定哪些物件是"可達的",哪些物件是"不可達的".當GC確定一些物件為"不可達"時,GC就有責任回收這些記憶體空間。但是,為了保證 GC能夠在不同平臺實現的問題,Java規範對GC的很多行為都沒有進行嚴格的規定。例如,對於採用什麼型別的回收演算法、什麼時候進行回收等重要問題都沒有明確的規定。因此,不同的JVM的實現者往往有不同的實現演算法。這也給Java程式設計師的開發帶來行多不確定性。本文研究了幾個與GC工作相關的問題,努力減少這種不確定性給Java程式帶來的負面影響。
四、GC分代劃分
JVM記憶體模型中Heap區分兩大塊,一塊是 Young Generation,另一塊是Old Generation
1) 在Young Generation中,有一個叫Eden Space的空間,主要是用來存放新生的物件,還有兩個Survivor Spaces(from、to),它們的大小總是一樣,它們用來存放每次垃圾回收後存活下來的物件。
2) 在Old Generation中,主要存放應用程式中生命週期長的記憶體物件。
3) 在Young Generation塊中,垃圾回收一般用Copying的演算法,速度快。每次GC的時候,存活下來的物件首先由Eden拷貝到某個SurvivorSpace,當Survivor Space空間滿了後,剩下的live物件就被直接拷貝到OldGeneration中去。因此,每次GC後,Eden記憶體塊會被清空。
4) 在Old Generation塊中,垃圾回收一般用mark-compact的演算法,速度慢些,但減少記憶體要求。
5) 垃圾回收分多級,0級為全部(Full)的垃圾回收,會回收OLD段中的垃圾;1級或以上為部分垃圾回收,只會回收Young中的垃圾,記憶體溢位通常發生於OLD段或Perm段垃圾回收後,仍然無記憶體空間容納新的Java物件的情況。
五、增量式GC
增量式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類的一個方法,詳見我的開源專案:src-jdk1.7.0_02
protectedvoid finalize()throwsThrowable{}
該方法的訪問修飾符為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
My Object還活著
此例子中需要注意,雖然MyObject物件在finalize中變成可達物件,但是下次回收時候,finalize卻不再被呼叫,因為finalize函式最多隻呼叫一次。
七、GC程式互動
程式如何與GC進行互動呢? Java2增強了記憶體管理功能,增加了一個java.lang.ref包,詳見我的開源專案:src-jdk1.7.0_02
其中定義了三種引用類。這三種引用類分別為:SoftReference、 WeakReference、 PhantomReference
通過使用這些引用類,程式設計師可以在一定程度與GC進行互動,以便改善GC的工作效率,這些引用類的引用強度介於可達物件和不可達物件之間。
建立一個引用物件也非常容易,例如:如果你需要建立一個Soft Reference物件,那麼首先建立一個物件,並採用普通引用方式(可達物件);然後再建立一個SoftReference引用該物件;最後將普通引用設定為null。通過這種方式,這個物件就只有一個Soft Reference引用。同時,我們稱這個物件為Soft Reference 物件。
Soft Reference的主要特點是據有較強的引用功能。只有當記憶體不夠的時候,才進行回收這類記憶體,因此在記憶體足夠的時候,它們通常不被回收。另外,這些引用物件還能保證在Java丟擲OutOfMemory 異常之前,被設定為null。它可以用於實現一些常用圖片的快取,實現Cache的功能,保證最大限度的使用記憶體而不引起OutOfMemory。以下給出這種引用型別的使用虛擬碼:
// 申請一個影象物件 Image image=new Image(); // 建立Image物件 … // 使用 image … // 使用完了image,將它設定為soft 引用型別,並且釋放強引用; SoftReference sr=new SoftReference(image); image=null; … // 下次使用時 if (sr!=null) image=sr.get(); else{ image=new Image(); //由於GC由於低記憶體,已釋放image,因此需要重新裝載; sr=new SoftReference(image); }
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程式的暫停時間。
參考推薦: