1 簡單介紹下----->記憶體回收概念
GC中的垃圾,指的是存在於記憶體中的、不會再被使用的對象。而記憶體回收就是把那些不再被使用的對象進行清除,收回佔用的記憶體空間。如果不及時對記憶體中的垃圾進行清理,那麼這些垃圾對象所佔的記憶體空間會一直保留到應用程式結束,被保留的空間無法被其他對象使用。如果大量不會被使用的對象一致佔著空間不放,如果應用程式需要記憶體空間,沒有多餘的記憶體空間供其使用的話,就會導致記憶體溢出。因此,對記憶體空間的管理來說,識別和清理垃圾對象是至關重要的。
但是怎麼識別一個對象是否存活。。也就是可達的。依據什麼策略來判斷一個對象的可達性。。
在java中使用根搜尋演算法(GC Roots Tracing)判斷一個對象是否是可達的。演算法的基本思路就是通過一系列的根節點"GC Roots"的對象作為起始點,從這些節點開始向下搜尋,搜尋所走過的路徑稱為引用鏈,當一個對象到GC Roots沒有引用鏈相連時,則說明這個對象是不可達的。就會被判斷為可被回收的對象。
一般什麼樣的對象可以作為 GCRoots呢。
在java中以下幾種對象可以作為GCRoots:
1)虛擬機器棧(棧幀中的本地變數表)中引用的對象
2)方法區中的類靜態屬性引用的對象。
3)方法區中的常量引用的對象
4)本地方法棧中JNI(通常說的Native方法)引用的對象
2 重點圖解介紹----->記憶體回收演算法
下面首先會先介紹演算法的主要思想,然後會使用圖解的方式直觀演算法的工作流程。。。。。。。。。
重點掌握其演算法思想,以及其演算法的優缺點和適用情境。。。。。
(1) 引用計數法
引用計數法是最經典的一種記憶體回收演算法。其實現很簡單,對於一個A對象,只要有任何一個對象引用了A,則A的引用計算機就加1,當引用失效時,引用計數器減1.只要A的引用計數器值為0,則對象A就不可能再被使用。
雖然其思想實現都很簡單(為每一個對象配備一個整型的計數器),但是該演算法卻存在兩個嚴重的問題:
1) 無法處理循環參考的問題,因此在Java的記憶體回收行程中,沒有使用該演算法。
2) 引用計數器要求在每次因引用產生和消除的時候,需要伴隨一個加法操作和減法操作,對系統效能會有一定的影響。
一個簡單的循環參考問題描述:
對象A和對象B,對象A中含有對象B的引用,對象B中含有對象A的引用。此時對象A和B的引用計數器都不為0,但是系統中卻不存在任何第三個對象引用A和B。也就是說A和B是應該被回收的垃圾對象,但由於垃圾對象間的互相引用使得記憶體回收行程無法識別,從而引起記憶體流失(由於某種原因不能回收垃圾對象佔用的記憶體空間)。
如下圖:不可達對象出現循環參考,它的引用計數器不為0,
注意:由於引用計數器演算法存在循環參考以及效能的問題,java虛擬機器並未使用此演算法作為記憶體回收演算法。
【可達對象】:通過根對象的進行引用搜尋,最終可以到達的對象。
【不可達對象】:通過根對象進行引用搜尋,最終沒有被引用到的對象。
(2)標記清除法
標記清除法是現代記憶體回收演算法的思想基礎。
標記清除法將記憶體回收分為兩個階段:標記階段和清除階段。
在標記階段,首先通過根節點,標記所有從根節點開始的可達對象,因此未被標記的對象就是未被引用的垃圾對象。然後在清除階段,清除所有未被標記的對象。這種方法可以解決循環參考的問題,只有兩個對象不可達,即使它們互相引用也無濟於事。也是會被判定位不可達對象。
標記清除演算法可能產生的最大的問題就是空間片段。
如下圖所示,簡單描述了使用標記清除法對一塊連續的記憶體空間進行回收。
從根節點開始(在這裡僅顯示了兩個根節點),所有的有參考關聯性的對象均被標記為存活對象(箭頭表示引用)。從根節點起,不可達對象均為垃圾對象。在標記操作完成後,系統回收所有不可達對象。
從上圖可以看出,回收後的記憶體空間不再連續。在對象的對空間分配過程中,尤其是大對象的記憶體配置,不連續記憶體空間的工作效率要低於連續空間的,這也是該演算法的缺點。
注意:標記清除演算法先通過根節點標記所有可達對象,然後清除所有不可達對象,完成記憶體回收。後面會講到標記壓縮演算法,注意兩者的區別。。。。。。
(3) 複製演算法
演算法思想:將原有的記憶體空間分為兩塊相同的儲存空間,每次只使用一塊,在記憶體回收時,將正在使用的記憶體塊中存活對象複製到未使用的那一塊記憶體空間中,之後清除正在使用的記憶體塊中的所有對象,完成記憶體回收。
如果系統中的垃圾對象很多,複製演算法需要複製的存活對象就會相對較少(適用情境)。因此,在真正需要記憶體回收的時刻,複製演算法的效率是很高的。而且,由於存活對象在記憶體回收過程中是一起被賦值到另一塊記憶體空間中的,因此,可確保回收的記憶體空間是沒有片段的。(優點)
但是複製演算法的代價是將系統記憶體空間折半,只使用一半空間,而且如果記憶體空間中垃圾對象少的話,複製對象也是很耗時的,因此,單純的複製演算法也是不可取的。(缺點)
圖解演算法回收流程:
A、B兩塊相同的記憶體空間(原有記憶體空間折半得到的兩塊相同大小記憶體空間AB),A在進行記憶體回收,將存活的對象複製到B中,B中的空間在複製後保持連續。完成複製後,清空A。並將空間B設定為當前使用記憶體空間。
在java中的新生代串列記憶體回收行程中,使用了複製演算法的思想,新生代分為eden空間、from空間和to空間3個部分,其中from和to空間可以看做用於複製的兩塊大小相同、可互換角色的記憶體空間塊(同一時間只能有一個被當做當前記憶體空間使用,另一個在記憶體回收時才發揮作用),from和to空間也稱為survivor空間,用於存放未被回收的對象。
【新生代對象】:存放年輕對象的堆空間,年輕對象指剛剛建立,或者經曆記憶體回收次數不多的對象。
【老年代對象】:存放老年對象的堆空間。即為經曆多次記憶體回收依然存活的對象。
在記憶體回收時,eden空間中存活的對象會被複製到未使用的survivor空間中(圖中的to),正在使用的survivor空間(圖中的from)中的年輕對象也會被複製到to空間中(大對象或者老年對象會直接進入老年代,如果to空間已滿,則對象也會進入老年代)。此時eden和from空間中剩餘對象就是垃圾對象,直接清空,to空間則存放此次回收後存活下來的對象。
優點:這種複製演算法保證了記憶體空間的連續性,又避免了大量的空間浪費。
注意:複製演算法比較適用於新生代。因為在新生代中,垃圾對象通常會多於存活對象,演算法的效果會比較好。
(4) 標記壓縮演算法
複製演算法的高效性是建立在存活對象少、垃圾對象多的情況下,這種情況在新生代比較常見,
但是在老年代中,大部分對象都是存活的對象,如果還是有複製演算法的話,成本會比較高。因此,基於老年代這種特性,應該使用其他的回收演算法。
標記壓縮演算法是老年代的回收演算法,它在標記清除演算法的基礎上做了最佳化。(回憶一下,標記清除演算法的缺點,記憶體回收後記憶體空間不再連續,影響了記憶體空間的使用效率。。。)
和標記清除演算法一樣,標記壓縮演算法也首先從根節點開始,對所有可達的對象做一次標記,
但之後,它並不是簡單的清理未標記的對象,而是將所有的存活對象壓縮到記憶體空間的一端,之後,清理邊界外所有的空間。
這樣做避免的片段的產生,又不需要兩塊相同的記憶體空間,因此性價比高。
圖解其演算法工作過程:
通過根節點標記出所有的可達對象後,沿著虛線進行對象的移動,將所有的可達對象移到一端,並保持他們之間的參考關聯性,最後,清理邊界外的空間。
標記壓縮演算法的最終效果等同於標記清除演算法執行完成後,再進行一次記憶體片段的整理,因此也稱之為標記清除壓縮演算法。
(5) 分代演算法
前面介紹的記憶體回收演算法中,並沒有一種演算法可以完全替代其他演算法,各自具有自己的特點和優勢,因此需要根據垃圾對象的特性選擇合適的記憶體回收演算法。
分代演算法思想:將記憶體空間根據對象的特點不同進行劃分,選擇合適的記憶體回收演算法,以提高記憶體回收的效率。
通常,java虛擬機器會將所有的建立對象都放入稱為新生代的記憶體空間。
新生代的特點是:對象朝生夕滅,大約90%的對象會很快回收,因此,新生代比較適合使用複製演算法。
當一個對象經過幾次記憶體回收後依然存活,對象就會放入老年代的記憶體空間,在老年代中,幾乎所有的對象都是經過幾次記憶體回收後依然得以存活的,因此,認為這些對象在一段時間內,甚至在程式的整個生命週期將是常駐記憶體的。
老年代的存活率是很高的,如果依然使用複製演算法回收老年代,將需要複製大量的對象。這種做法是不可取的,根據分代的思想,對老年代的回收使用標記清除或者標記壓縮演算法可以提高記憶體回收效率。
注意:分代的思想被現有的虛擬機器廣泛使用,幾乎所有的記憶體回收行程都區分新生代和老年代。
對於新生代和老年代來說,通常新生代回收的頻率很高,但是每次回收的時間都很短,而老年代回收的頻率比較低,但是被消耗很多的時間。為了支援高頻率的新生代回收,虛擬機器可能使用一種叫做卡表的資料結構,卡表為一個位元位集合,每一個位元位可以用來表示老年代的某一地區中的所有對象是否持有新生代對象的引用,
這樣以來,新生代GC時,可以不用花大量時間掃描所有老年代對象,來確定每一個對象的參考關聯性,而可以先掃描卡表,只有當卡表的標記為1時,才需要掃描給定地區的老年代對象,而卡表為0的所在地區的老年代對象,一定不含有新生代對象的引用。
如下圖表示:
卡表中每一位表示老年代4KB的空間,卡表記錄為0的老年代地區沒有任何對象指向新生代,只有卡表為1的地區才有對象包含新生代對象的引用,因此在新生代GC時,只需要掃面卡表為1所在的老年代空間,使用這種方式,可以大大加快新生代的回收速度。
(6) 分區演算法
演算法思想:分區演算法將整個堆空間劃分為連續的不同小區間,
如圖所示:
每一個小區間都獨立使用,獨立回收。
演算法優點是:可以控制一次回收多少個小區間
通常,相同的條件下,堆空間越大,一次GC所需的時間就越長,從而產生的停頓時間就越長。為了更好的控制GC產生的停頓時間,將一塊大的記憶體地區分割成多個小塊,根據目標的停頓時間,每次合理的回收若干個小區間,而不是整個堆空間,從而減少一個GC的停頓時間。
from: http://blog.csdn.net/wen7280/article/details/54428387