【jvm】GC與垃圾回收算法

GC

Garbage Collection GC 垃圾收集,在瞭解了jvm的內存區域以後,須要關心的問題就是垃圾收集了,由於咱們的內存是有限的,程序在運行中會不斷的產生新的對象佔用內存空間,因此咱們須要一個垃圾收集機制去回收內存java

在java內存運行時區域的各個部分,其中程序計數器,虛擬機棧,本地方法棧三個區域隨着線程的建立而建立,銷燬而銷燬,棧中的每一個棧幀分配多少內存基本上在類結構肯定下來是就抑制了,因此這幾個區域不須要過多考慮回收的問題,方法結束或者線程結束,內存天然就回收了,而在堆和方法區這兩塊區域中,咱們只有在程序運行期間才能知道會建立哪些對象,這部份內存的分配合回收都是動態的,因此咱們主要關注點在若是進行回收堆內存和方法區這兩塊區域的垃圾內存算法

對象是否存活

垃圾收集,咱們首先要判斷哪些對象是垃圾的對象jvm

引用計數算法: 每一個對象都添加一個引用計數器,當有地方引用它的時候,計數器就加1,當引用失效的時候計數器就減1,這樣經過這個引用計數器就能夠知道當前對象是否被引用,可是這種方式的弊端就是沒法解決循環引用的問題,假如a持有b的引用,b持有a的引用,兩個對象的計數器都是1,可是a和b這兩個對象只是被對方引用,假如這兩個對象都是垃圾對象,可是因爲計數器不爲零,因此沒法進行回收ide

可達性分析算法: 當一個對象到GC Roots沒有任何引用鏈相連的時候,就證實這個對象是不可達的對象this

因此這個GC Roots很重要,包括如下幾種:spa

  • 虛擬機棧中引用的對象
  • 方法區中類靜態屬性引用的對象,常量引用的對象
  • 本地方法棧中JNI引用的對象

4種引用

不管哪一種算法都須要判斷引用,jdk中存在着4種引用線程

  1. 強引用(Strong Reference)3d

    Object object = new Object();
    複製代碼

    當內存不夠時,程序會拋出異常,也不會進行回收強引用指向的對象code

  2. 軟引用(Soft Reference)cdn

    用來描述一些有用但非必須的對象,必要時能夠進行垃圾回收

    SoftReference<Object> softReference = new SoftReference<>(object);
    複製代碼

    當內存充足時,垃圾收集器不會回收弱引用指向的對象,當內存不足時,垃圾收集器纔會回收軟引用指向的對象

  3. 弱引用(Weak Reference)

    描述非必須對象

    WeakReference<Object> weakReference = new WeakReference<>(object);
    複製代碼

    每次垃圾收集時被回收

  4. 虛引用(Phantom Reference)

    這個引用類型強度最低,一個對象是否有虛引用對其生存週期沒有任何影響

    PhantomReference<Object> phantomReference = new PhantomReference<>(object, new ReferenceQueue<>());
    複製代碼

    每次垃圾收集時被回收

    虛引用必須和引用隊列(ReferenceQueue)聯合使用。當垃圾回收器準備回收一個對象時,若是發現它還有虛引用,就會在回收對象的內存以前,把這個虛引用加入到與之關聯的引用隊列中

二次標記

在發現不可達對象後,這對象也不是必定會回收,一個對象被回收,至少要經歷兩次標記過程

第一次:當對象不可達時被第一次標記

第二次:若是未可達對象,沒有覆蓋finalize()方法不須要執行finalize()方法,或者已經執行過finalize()方法了,此時進行第二次標記

若是未可達對象有必要執行finalize()方法,則會被放入一個F-Queue的隊列中,後續jvm會建立一個低優先級的線程去執行它,只要未可達對象在finalize()方法裏從新將本身賦予給某個類的對象或者對象的屬性,就能夠避免被垃圾回收

驗證:

/** * @author: chenmingyu * @date: 2019/9/18 18:19 * @description: */
public class FinalizeTest {

    private static FinalizeTest FINALIZE_TEST;

    public void test(){
        System.out.println("當前存活");
    }

    @Override
    protected void finalize() throws Throwable {
        FINALIZE_TEST = this;
        System.out.println("執行finalize方法");
    }

    public static void main(String[] args) throws Exception {

        FINALIZE_TEST = new FinalizeTest();
        FINALIZE_TEST = null;
        System.gc();
        TimeUnit.SECONDS.sleep(2L);
        if(FINALIZE_TEST != null){
            FINALIZE_TEST.test();
        } else {
            System.out.println("已死亡");
        }

        FINALIZE_TEST = null;
        System.gc();
        TimeUnit.SECONDS.sleep(2L);
        if(FINALIZE_TEST != null){
            FINALIZE_TEST.test();
        } else {
            System.out.println("已死亡");
        }
    }
}
複製代碼

輸出:

當一次手動調用gc時,FINALIZE_TEST對象被第一次標記,可是在執行finalize()方法時,從新將本身賦給了靜態變量,這樣這個對象就有從新有了強引用,避免了被回收

第二次手動調用gc時,FINALIZE_TEST對象被第一次標記,不在執行finalize()方法,由於finalize()方法只會被系統自動調用一次,因此以後不會再執行finalize()方法,進行了二次標記,而後對象被垃圾收集器回收

通過兩次標記以後,對象基本上就會被回收了

能夠本身將上面重寫finalize()方法去掉,本身試一下效果

方法區回收

對方法區的回收主要是對無用類的回收

無用類的條件:

  1. 該類的全部實例都已經被回收
  2. 加載該類的ClassLoader已經被回收
  3. 該類對應的Class對象沒有在任何地方被引用,沒法在任何地方經過反射訪問該類

方法區的垃圾回收性價比低,因此java虛擬機規範中要求虛擬機能夠不在方法區實現垃圾收集

垃圾收集算法

標記清除算法

算法分爲兩個部分:標記和清除

標記階段: 首先按照可達性分析,將GC Roots可達的對象進行標記,未被標記的對象就是須要回收的對象

清除階段: 在標記完成後統一回收全部未被標記的對象

這種算法適用於垃圾比較少的區域,好比老年代

缺點: 標記和清除過程效率都不高,回收後會產生大量不連續的內存碎片,空間碎片太多可能致使後續分配大對象時,沒法找到足夠的連續內存而觸發領另外一次GC

複製算法

複製算法將內存空間分爲大小相同的兩塊,每次只使用其中一塊,在垃圾回收時,將正在使用的內存中的存活對象複製到未使用的內存塊中,以後清除掉正在使用的內存中全部對象,交換兩塊內存的角色,周而復始

優勢:使用複製算法解決了標記清除算法的效率問題,分配內存時不用在考慮內存碎片的問題,按順序分配內存,運行效率高

缺點:內存可用率縮小爲原來的一半,若是對象存活率較高時,效率將會變低

這種算法適用於新生代

有研究代表,新生代中的對象有98%都是朝生夕死的,因此不須要按照1:1的比例來劃份內存,而是將內存分爲一塊較大的內存區域叫Eden區和兩塊較小的內存區域叫Survivor區,每次使用 Eden 空間和其中一塊 Survivor。在回收時,將 Eden 和 Survivor 中還存活着的對象一次性複製到另外一塊 Survivor 空間上,最後清理 Eden 和 使用過的那一塊 Survivor

HotSpot 虛擬機的 Eden 和 Survivor 的大小比例默認爲 8:1,保證了內存的利用率達到 90 %。若是每次回收有多於 10% 的對象存活,那麼一塊 Survivor 空間就不夠用了,此時須要依賴於老年代進行分配擔保,也就是借用老年代的空間,若是老年代的內存不夠用,就會觸發一次fullGC

標記整理算法

標記整理算法能夠分爲三個階段,第一標記階段,第二整理階段,第三清除

實現過程是首先進行標記,將存活的對象標記出來,在內存中把存活的對象往一端移動,直接回收邊界之外的內存,因此不會產生內存碎片,提升了內存的利用率,這種算法適用於老年代

缺點:效率不高,不只要標記存活對象還要整理全部存活對象的引用地址

分代收集算法

分代收集算法根據對象存活的生命週期不一樣將內存劃分爲不一樣的區域,通常是把堆分紅新生代和老年代,這樣就能夠根據各個年代的特色採用最合適的收集算法

新生代:新生代中每次垃圾收集都有大量垃圾對象須要回收,只有少許的對象存活,因此選擇複製算法是最高效的,只須要移動少許的對象便可

老年代:老年代中對象存活率高,沒有額外的空間對它進行分配擔保,因此能夠採用標記清除或者標記整理算法

參考:深刻理解java虛擬機第二版

更多閱讀:chenmingyu.top/categories/

相關文章
相關標籤/搜索