Java的內存回收——垃圾回收機制

3、垃圾回收機制

       垃圾回收機制主要完成以下兩件事情。
  1. 跟蹤並監控每個Java對象,當某個對象處於不可達狀態時,回收該對象所佔用的內存空間。
  2. 清理內存分配、回收過程中產生的內存碎片。
       垃圾回收機制需要完成的這兩方面工作工作量都不小,所以垃圾回收算法就成爲了限制Java程序運行效率的重要因素。實現高效JVM的一個重要方面就是提供高效的垃圾回收機制,高效的垃圾回收機制既能夠保證垃圾回收的快速運行,避免內存的分配和回收成爲應用程序的性能瓶頸,又不能導致應用程序產生停頓。

3、1 垃圾回收機制

       JVM垃圾回收機制判斷某個對象是否可以回收的唯一標準是:是否還有其他引用指向該對象?如果存在指向該對象,垃圾回收機制就不會回收該對象;否則,垃圾回收機制就會嘗試回收它。
       實際上,垃圾回收機制不可能實時監測到每個Java對象的狀態,因此當一個對象失去引用後,它也不會被立即回收,只有等垃圾回收機制運行時纔會被回收。
       對於一個垃圾回收器的設計算法來說,大致有如下可供選擇的設計。
  • 串行回收(Serial)和並行回收(Parallel):串行回收就是不管系統有多少個CPU,始終只用一個CPU來執行垃圾回收操作;而並行回收就是把整個回收工作拆分成多個部分,每個部分由一個CPU負責,從而讓多個CPU並行回收。並行回收的執行效率很高,但複雜度增加,另外也會產生一些副作用,比如內存碎片會增加。
  • 併發執行(Concurrent)和應用程序停止(Stop-the-world):應用程序停止的垃圾回收方式在執行垃圾回收的同時會導致應用程序暫停。併發執行的垃圾回收機制需要解決和應用程序的執行衝突,因此併發執行垃圾回收的系統開銷比應用程序停止更高,而且執行時也需要更多的內存。
  • 複製(Copying):將堆內存分爲兩個相同空間,從根開始訪問每一個關聯的可達對象,將所有的可達對象複製到另一塊相同的內存中。對於複製算法而言,因此只需要訪問所有的可達對象,將所有的可達對象複製完成後就回收整個空間,完全不用理會不可達對象,所以這種方式的優點就是垃圾回收過程不會產生碎片,遍歷空間的成本較小,但是缺點也是很明顯,需要複製數據和額外的內存。
  • 標記清除(mark-sweep):也就是不壓縮(Non-Compacting)回收方式。垃圾回收器先從根開始訪問所有的可達對象,將它們標記爲可達狀態,然後再遍歷一次整個內存區域,對所有的沒有標記爲可達狀態的對象進行回收處理。這種方式無須進行大規模的複製操作,而且內存利用率高,但是這種算法需要兩次遍歷內存空間,遍歷的成本較大,因此造成應用程序暫停的時間隨堆空間的大小線性增大,而且垃圾回收回來的內存往往不是連續的,因此整理後堆內存裏的碎片更多。
  • 標記壓縮(mark-sweep-compact):也就是壓縮(Compacting)回收方式。這種方式充分利用上述兩種算法的有點,垃圾回收器會先從根開始訪問所有可達對象,將它們標記爲可達狀態。接下來垃圾回收器會將這些活動對象搬遷在一起,這個過程也被稱爲內存壓縮,然後垃圾回收機制再次回收那些不可達對象所佔用的內存空間,這樣就避免了回收產生內存碎片。
       現行的垃圾回收器用分代的方式來採用不同的回收設計。分代的基本思路是根據對象生存時間的長短,把堆內存分成三個代:
  • Young
  • Old
  • Permanent
        垃圾回收器會根據不同代的特點採用不同的回收算法,從而充分利用各種算法的優點。

3、2 堆內存分代回收

       分代回收的一個依據即使對象生存時間的長短,然後根據不同代採取不同的垃圾回收策略。採用這種“分代回收”的策略基於如下兩點事實。
  • 絕大多數的對象不會被長時間引用,這些對象在其Young期間就會被回收。
  • 很老的對象(生存時間很長)和很新的對象(生存時間很短)之間很少存在相互引用的情況。
       根據上面兩點事實,對於Young代的對象而言,大部分對象都會很快就進入不可達狀態,只有少量的對象能熬到垃圾回收執行時,而垃圾回收器只需保留Young代中處於可達狀態的對象,如果採用複製算法只需要少量的複製成本,因此大部分垃圾回收器對Young代都採用複製算法

1、Young代
       對Young代採用複製算法只需遍歷那些處於可達狀態的對象,而且這些對象的數量很少,可複製成本也不大,因此可以充分發揮複製算法的優點。
       Young代由一個Eden區和兩個Survivor區構成。絕大多數對象先分配到Eden區中(有一些大的對象可能會直接被分配到Old代中),Survivor區中的對象都至少在Young代中經歷過一次垃圾回收,所以這些對象在被轉移到Old代之前會先保留在Survivor空間中。同一時間兩個Survivor空間中有一個用來保存對象,而另外一個是空的,用來在下次垃圾回收時保存Young代中的對象。每次複製就是將Eden和第一個Survivor區的可達對象複製到第二個Survivor區,然後清空Eden和第一個Survivor區。Young代的分區如下圖所示。


2、Old代
       如果Young代中的對象經過數次垃圾回收依然沒有被回收掉,即這個對象經過足夠長的時間還處於可達狀態,垃圾回收機制就會將這個對象轉移到Old代。隨着時間的流逝,Old代的對象會越來越多,因此Old代的空間要比Young代的空間更大。處於這兩點考慮,Old代的垃圾回收具有如下兩個特徵。
  • Old代垃圾回收的執行頻率無須太高,因此很少有對象死掉。
  • 每次對Old代執行垃圾回收都需要更長的時間來完成。
       基於以上考慮,垃圾回收器通常會使用標記壓縮算法,這算法可以避免複製Old代的大量對象,而且由於Old代的對象不會很快死亡,回收過程中不會大量地產生內存碎片,因此相對比較划算。

3、Permanent代
       Permanent代主要用於裝載Class、方法等消息,默認爲64MB,垃圾回收機制通常不會回收Permanent代中的對象。對於那些需要加載很多類的服務器程序,往往需要加大Permanent代的內存,否則可能會因爲內存不足而導致程序終止。
       當Young代的內存將要用完時,垃圾回收機制會對Young代進行垃圾回收,垃圾回收機制會採用較高的頻率對Young代進行掃描和回收。因爲這種回收的系統開銷比較小,因此被稱爲次要回收(minor collection)。當Old代的內存將要用完時,垃圾回收機制會進行全回收,也就是對Young代和Old代都要進行回收,此時回收成本就大得多了,因此也稱爲主要回收(major collection)。

3、3 常見的垃圾回收器

1、串行回收器(Serial Collector)
       串行回收器對Young代和Old代的回收都是串行的(只使用一個CPU),而且垃圾回收執行期間會使得應用程序產生暫停。具體策略爲:Young代採用串行復制算法,Old代採用串行標記壓縮算法
       假設程序Young代的內存分配示意圖如下所示。所有劃叉的區域代表表示不可達對象,空白區域代表可達對象。


       對於上圖所示的內存分配示意圖,垃圾回收器將會採用下圖所示的方式進行回收。

       系統將Eden區中的活動對象直接複製到初始爲空的Survivor區中(也就是To區),如果有些對象佔用空間特別大,垃圾回收器會直接將其複製到Old代中。
       對於Form Survivor區中的活動對象(該對象至少經歷過一次垃圾回收),到底是複製到To Survivor區中,還是複製到Old代中,則取決於這個對象的生存時間:如果這個對象的生存時間較長,它將複製到Old代中;否則,將被複制到To Survivor區中。
       完成上面複製之後,Eden和From Survivor區剩下的對象都是不可達對象,系統直接回收Eden區和From Survivor區的所有內存,而原來空的To Survivor區則保存了活動對象。在下一次回收時,原本的From Survivor區變爲To Survivor區,原本的To Survivor區則變爲From Survivor區。完成後的內存分配示意圖如下所示。

       串行回收器對Old代的回收採用串行、標記壓縮算法(mark-sweep-compact),這個算法有三個階段:mark(標識可達對象)、sweep(清除)、compact(壓縮)。在mark階段,回收器會識別出哪些對象仍然是可達的,在sweep階段將會回收不可達對象所佔用的內存空間,在compact階段回收器執行sliding compaction,把活動對象往Old代的前段移動,而在尾部保留一塊連續的空間,以便下次爲新對象分配內存空間。

2、並行回收器
       並行回收器對於Young代採用與串行回收器基本相似的回收算法,只是增加了多CPU並行的能力,即同時啓動多線程並行來執行垃圾回收。並行回收器對於Old代採用與串行回收器完全相同的回收算法,不管計算機有幾個CPU,並行回收器依然採用單線程、標記整理的方式進行回收。

3、並行壓縮回收器(Parallel Compacting Collector)
       並行壓縮算法是從JDK5 update 6開始引入的,它和並行回收器最大的不同是對Old代的回收是用來不同的算法。
       並行壓縮回收器對Young代採用與並行回收器完全相同的回收算法。並行壓縮回收器的改變主要是體現在對Old代的回收上,系統首先將Old代劃分爲幾個固定大小的區域。在mark階段,多個垃圾回收線程會並行標記Old代中的可達對象。當某個對象被標記爲可達對象時,還會更新該對象所在區域的大小,以及該對象的位置信息。在summary階段,直接操作Old代的區域,而不是單個的對象。由於每次垃圾回收的壓縮都是在Old代的左邊部分存儲大量的可達對象,對這樣的高密度可達對象的區域進行壓縮往往很不划算。所以summary階段會從最左邊的區域開始檢測每個區域的密度,當檢測到某個區域中能回收的空間達到某個數值時(也就是可達對象的密度較小時),垃圾回收器會判定該區域,以及該區域右邊的所有區域都應該進行回收,而該區域左邊的區域都會被標識爲密集區域,垃圾回收器既不會吧新對象移動到這些密集區域中,也不會對這些密集區域進行壓縮。summary階段目前還是串行操作,雖然可以使用並行方式實現,但是重要性不如對mark和壓縮階段的並行重要。
       最後是compact階段,回收器利用summary階段生成的數據識別出有哪些區域是需要裝填的,多個垃圾回收線程可以並行地將數據複製到這些區域中。經過這個過程後,Old代的一端會密集地存在大量的活動對象,另一端則存在大塊的空閒塊。

4、併發標識-清理(Mark-Sweep)回收器(CMS)
       CMS回收器對Young代的回收方式和並行回收器的回收方式完全相同。由於對Young代的回收依然採用複製回收算法,因此垃圾回收時依然會導致程序暫停,除非依靠多CPU並行來提高垃圾回收的速度。通常來說,建議適當加大Young代的內存。如果Young代的內存足夠大就不用頻繁地進行垃圾回收了,而且增大垃圾回收的時間間隔後可以讓更多的位於Young代中的Java對象自己死掉,從而避免複製。但將Young代的內存設置得過大也有一個壞處:當垃圾回收器回收Young代的內存時,複製成本會顯著上升(複製算法必須等Young代滿了之後纔開始回收),所以回收時會讓系統的暫停時間顯著加大。
       CMS對Old代的回收多數是併發操作,而不是並行操作。垃圾回收開始時需要一個短暫的暫停,此階段成爲初始標識(initial mark)階段,這個階段僅僅標識出那些被直接引用的可達對象。接下來進入併發標識階段(concurrent marking phase),垃圾回收器會依據在初始標識中發現的可達對象來尋找其他的可達對象。由於在併發標識階段應用程序會同時運行,無法保證所有的可達都被標識出來,因此應用程序會再次很短地暫停一下,多線程並行地重新標識之前可能因爲併發而漏掉的對象,這個階段被稱爲再標識(remark)階段。
       由於CMS不會進行內存壓縮,也就是說,不可達對象佔用的內存被回收以後,垃圾回收器不會移動可達對象佔用的內存。由於Old代的可用空間是不連續的,因此CMS垃圾會回收器必須保存一份可用空間的列表。當需要分配對象時,垃圾回收器就是要通過這份列表找到能容納新對象的空間,這樣就會使得分配內存時的效率下降,從而影響了Young代回收過程中將Young代
對象移動到Old代的效率。
       對於CMS回收器而言,當垃圾回收器執行併發標識時,應用程序在運行的同時也在分配對象,因此Old代也同時在增長。而且,雖然可達對象在標識階段會被識別出來,但有些在標識階段成爲垃圾的對象並不能立即被回收,只有等到下次垃圾回收時才能被回收。因此CMS回收器較之前的幾種回收器需要更大的堆內存。








發佈了46 篇原創文章 · 獲贊 44 · 訪問量 2萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章