Java的垃圾回收
GC也是一種內存管理吧,也許是因為第一次在Java中聽說這個名詞,所以涉及到Java的很多時候會被人問起。
首先,什么是垃圾?
直觀上看垃圾就是沒有用的東西,事實也是 這樣,前面分配過的內存如果在以后都不會被用到了那么就可以認為這類的內存是垃圾。從這個角度上看,任何在分配內存的地方都可能產生垃圾。大部分分配內存 的行為都是為了對象,那么大部分的垃圾也就產生在堆上了。但是類的信息保存也需要內存啊(況且也會對應一個Class對象),所以方法區上也應該會有垃圾 回收。
然后,如何識別垃圾?
很多的內存管理都是基于標記計數的(比如 內核里面),在引用的時候+1,在釋放的時候-1,在發現計數值為0的時候這塊內存就不再需要了。但是為什么很多地方都不會使用引用計數?最致命的問題引 用計數只是一個充分非必要條件,也就是說:引用計數為0的時候內存不會再被使用,而不會再被使用的內存引用計數不一定為0。最簡單的例子就是循環引用了:如果A引用了B,同時B又引用了A,那么這兩對象的計數永遠也都不會是0了(那么這兩個對象占用的空間也當然用于不會釋放)。那么什么是充分必要條件?很簡單,根據垃圾的定義去判斷就可以了:對象的訪問肯定是通過引用來實現的,如果訪問不到了那就說明是垃圾,那么在判斷的時候我們只需要順著引用去遍歷也次就可以找到所有正在使用的內存(剩下的就是垃圾了)。
在Java中可以作為根的對象包括以下幾種:
- 虛擬機棧中引用的對象;
- 方法區中的類靜態屬性引用的對象;
- 方法區中常量引用的對象;
- 本地方法棧中JNI的引用的對象;
那么,如何回收?
其實對象并非只有“有用”和“沒用”這兩種狀態,因為在GC之前還會有機會調用finalize方法,在改方法中對象可能復活(但是只能復活一次)。那么,指向對象的引用可能有以下四種:
- 強引用;
- 軟引用;
- 弱引用;
- 虛引用;
最簡單的回收算法是“標記-清除算法”,其實就是標記出來垃圾然后將其釋放:
從上圖中也非常容易看到這種算法的缺點:容易產生內存碎片,另一個不太明顯的缺點就是效率低。為什么效率低呢?因為堆上面大部分的對象是垃圾(至于為什么就不多說了),而在該算法中刪除的過程重要處理的就是這部分,所以會慢一點。
“復制算法”正好可以解決這個問題:
實現的時候將內存分成兩部分,每次只是用其中 的一部分,在進行垃圾回收的時候將正在使用的對象拷貝到另一半的內存中。但是,這樣很明顯會浪費很多的內存,那么怎么優化呢?還是從垃圾比較多的角度來 想。每次回收之后的對象占用的空間其實很小的,也就是只要很少的內存,也就是:
較大的一塊內存區域是Eden,還有兩個較小 的Survivor,具體的過程如上(兩個Surivior交替使用)。當然Survivor的大小可能不夠用,這個時候就需要“內存擔保”了。那么現在 再來看下垃圾回收的過程中還有沒有做一些無用功?堆上的一些對象可能有很長的生命周期(在Web應用中更是如此),那么這些對象就會被不停地在兩個 Survivor之間拷來拷去。這樣也就有了“分代收集算法”:
其實分代收集并沒有什么特別的地方,如果 一個對象的生命周期比較長,那么就把他放到老年代中。那怎么去預測一個對象的生命長短?一個很簡單的方法就是如果活了足夠久的時間,那么我們就認為這個對 象會繼續活很久(當然這個閥值可以去動態計算),當然也有可能剛進入老年代就變成垃圾了,不過這個概率應該很小。老年代是沒有內存擔保的(還有一個原因就 是老年代中的存活率很高),所以適合用“標記-清理”的方式來收集。
上面的這些算法都有一個共同的問題:垃圾回收都是全量的。如果全量收集消耗了很多的時間,那么用戶程序的體驗就會下降很多(尤其是搜索引擎)。那么每次不是全量地,而是只回收一部分的垃圾,這樣就能減少一次回收的時間(也就減少了用戶程序的間隔),那么就有了“火車算法”:
首先將內存分成不同的塊,回收也是針對塊來操作的,所以檢查的是有沒有外部的引用指向塊內的對象,主要的操作如下:
- 新的對象要不被打包成車廂掛到現有的火車尾部,或者當成一輛新的火車進站;
- 任何被其他車廂引用的對象都移出去;
- 如果存在來自非成熟對象空間的引用,將其移到其他的火車上去;
- 如果被其他火車引用,移動到對應的火車上去;
- 1、2完成之后,將被本列火車引用的對象移動到最后一節車廂去;
- 當然,可能編號最小的車廂整個就被回收了;
上面討論的都是回收算法,當然,算法都是去適應不同的場景,很少見一個算法能很好的處理各種情況。那么,如果利用這些算法來完成垃圾回收的任務則是垃圾回收器要干的事:
Serial Old收集器如下:
使用的算法是“標記-清理”,關鍵是單線程的。Parallel Old則僅僅是將Serial Old改成了多線程形式。
CMS做了很多讓垃圾回收和用戶進程并行的努力:
其中:
- 初始標記:只標記被GC Roots直接引用的對象,非常快;
- 并發標記:也就是GC Roots Tracing;
- 重新標記:修正并發標記期間由用戶程序改變的對象的標記記錄;
- 并發清理:清除垃圾對象;
- 重置線程:重置CMS的數據結構,等待下一次的垃圾回收;
CMS的優點是能使得最費時的標記和清理階段 和用戶線程并行,使得Stop The World(很酷的名字吧)的時間盡量變短,但這也是一把雙刃劍,由于占用了CPU資源用戶程序執行的速度當然是會變慢。另一個缺點是,CMS采用“標記 -清理”的算法,這樣就容易產生內存碎片。
來自:http://www.cnblogs.com/tianchi/archive/2012/12/11/2812145.html