標籤:
一個優秀Java程式員,必須瞭解Java記憶體模型、GC工作原理,以及如何最佳化GC的效能、與GC進行有限的互動,有一些應用程式對效能要求較高,例如嵌入式系統、即時系統等,只有全面提升記憶體的管理效率,才能提高整個應用程式的效能。
本文將從JVM記憶體模型、GC工作原理,以及GC的幾個關鍵問題進行探討,從GC角度提高Java程式的效能。
一、Java記憶體模型
按照官方的說法:JAVA 虛擬機器具有一個堆,堆是運行時資料區域,所有類執行個體和數組的記憶體均從此處分配。
JVM主要管理兩種類型記憶體:堆和非堆,堆記憶體(Heap Memory)是在 JAVA 虛擬機器啟動時建立,非堆記憶體(Non-heap Memory)是在JVM堆之外的記憶體。
簡單來說,堆是Java代碼可及的記憶體,留給開發人員使用的;非堆是JVM留給自己用的,包含方法區、JVM內部處理或最佳化所需的記憶體(如 JITCompiler,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
protected void finalize() throws Throwable { }
該方法的存取修飾詞為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只可能運行一次。
[java] view plaincopyprint?
- 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。以下給出這種參考型別的使用虛擬碼:
[java] view plaincopyprint?
- // 申請一個映像對象
- 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程式的暫停時間。
參考推薦:
Java記憶體模型及GC原理
一個優秀的Java程式員必須瞭解的GC機制
Android 智能指標原理(推薦)
Java虛擬機器規範
Java虛擬機器參數
Java記憶體模型
Java系列教程(推薦)
Java記憶體回收原理(360doc)
Java記憶體模型及GC原理(圖解)
Java的記憶體結構和垃圾收集(圖解)
JDK5.0中JVM堆模型、GC垃圾收集詳細解析(圖解)
Java記憶體泄露的理解與解決
Java gc的調用機制和編程規則
Java 記憶體流失執行個體及解決方案研究
JVM 優點與缺點的深入分析 [草稿]
著作權聲明:本文為博主原創文章,未經博主允許不得轉載。
from:http://blog.csdn.net/sunboy_2050/article/details/6252552
【轉】Java 記憶體模型及GC原理