天天看點

GC-垃圾收集算法與關鍵收集器

引言:

在筆者的上兩篇博文中,主要介紹了jvm的結構和對象的“生死”問題。今天主要來說說垃圾收集算法與各種關鍵的收集器,分析比較各種收集算法的優劣。如果時間和篇幅允許的話對記憶體動态配置設定做一些解釋,因為垃圾回收和動态配置設定是java的兩大基本特性。筆者目前整理的一些blog針對面試都是超高頻出現的。大家可以點選連結:http://blog.csdn.net/u012403290

技術點:

1、新生代GC(Minor GC):

上篇博文中,我介紹了新生代,老年代、永久代的概念,新生代的意思就是在這個群體中,具有“朝生夕死”的性質。Minor GC就是對新生代進行垃圾收集。因為新生代的特性,是以MInor GC也會異常頻繁。

2、老年代GC(Full GC/Major GC):

Full GC就是對老年代進行收集,收集的頻率相對較低,而且收集的時間和速度相對新生代GC來說慢了很多(據說是10倍以上)。

3、GC停頓:

指在垃圾回收的過程中,要停止所有的使用者線程。為了防止GC過程中對象的引用還在發生變化。換句話說就是在你打掃房間的時候,不允許别人繼續扔垃圾了。但是,這裡還是要說明一下,很多知道垃圾回收的小夥伴會說在CMS收集器(後面會讨論到)的時候幾乎不發生停頓(GC操作可與使用者線程并發)啊?其實不然,再讨論生死那篇博文中,我說過“可達性分析算法”判斷對象是否存活,在枚舉根節點的時候還是會發生停頓的。

4、安全點和安全區域:

意思就是當程式跑到這個安全點或者安全區域内才能發生垃圾回收。這樣的話系統的性能和資料安全性都能得到比較好的保障。

5、配置設定擔保機制:

是指在記憶體配置設定中,如果按照複制算法(下面會說道)進行配置設定記憶體的時候,當回收的時候發現把某一塊的存活對象拷貝到另外一塊的時候發現另外一塊的記憶體不足,這個時候就會觸發配置設定擔保機制,這些仍舊存活的對象會直接配置設定到老年代。也就是說你向銀行貸款需要有個擔保人是一樣的,如果你無力償還的時候,需要擔保人進行償還。

回收算法

在介紹回收算法的時候我會比較一下各個算法的優劣:

1、标記清除算法(Mark-Sweep):

最基礎回收算法之一。分為兩步,第一步先标記出要回收的對象,然後清除所有被标記的對象。這個算法很好了解,但是它存在着極大的缺陷:①效率不高;②因為如此的回收方式,會打亂記憶體對象本有的編排順序,容易造成記憶體碎片和連續的記憶體不足問題。比如說本來你左邊有一張桌子(一塊記憶體),本來坐着一個200斤的漢子(一個對象),然後這個漢子離開了(被清除),後來又過來一個妹子坐了你左邊(新對象進入),然後那個位置對于新來的妹子來說大了太多,有很多空間她都不需要(産生記憶體碎片),這樣一來空間就浪費了。這個時候呢,你右邊的位子本來坐了一個80斤的妹子(一個對象),後來這個妹子走了(對象被清除),後來來了一個200斤的漢子(新對象進入),發現完全坐不下了(沒有足夠大的記憶體給新對象配置設定),這個你可能就需要說“朋友,你去那邊坐吧,那邊空(又觸發了一次GC操作)”。不知道這麼說,大家能了解麼?

2、複制算法(copying):

最基礎算法之一。主要是将可用記憶體劃分為大小相等的兩塊。在使用的過程中就隻使用其中的一塊,如果使用的這一塊被使用完,在GC的過程中把存活對象指派到另一塊上去,并清理原來那塊。比如說上課的時候上一半,人滿坐不下,老師說大家去隔壁教室上課(GC操作),然後有些愛逃課的小夥伴就乘機溜走了(可回收的對象清除),在新的教室大家按順序一個個坐下來(把存活的對象都複制到另一塊記憶體),大家座位之間的空位也很合理(沒有了空間碎片和不足的問題)。複制算法的缺陷就在于如果按這麼操作,那麼就會有很大部分的記憶體在一定時間會被置空,使用率比較低。當然了,在jvm中對于這種的做法是分為1個Eden區和兩個Survivor區,兩者時間記憶體大小預設為8:1。對象進來先存在Eden區,回收的時候回把存活的對象拷貝到另外一塊Survivor區中(有人會問那還有一塊Survivor呢?是這樣的,兩個Survivor隻是進行了切換的操作,它沒有座位新來對象的存儲,他隻是作為在GC複制的時候進行對象轉換的作用)。如果在copy的過程中發現另外一塊Survivor的記憶體不足,則就會觸發配置設定擔保機制。

3、标記整理算法(Mark-Compact):

建立在标記清除算法的基礎之上,在标記操作執行之後,會讓仍舊存活的對象都向着一端移動,然後清理掉仍舊存活對象邊界以後的資料。

4、分代收集算法:

根據對象所屬的年齡(也就是新生代和老年代)進行不同的收集,比如說新生代“朝生夕死“的性質就可以用複制算法進行收集,而老年代中對象存活的時間就比較長,那就需要用标記清除或者标記整理來進行回收。

垃圾收集器

下圖是所有的垃圾收集器,橫線上邊為新生代收集器,橫線下邊為老年代收集器,橫線上表示既能收集新生代也能收集老年代。各個收集器之間的連結表示此兩者可以共同工作。作為垃圾收集器必須要配合新生代收集和老年代收集共同使用。

GC-垃圾收集算法與關鍵收集器

1、Serial收集器:

新生代收集器,采用複制算法進行垃圾收集。最古老的收集器,Serial表示串行的意思,也就是說該收集器是一個單線程的收集器。它在進行垃圾收集是必須暫停其他所有的使用者線程,GC停頓感很強。但是單線程收集效率很高,它不用考慮線程互動,專心收集垃圾。

2、ParNew:

新生代收集器,采用複制算法進行垃圾收集。該收集器可以多線程進行收集垃圾,相當于Serial收集器的多線程版本。

3、Parallel Scavenge收集器:

新生代收集器,采用複制算法進行垃圾收集。它也是可以進行多線程收集垃圾,但是它多了一個獨特的能力,引入了一個吞吐量(吞吐量=使用者代碼執行時間/(使用者代碼執行時間+GC所用時間))的概念,被稱為吞吐量優先收集器。它可以自動調節記憶體配置設定。

4、Serial Old收集器

老年代收集器,采用标記整理算法進行垃圾收集。和Serial收集器一樣是一個單線程收集器。

5、Parallel Old收集器

老年代收集器,采用标記整理算法進行垃圾收集。是一個多線程垃圾收集器。

6、CMS收集器(Concurrent Mark Sweep):

老年代收集器,采用标記清除算法進行垃圾收集。在前面提到過,它的GC停頓時間非常短,它主要有4個步驟進行垃圾收集:①初始标記;②并發标記;③重新标記;④并發清除。這裡我再描述一下每個狀态的具體情況。在初始标記的時候會産生GC停頓,它是單線程标記。在并發标記的過程中不會産生GC停頓,它可以與使用者操作線程進行并發,并不影響使用者操作,它主要是尋找引用鍊(在談論生死的部落格中有提到)的根節點。在重新标記的時候會産生GC停頓,它可以并行操作,主要是修正前面的标記過程中又變化的對象引用。再并發清理階段,可以與使用者操作線程并發處理,不産生GC停頓。仔細看我上面的描述,你會發現有并發和并行兩種概念,切不可混為一談。并發是GC線程與使用者線程同時操作,而并行的意思是各個GC線程可以同時操作。并發不存在GC停頓,而并行存在GC停頓。

7、G1收集器

據說是迄今為止最牛逼的收集器。它的GC停頓時間也非常短,主要有4個步驟:①初始标記;②并發标記;③最終标記;④篩選回收。在初始标記中會産生GC停頓,單線程進行标記。在并發标記中不會産生GC停頓,與使用者線程并發操作。在最終标記中,GC線程并行處理,會産生GC停頓。修正前面标記過程中導緻對象應用又變化的部分。在篩選回收會産生GC停頓,并行處理。

限于篇幅,GC收集器就寫這麼多。如果各位看官有興趣,可以針對每一個收集器進行深入探究。

對象配置設定記憶體

前面引言中提到,記憶體動态配置設定與垃圾回收機制是java的兩大特性。簡單說一下對象配置設定的一些規則:

1、前面在說複制算法的時候提到了Eden區,這個區具有優先配置設定權利。如果配置設定在這個區中的記憶體不足以新對象入住就會發生一次Minor GC操作。

2、如果新進入的對象所需連續的記憶體非常之大(典型的就是數組),那麼這個對象可能直接進入老年代中。

3、如果在回收機制進行多次回收之後,那些仍舊存活的對象将會進入老年代。有人會說,那麼jvm怎麼會知道這個對象存活了幾次GC呢?是可以知道的,在執行複制清理算法的時候,存活對象每移動一次,它的年齡就會+1,當年齡達到一定程度後就會放入老年代中。

好啦,總結一下這幾天寫的,都是比較理論的東西。貌似很多小夥伴都不感興趣。我想明天是不是要換換口味了,要不明天我們一起研究一下ConcurrentHashMap的資料結構和底層原理?

如果博文存在什麼問題,或者有什麼想法,可以聯系我呀,下面是我的微信二維碼:

GC-垃圾收集算法與關鍵收集器