Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> 細說JVM系列:自動內存管理內存回收:垃圾收集理論-垃圾收集算法

細說JVM系列:自動內存管理內存回收:垃圾收集理論-垃圾收集算法

編輯:關於Android編程

這裡主要講解垃圾收集理論上的算法,下一篇會介紹一些實現了這些算法的垃圾收集器。

一般我們談垃圾收集從三個問題來幫你理解jvm的垃圾收集策略:

1.怎麼判斷哪些內存是垃圾?
2.用什麼方法回收?
3.什麼時候回收?

垃圾回收的區域?

前面介紹了java內存運行時區域的各個部分,其中程序計數器、虛擬機棧、本地方法棧3個區域隨線程而生,隨線程而滅,棧中的棧幀隨著方法的進入和退出而有條不紊的執行著出棧和入棧操作。每一個棧幀中分配多少內存基本上都在類結構確定下來時就已知的,因此這幾個區域的內存分配和回收都具備確定性,在這幾個區域內都不需要過多考慮回收的問題,因為方法結束或者線程結束時,內存自然就跟隨著回收了。而java堆和方法區則不一樣,一個接口中的多個實現類需要的內存可能不一樣,一個方法中的多個分支需要的內存也可能不一樣,我們只能在程序處於運行期間時才能知道會創建哪些對象,這部分內存的分配和回收是動態的,垃圾收集器所關注的是這部分內存。

一.怎麼判斷哪些內存是垃圾?

1.引用計數法

給對象中添加一個引用計數器,每當有一個地方引用它時,計數器值就加1,當引用失效時,計數器就減1,任何時刻計數器為0的對象就是不可能再被使用的。

但是java虛擬機並沒有選用引用計數算法來管理內存,其中最重要的原因是它很難解決對象之間相互引用的問題。

public class MyObject {
    public Object ref = null;
    public static void main(String[] args) {
        MyObject myObject1 = new MyObject();
        MyObject myObject2 = new MyObject();
        myObject1.ref = myObject2;
        myObject2.ref = myObject1;
        myObject1 = null;
        myObject2 = null;
    }
}

\

2.可達性分析算法

在java中采用的是可達性分析算法來判定對象是否存活的。這個算法的基本思路就是:通過一系列的稱為”GC Roots”的對象作為起始點,從這些節點開始向下搜索,搜索所走過的路徑稱為引用鏈(Reference Chain),當一個對象到GC Roots沒有任何引用鏈相連(用圖論的話來說,就是從GC Roots到這個對象不可達)時,則證明此對象是不可用的。

\

在java語言中,可作為GC Roots的對象包括下面幾種:

虛擬機棧(棧幀的本地變量表)中引用的對象; 方法區中類靜態屬性引用的對象; 方法區中常量引用的對象; 本地方法棧中JNI(即一般說的native方法)引用的對象;

說明:

即使在可達性分析算法中不可達的對象,也並非是“非死不可”的,這時候他們暫時處於”緩刑“階段,要真正宣告一個對象的死亡,至少要經歷兩次標記過程:如果對象在進行可達性分析後發現沒有與GC Roots相連接的引用鏈,那它將會被第一次標記並且進行一次篩選,篩選的條件是此對象是否有必要執行finalize方法。當對象沒有覆蓋finalize方法,或者finalize方法已經被虛擬機調用過,虛擬機將這兩種情況都視為”沒有必要執行了“。

如果這個對象被判定為有必要執行finalize方法,那麼這個對象將會放置在一個叫做F-Queue的隊列之中,並在稍後由一個由虛擬機自動建立的、低優先級的Finalizer線程去執行它(即觸發這個方法)。

稍後GC將對F-Queue中的對象進行第二次小規模的標記,如果對象要在finalize中成功救贖自己——只要重新與引用鏈上的任何一個對象建立關聯即可,那麼第二次標記它將會被移除出”即將回收的“的集合,如果這個時候對象還沒有逃脫,那基本上他就真的被回收了。

finalize方法建議大家完全忘記掉,不要在平時的編碼中去調用。

3.回收方法區

方法區的垃圾回收主要回收兩部分內容:廢棄常量和無用的類。回收廢棄常量與回收java堆中的對象類似,也采用可達性。

回收無用的類條件就苛刻多了,類需要同時滿足下面的3個條件才能算是”無用的類“:

該類所有的實例都已經被回收,也就是java堆中不存在該類的任何實例。 加載該類的ClassLoader已經被回收。 該類對應的java.lang.Class對象沒有在任何地方呗引用,無法在任何地方通過訪問該類的方法。

二.什麼時候回收?

??一個應用程序中那個線程運行有線程自身的優先級來決定的。垃圾回收線程幾乎是所有線程中優先級最低的。

三.用什麼方法回收?

1.標記-清理算法

標記階段:標記出所有需要回收的對象,在標記完成後統一回收所有被標記的對象,前面已經提過。

清理階段:

該算法的不足:

1.效率問題。效率比較低。
2.空間問題,標記清理算法會產生大量的不連續的碎片。

2.復制算法

為了解決效率問題,復制算法出現了。

它將可用內存按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用完了,就將存活的對象復制到另外一塊上面,然後再把已使用過的內存空間一次清理掉。

??我們首先一起來看一下復制算法的做法,復制算法將內存劃分為兩個區間,在任意時間點,所有動態分配的對象都只能分配在其中一個區間(稱為活動區間),而另外一個區間(稱為空閒區間)則是空閒的。

??當有效內存空間耗盡時,JVM將暫停程序運行,開啟復制算法GC線程。接下來GC線程會將活動區間內的存活對象,全部復制到空閒區間,且嚴格按照內存地址依次排列,與此同時,GC線程將更新存活對象的內存引用地址指向新的內存地址。

??此時,空閒區間已經與活動區間交換,而垃圾對象現在已經全部留在了原來的活動區間,也就是現在的空閒區間。事實上,在活動區間轉換為空間區間的同時,垃圾對象已經被一次性全部回收。

??聽起來復雜嗎?

??其實一點也不復雜,有了上一章的基礎,相信各位理解這個算法不會費太多力氣。LZ給各位繪制一幅圖來說明問題,如下所示。

這裡寫圖片描述

??其實這個圖依然是上一章的例子,只不過此時內存被復制算法分成了兩部分,下面我們看下當復制算法的GC線程處理之後,兩個區域會變成什麼樣子,如下所示。

這裡寫圖片描述

??可以看到,1和4號對象被清除了,而2、3、5、6號對象則是規則的排列在剛才的空閒區間,也就是現在的活動區間之內。此時左半部分已經變成了空閒區間,不難想象,在下一次GC之後,左邊將會再次變成活動區間。

??很明顯,復制算法彌補了標記/清除算法中,內存布局混亂的缺點。不過與此同時,它的缺點也是相當明顯的。

??1、它浪費了一半的內存,這太要命了。
??2、如果對象的存活率很高,我們可以極端一點,假設是100%存活,那麼我們需要將所有對象都復制一遍,並將所有引用地址重置一遍。復制這一工作所花費的時間,在對象存活率達到一定程度時,將會變的不可忽視。

??所以從以上描述不難看出,復制算法要想使用,最起碼對象的存活率要非常低才行,而且最重要的是,我們必須要克服50%內存的浪費。

3.標記-整理算法

??標記/整理算法與標記/清除算法非常相似,它也是分為兩個階段:標記和整理。

??標記:它的第一個階段與標記/清除算法是一模一樣的,均是遍歷GC Roots,然後將存活的對象標記。

??整理:移動所有存活的對象,且按照內存地址次序依次排列,然後將末端內存地址以後的內存全部回收。因此,第二階段才稱為整理階段。

??它GC前後的圖示與復制算法的圖非常相似,只不過沒有了活動區間和空閒區間的區別,而過程又與標記/清除算法非常相似,我們來看GC前內存中對象的狀態與布局,如下圖所示。

這裡寫圖片描述

??這張圖其實與標記/清楚算法一模一樣,只是LZ為了方便表示內存規則的連續排列,加了一個矩形表示內存區域。倘若此時GC線程開始工作,那麼緊接著開始的就是標記階段了。此階段與標記/清除算法的標記階段是一樣一樣的,我們看標記階段過後對象的狀態,如下圖。

這裡寫圖片描述
??沒什麼可解釋的,接下來,便應該是整理階段了。我們來看當整理階段處理完以後,內存的布局是如何的,如下圖。
這裡寫圖片描述
??可以看到,標記的存活對象將會被整理,按照內存地址依次排列,而未被標記的內存會被清理掉。如此一來,當我們需要給新對象分配內存時,JVM只需要持有一個內存的起始地址即可,這比維護一個空閒列表顯然少了許多開銷。
??不難看出,標記/整理算法不僅可以彌補標記/清除算法當中,內存區域分散的缺點,也消除了復制算法當中,內存減半的高額代價,可謂是一舉兩得,一箭雙雕,一石兩鳥。
??不過任何算法都會有其缺點,標記/整理算法唯一的缺點就是效率也不高,不僅要標記所有存活對象,還要整理所有存活對象的引用地址。從效率上來說,標記/整理算法要低於復制算法。

4.分代收集算法

??當前的商業虛擬機的垃圾收集都采用“分代收集”算法。這種算法並沒有什麼新的意思,只是根據對象的存活周期的不同將內存劃分為幾塊。一般是把java堆分為新生代和老生代,這樣就可以根據各個年代的特點采用最適合的收集算法。在新生代中,每次垃圾回收時都發現有大批對象死去,只有少量存活,那就選用復制算法,只需要付出少量存活對象的復制成本就可以完成收集。而老生代中因為對象存活率高、沒有額外空間對它進行分配擔保,就必須使用“標記-清理”或“標記-整理”算法來進行回收。

5.算法對比

它們的共同點主要有以下兩點。

1、三個算法都基於根搜索算法去判斷一個對象是否應該被回收,而支撐根搜索算法可以正常工作的理論依據,就是語法中變量作用域的相關內容。因此,要想防止內存洩露,最根本的辦法就是掌握好變量作用域,而不應該使用前面內存管理雜談一章中所提到的C/C++式內存管理方式。 2、在GC線程開啟時,它們都要暫停應用程序(stop the world)。
??它們的區別LZ按照下面幾點來給各位展示。(>表示前者要優於後者,=表示兩者效果一樣)

效率:復制算法>標記/整理算法>標記/清除算法。
內存整齊度:復制算法=標記/整理算法>標記/清除算法。
內存利用率:標記/整理算法=標記/清除算法>復制算法。

可以看到標記/清除算法是比較落後的算法了,但是後兩種算法卻是在此基礎上建立的,俗話說“吃水不忘挖井人”,因此各位也莫要忘記了標記/清除這一算法前輩。

難道就沒有一種最優算法嗎?

當然是沒有的,這個世界是公平的,任何東西都有兩面性,試想一下,你怎麼可能找到一個又漂亮又勤快又有錢又通情達理,性格又合適,家境也合適,身高長相等等等等都合適的女人?就算你找到了,至少有一點這個女人也肯定不滿足,那就是多半不會恰巧又愛上了與LZ相似的各位苦逼猿友們。你是不是想說你比LZ強太多了,那LZ只想對你說,高富帥是不會爬在電腦前看技術文章的,0.0。

沒有最好的算法,只有最合適的算法。

既然這三種算法都各有缺陷,高人們自然不會容許這種情況發生。因此,高人們提出可以根據對象的不同特性,使用不同的算法處理,類似於蘿卜白菜各有所愛的原理。於是奇跡發生了,高人們終於找到了GC算法中的神級算法—–分代搜集算法。

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved