安卓復習之旅—JavaGC 機制
概述因為在之前的內存優化 博客中已經提到了Java的內存區域,而垃圾回收是針對堆內存而言的,所以就把堆內存再深入的講一下,然后再講GC機制;
堆內存模型
堆內存由垃圾回收器的自動內存管理系統回收,分為兩大部分:新生代和老年代。老年代主要存放應用程序中生命周期長的存活對象。新生代又分為三個部分:一個Eden區和兩個Survivor區,Eden區存放新生的對象,Survivor存放每次垃圾回收后存活的對象。
可回收對象的判定
講算法之前,我們先要搞清楚一個問題,什么樣的對象是垃圾(無用對象),需要被回收?
目前市面上有兩種算法用來判定一個對象是否為垃圾。
1. 引用計數算法
給對象中添加一個引用計數器,每當有一個地方引用它時,計數器值就加1;當引用失效時,計數器值就減1;任何時刻計數器為0的對象就是不可能再被使用的。
優點是簡單,高效。
缺點是很難處理循環引用,比如圖中相互引用的兩個對象則無法釋放。
2. 可達性分析算法(根搜索算法)
為了解決上面的循環引用問題,Java采用了一種新的算法:可達性分析算法。
從GC Roots(每種具體實現對GC Roots有不同的定義)作為起點,向下搜索它們引用的對象,可以生成一棵引用樹,樹的節點視為可達對象,反之視為不可達。
OK,即使循環引用了,只要沒有被GC Roots引用了依然會被回收,完美!
但是,這個GC Roots的定義就要考究了,Java語言定義了如下GC Roots對象:
虛擬機棧(幀棧中的本地變量表)中引用的對象。
方法區中靜態屬性引用的對象。
方法區中常量引用的對象。
本地方法棧中JNI引用的對象。
因為垃圾回收的時候,需要整個的引用狀態保持不變,否則判定是判定垃圾,等我稍后回收的時候它又被引用了,這就全亂套了。所以,GC的時候,其他所有的程序執行處于暫停狀態,卡住了。
幸運的是,這個卡頓是非常短(尤其是新生代),對程序的影響微乎其微 (關于其他GC比如并發GC之類的,在此不討論)。
所以GC的卡頓問題由此而來,也是情有可原,暫時無可避免。
幾種垃圾回收算法
1. 標記清除算法 (Mark-Sweep)
標記-清除算法分為兩個階段:標記階段和清除階段。標記階段的任務是標記出所有需要被回收的對象,清除階段就是回收被標記的對象所占用的空間。
優點是簡單,容易實現。
缺點是容易產生內存碎片,碎片太多可能會導致后續過程中需要為大對象分配空間時無法找到足夠的空間而提前觸發新的一次垃圾收集動作。
2. 復制算法 (Copying)
復制算法將可用內存按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用完了,就將還存活著的對象復制到另外一塊上面,然后再把已使用的內存空間一次清理掉,這樣一來就不容易出現內存碎片的問題。
優缺點就是,實現簡單,運行高效且不容易產生內存碎片,但是卻對內存空間的使用做出了高昂的代價,因為能夠使用的內存縮減到原來的一半。
從算法原理我們可以看出,Copying算法的效率跟存活對象的數目多少有很大的關系,如果存活對象很多,那么Copying算法的效率將會大大降低。
3. 標記整理算法 (Mark-Compact)
該算法標記階段和Mark-Sweep一樣,但是在完成標記之后,它不是直接清理可回收對象,而是將存活對象都向一端移動,然后清理掉端邊界以外的內存。
所以,特別適用于存活對象多,回收對象少的情況下。
4. 分代回收算法
分代回收算法其實不算一種新的算法,而是根據復制算法和標記整理算法的的特點綜合而成。這種綜合是考慮到java的語言特性的。
這里重復一下兩種老算法的適用場景:
復制算法:適用于存活對象很少。回收對象多
標記整理算法: 適用用于存活對象多,回收對象少
剛好互補!不同類型的對象生命周期決定了更適合采用哪種算法。
于是,我們根據對象存活的生命周期將內存劃分為若干個不同的區域。一般情況下將堆區劃分為老年代(Old Generation)和新生代(Young Generation),老年代的特點是每次垃圾收集時只有少量對象需要被回收,而新生代的特點是每次垃圾回收時都有大量的對象需要被回收,那么就可以根據不同代的特點采取最適合的收集算法。
這就是分代回收算法。
現在回頭去看堆內存為什么要劃分新生代和老年代,是不是覺得如此的清晰和自然了?
我們再說的細一點:
1.對于新生代采取Copying算法,因為新生代中每次垃圾回收都要回收大部分對象,也就是說需要復制的操作次數較少,采用Copying算法效率最高。
2.由于老年代的特點是每次回收都只回收少量對象,一般使用的是Mark-Compact算法。
Eden空間和兩塊Survivor空間的工作流程
現在假定有新生代Eden,Survivor A, Survivor B三塊空間和老生代Old一塊空間。
// 分配了一個又一個對象
放到Eden區
// 不好,Eden區滿了,只能GC(新生代GC:Minor GC)了
把Eden區的存活對象copy到Survivor A區,然后清空Eden區(本來Survivor B區也需要清空的,不過本來就是空的)
// 又分配了一個又一個對象
放到Eden區
// 不好,Eden區又滿了,只能GC(新生代GC:Minor GC)了
把Eden區和Survivor A區的存活對象copy到Survivor B區,然后清空Eden區和Survivor A區
// 又分配了一個又一個對象
放到Eden區
// 不好,Eden區又滿了,只能GC(新生代GC:Minor GC)了
把Eden區和Survivor B區的存活對象copy到Survivor A區,然后清空Eden區和Survivor B區
// ...
// 有的對象來回在Survivor A區或者B區呆了比如15次,就被分配到老年代Old區
// 有的對象太大,超過了Eden區,直接被分配在Old區
// 有的存活對象,放不下Survivor區,也被分配到Old區
// ...
// 在某次Minor GC的過程中突然發現:
// 不好,老年代Old區也滿了,這是一次大GC(老年代GC:Major GC)
Old區慢慢的整理一番,空間又夠了
// 繼續Minor GC
// ...
// ...
觸發GC的類型
了解這些是為了解決實際問題,Java虛擬機會把每次觸發GC的信息打印出來來幫助我們分析問題,所以掌握觸發GC的類型是分析日志的基礎。
GC_FOR_MALLOC: 表示是在堆上分配對象時內存不足觸發的GC。
GC_CONCURRENT: 當我們應用程序的堆內存達到一定量,或者可以理解為快要滿的時候,系統會自動觸發GC操作來釋放內存。
GC_EXPLICIT: 表示是應用程序調用System.gc、VMRuntime.gc接口或者收到SIGUSR1信號時觸發的GC。
GC_BEFORE_OOM: 表示是在準備拋OOM異常之前進行的最后努力而觸發的GC。
程序如何與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.以下給出這種引用類型的使用偽代碼;
//申請一個圖像對象
Image image=new Image();//創建Image對象
…
//使用 image
…
//使用完了image,將它設置為soft 引用類型,并且釋放強引用;
SoftReference sr=new SoftReference(image);
image=null;
…
//下次使用時
if (sr!=null) image=sr.get();
else{
//由于GC由于低內存,已釋放image,因此需要重新裝載;
image=new 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運行。
來自:http://blog.csdn.net/lin_t_s/article/details/53557188