Java 內存模型及GC原理
一個優秀Java程序員,必須了解Java內存模型、GC工作原理,以及如何優化GC的性能、與GC進行有限的交互,有一些應用程序對性能要求較高,例如嵌入式系統、實時系統等,只有全面提升內存的管理效率,才能提高整個應用程序的性能。
本文將從JVM內存模型、GC工作原理,以及GC的幾個關鍵問題進行探討,從GC角度提高Java程序的性能。
一、Java內存模型
按照官方的說法:Java 虛擬機具有一個堆,堆是運行時數據區域,所有類實例和數組的內存均從此處分配。
JVM主要管理兩種類型內存:堆和非堆,堆內存(Heap Memory)是在 Java 虛擬機啟動時創建,非堆內存(Non-heap Memory)是在JVM堆之外的內存。
簡單來說,堆是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</span> 指定,詳見 可以通過 -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只可能運行一次。