一篇文章完全瞭解Java垃圾收集(GC)機制

垃圾收集(Garbage Collection ,GC),是一個長久以來就被思考的問題,當考慮GC的時候,咱們必須思考3件事情:程序員

  • 哪些內存須要回收?
  • 何時回收?
  • 如何回收?

那麼在Java中,咱們要怎麼來考慮GC呢?首先回想如下內存區域的劃分,其中程序計數器、本地方法棧、虛擬機棧三個區域隨線程而生,隨線程釋放,棧中的棧幀隨着方法的進入和退出執行着出棧和入棧的操做,每個棧幀分配多少內存基本是在類結構肯定時就已經固定的(可能會進行一些優化,可是大致上已知),所以這幾個區域就不須要考慮回收的問題,由於方法結束或者線程結束時,內存天然都被回收。不須要額外的GC算法等。算法

然而Java堆和方法區則不同,一個接口所對應的多個實現類所須要的內存可能不同,一個方法中的多個分支所須要的內存也可能不同,咱們只有在程序處於運行期間才能知道程序須要建立那些對象,這部分的內存的分配和回收是動態的,所以,垃圾收集器關注的是這方面的內存。數組

一. 如何肯定對象能夠回收

1.引用計數算法

  最容易想到與理解的算法,即對於每個對象,每當該對象被引用時,計數器值就+1,引用失效時,計數器就-1。所以,當對象的引用計數爲0時,即爲不可再被使用的。該算法也在一些領域被使用來進行內存管理,可是JAVA虛擬機中並無選用該算法。主要是由於不能很好的解決循環引用的問題。bash

舉個簡單的例子來講明循環引用:多線程

class Container{    public Object obj ;
}public class ReferTest {    public static void main(String[] args){
        Container c1 =new Container();
        Container c2 =new Container();
        c1.obj = c2 ;
        c2.obj = c1 ;
        
        c1 = null ;
        c2 = null ;        //此時c1 c1會被斷定爲死亡對象麼?    }
}
複製代碼

事實上會被斷定爲死亡對象,由於JAVA虛擬機不是採用引用計數來進行判斷的,所以若是發生垃圾回收,c1,c2 都會被回收內存。優化

2.可達性分析

Java、C#的主流實現都是採用該種方式,來判斷對象是否存活。spa

這個算法的基本思路就是一系列「GC Roots」做爲起始點,從這些節點向下搜索,搜索到的全部引用鏈中的對象都是可達的,其他的對象都是不可達的,如上例,即便c1,c2互相引用,可是c1,c2都不屬於GC Roots對象,所以都不可達。線程

Java中,如下幾種對象能夠做爲GC Roots:代理

  • 虛擬機棧(棧幀中的本地變量表)中引用的對象。
  • 本地方法棧JNI方法引用的對象。
  • 方法區類的靜態屬性引用的對象。
  • 方法區常量引用的對象。
3.引用的分類

瞭解了GC Roots以後,咱們可能會但願存在這麼一種對象,內存夠的時候不進行回收,當須要內存時再將其回收。JDK 1.2 中對引用進行了擴充。將引用分爲了4種,從強到弱依次爲;code

強引用(Strong Reference)

咱們通常狀況下使用的都是強引用,如Object o = new Object(),之類的代碼。只要強引用還在,垃圾收集器就永遠不會回收被引用的對象。

軟引用(Soft Reference)

SoftReference類來實現,用來描述一些還有用可是沒必要須的對象,在系統若是不回收就會發生OOM時纔會對軟引用進行內存回收。

弱引用(Weak Reference)

WeakReference類來實現,描述非必需的對象,強度弱,只能活到下一次發生垃圾回收前,不管那時內存是否短缺,都會對軟引用對象進行內存回收

虛引用(Phantom Reference)

PhantomReference類實現,不會對生存時間發生任何影響,惟一目的時能在這個對象被收集器回收時獲得一個通知。

4.其餘

及其不建議使用finalize()方法,雖然能夠在回收時被調用,可是finalize()方法的執行代價高昂,不肯定性大,沒法保證各個對象的調用順序。使用finalize()能作的工做,使用try()finally()或其餘方式能夠執行的更好。你們能夠忘記JAVA中有這個方法的存在。自己就是在JAVA剛誕生時向C/C++程序員作的妥協,可是未獲得優化。

方法區(永久代)進行GC的效率極低,花費較大,可是在大量使用反射、動態代理等場景都須要虛擬機具有類卸載的功能,以保證永生代的空間。

二.垃圾收集算法

1.標記清除算法(Mark-Sweep)

算法分爲兩個階段,標記與清除。

標記階段:標記出全部須要回收的對象。回收階段:將全部標記區域回收。因爲該算法不對空間進行整理,所以會產生大量的內存碎片,內存空間碎片過多會致使在分配較大的對象時,由於沒有連續的內存而不得不提早觸發一個GC。另外,標記與清除的過程效率都不高。這也是最基礎的GC算法。

2.複製算法(Copying)

將內存的總容量分爲兩塊,每次只使用其中的一塊,當這一塊用完了,觸發GC,此時將還存活的對象轉移到另外一塊內存中,以前使用的那一塊內存徹底清理掉。這樣每次對一個半區進行回收,也不會存在內存碎片,實現簡單,運行高效,可是一次只能使用半塊內存可能會形成浪費。

在新生代中,絕大部分的對象時「朝生夕死」的,所以,不須要按照1:1來劃分空間。而是將內存分爲一塊較大的Eden區以及兩個Survivor區,HotSpot虛擬機中,Eden:Survivor=8:1 ,每次使用一個Eden區以及一個Survivor區,90%的空間,觸發GC後,將剩餘的對象轉移到未使用的Survivor中,而後清理Eden區和用過的Survivor區,空間不夠時,會擔保分配到老年代。這樣一次可使用90%的內存空間,極大的提升了內存的使用率。所以,新生代通常採用這種算法來回收。

3.標記整理算法(Mark-Compact)

若是回收時空間內的對象存活率較高,那麼使用複製算法一次只能使用50%的空間(以應對全部對象都存活的狀況),所以老年代採用標記整理算法。先對須要清理的對象進行標記,而後將存活的對象都向一端移動,直接清理掉端邊界之外的內存。這種方式也不會留下內存碎片。

標記整理算法沒有複製算法快。

三. Java垃圾收集器

(瞭解便可,須要時能夠網上細查)

新生代收集器:Serial收集器、ParNew收集器(Serial的多線程版本)、Parallel Scanvenge收集器(控制吞吐量,提升相應速度)

老年代收集器:Serial Old收集器、Parallel Old收集器、CMS收集器(最短停頓)、G1(新生代、老年代均可回收)

四. 內存的分配與回收

新生代:即複製算法中提到的Eden區以及2個Survivor區。

老年代:新生代存活足夠長時間後進入老年代。堆上的另外一塊區域。

Minor GC:發生在新生代的垃圾收集動做。由於Java對象存活時間通常較短,故Minor GC很是頻繁,通常回收速度也較快。

Full GC:發生在老年代的垃圾收集動做,伴隨着最少一次的Minor GC,且速度較慢(比Minor GC慢10倍以上)

1.空間的分配

1)對象優先在新生代Eden區分配。當Eden區沒有足夠空間時,將發動一次Minor GC.

2)較大對象須要連續的空間,如長字符串或數組,若是放在新生代會提早觸發GC。故大對象直接進入老年代區域,避免頻繁的GC。

3)長期存活的對象進入老年代,每一個對象有一個年齡,在對象頭Mark Word中記錄,剛被建立時年齡爲0,當它活過一次Minor GC,而且轉移到Survivor中,年齡變爲1,此後,在Survivor區中每活過一個Minor GC,年齡就會+1,當年齡達到某個程度(默認爲15),就會晉升到老年代。

4)此外,爲了適應內存的複雜狀況,年齡不必定達到規定值才能進入老年代。當Survivor區的相同年齡全部對象大小大於Survivor區大小的一半時,此年齡就會被做爲斷定標準,大於等於該年齡的都會進入老年代。

2.空間的回收--GC

這裏我用一張圖來完全解釋清除:

須要解釋的地方有:擔保失敗,這個的做用在圖上已經解釋的很清楚了,能夠在JVM參數設置。

另一個地方就是平均大小來做比較,由於有多少對象晉升到老年代是沒法知道的,因此只好取以前每一次晉升到老年代的對象的容量的平均值大小來做爲經驗值,來決定是否進行Full GC來讓老年代騰出更多空間。若是仍然失敗,那麼只能進行一次Full GC。在我我的開來,之因此使用擔保,經驗值來儘量的只進行MinorGC,全部的一切,都是爲了儘量不執行Full GC的狀況下將須要申請的內存空間搞定

相關文章
相關標籤/搜索