JVM與DVM ——(2)GC 回收機(jī)制與分代回收策略

垃圾回收(Garbage Collection魁巩,簡寫為 GC)可能是虛擬機(jī)眾多知識(shí)點(diǎn)中最為大眾所熟知的一個(gè)了,也是Java開發(fā)者最關(guān)注的一塊知識(shí)點(diǎn)。不同于C語言峻村,在Java 語言中,我們不需要手動(dòng)釋放對象的內(nèi)存锡凝,JVM 中的垃圾回收器(Garbage Collector)會(huì)為我們自動(dòng)回收粘昨。但是這種幸福是有代價(jià)的:一旦這種自動(dòng)化機(jī)制出錯(cuò),我們又不得不去深入理解 GC 回收機(jī)制私爷,甚至需要對這些“自動(dòng)化”的技術(shù)實(shí)施必要的監(jiān)控和調(diào)節(jié)雾棺。

Java 內(nèi)存運(yùn)行時(shí)區(qū)域的各個(gè)部分,其中程序計(jì)數(shù)器衬浑、虛擬機(jī)棧捌浩、本地方法棧 3 個(gè)區(qū)域隨線程而生,隨線程而滅工秩;棧中的棧幀隨著方法的進(jìn)入和退出而有條不紊地執(zhí)行著出棧和入棧操作尸饺,這幾個(gè)區(qū)域內(nèi)不需要過多考慮回收的問題进统。

而堆和方法區(qū)則不一樣,一個(gè)接口中的多個(gè)實(shí)現(xiàn)類需要的內(nèi)存可能不一樣浪听,一個(gè)方法中的多個(gè)分支需要的內(nèi)存也可能不一樣螟碎,我們只有在程序處于運(yùn)行期間時(shí)才能知道會(huì)創(chuàng)建哪些對象,這部分內(nèi)存的分配和回收都是動(dòng)態(tài)的迹栓,垃圾收集器所關(guān)注的就是這部分內(nèi)存掉分。

什么是垃圾

垃圾就是內(nèi)存中已經(jīng)沒有用的對象。 既然是”垃圾回收"克伊,那就必須知道哪些對象是垃圾酥郭。Java 虛擬機(jī)中使用一種叫作"可達(dá)性分析”的算法來決定對象是否可以被回收。

可達(dá)性分析

可達(dá)性分析算法是從離散數(shù)學(xué)中的圖論引入的愿吹,JVM 把內(nèi)存中所有的對象之間的引用關(guān)系看作一張圖不从,通過一組名為”GC Root"的對象作為起始點(diǎn),從這些節(jié)點(diǎn)開始向下搜索犁跪,搜索所走過的路徑稱為引用鏈椿息,最后通過判斷對象的引用鏈?zhǔn)欠窨蛇_(dá)來決定對象是否可以被回收。如下圖所示:

img

比如上圖中坷衍,對象A/B/C/D/E 與 GC Root 之間都存在一條直接或者間接的引用鏈寝优,這也代表它們與 GC Root 之間是可達(dá)的,因此它們是不能被 GC 回收掉的惫叛。而對象M和K雖然被對象 J 引用到倡勇,但是并不存在一條引用鏈連接它們與 GC Root,所以當(dāng) GC 進(jìn)行垃圾回收時(shí)嘉涌,只要遍歷到 J/K/M 這 3 個(gè)對象妻熊,就會(huì)將它們回收。

注意:上圖中圓形圖標(biāo)雖然標(biāo)記的是對象仑最,但實(shí)際上代表的是此對象在內(nèi)存中的引用扔役。包括 GC Root 也是一組引用而并非對象。

GC Root 對象

在 Java 中警医,有以下幾種對象可以作為 GC Root:

  1. Java 虛擬機(jī)棧(局部變量表)中的引用的對象亿胸。
  2. 方法區(qū)中靜態(tài)引用指向的對象。
  3. 仍處于存活狀態(tài)中的線程對象预皇。
  4. Native 方法中 JNI 引用的對象侈玄。

什么時(shí)候回收

不同的虛擬機(jī)實(shí)現(xiàn)有著不同的 GC 實(shí)現(xiàn)機(jī)制,但是一般情況下每一種 GC 實(shí)現(xiàn)都會(huì)在以下兩種情況下觸發(fā)垃圾回收吟温。

  1. Allocation Failure:在堆內(nèi)存中分配時(shí)序仙,如果因?yàn)榭捎檬S嗫臻g不足導(dǎo)致對象內(nèi)存分配失敗,這時(shí)系統(tǒng)會(huì)觸發(fā)一次 GC鲁豪。
  2. System.gc():在應(yīng)用層潘悼,Java 開發(fā)工程師可以主動(dòng)調(diào)用此 API 來請求一次 GC律秃。

代碼驗(yàn)證 GC Root 的幾種情況

了解了 Java 中的 GC Root,以及何時(shí)觸發(fā) GC治唤,接下來就通過幾個(gè)案例來驗(yàn)證 GC Root 的情況棒动。在看具體代碼之前,我們先了解一個(gè)執(zhí)行 Java 命令時(shí)的參數(shù)宾添。

-Xms 初始分配 JVM 運(yùn)行時(shí)的內(nèi)存大小船惨,如果不指定默認(rèn)為物理內(nèi)存的 1/64。

比如我們運(yùn)行如下命令執(zhí)行 HelloWorld 程序缕陕,從物理內(nèi)存中分配出 200M 空間分配給 JVM 內(nèi)存掷漱。

java -Xms200m HelloWorld

驗(yàn)證虛擬機(jī)棧(棧幀中的局部變量)中引用的對象作為 GC Root

運(yùn)行如下代碼:

public class GCRootLocalVariable {
    private int _10MB = 10 * 1024 * 1024;
    private byte[] memory = new byte[8 * _10MB];
    
    public static void main(String[] args){
        System.out.println("開始時(shí):");
        printMemory();
        method();
        System.gc();
        System.out.println("第二次GC完成");
        printMemory();
    }
    
    public static void method() {
        GCRootLocalVariable g = new GCRootLocalVariable();
        System.gc();
        System.out.println("第一次GC完成");
        printMemory();
    }

    /**
     * 打印出當(dāng)前JVM剩余空間和總的空間大小
     */
    public static void printMemory() {
        System.out.print("free is " + Runtime.getRuntime().freeMemory()/1024/1024 + " M, ");
        System.out.println("total is " + Runtime.getRuntime().totalMemory()/1024/1024 + " M, ");
    }
}

打印日志:

開始時(shí):
free is 242 M, total is 245 M,
第一次GC完成
free is 163 M, total is 245 M,
第二次GC完成
free is 243 M, total is 245 M,

可以看出:

  • 當(dāng)?shù)谝淮?GC 時(shí),g 作為局部變量榄檬,引用了 new 出的對象(80M),并且它作為 GC Roots衔统,在 GC 后并不會(huì)被 GC 回收鹿榜。
  • 當(dāng)?shù)诙?GC:method() 方法執(zhí)行完后,局部變量 g 跟隨方法消失锦爵,不再有引用類型指向該 80M 對象舱殿,所以第二次 GC 后此 80M 也會(huì)被回收。

注意:上面日志包括后面的實(shí)例中险掀,因?yàn)橛兄虚g變量沪袭,所以會(huì)有 1M 左右的誤差,但不影響我們分析 GC 過程樟氢。

驗(yàn)證方法區(qū)中的靜態(tài)變量引用的對象作為 GC Root

運(yùn)行如下代碼:

public class GCRootStaticVariable{
    private static int _10MB = 10 * 1024 * 1024;
    private byte[] memory;
    private static GCRootStaticVariable staticVariable;

    public GCRootStaticVariable(int size) {
        memory = new byte[size];
    }

    public static void main(String[] args){
        System.out.println("程序開始:");
        printMemory();
        GCRootStaticVariable g = new GCRootStaticVariable(4 * _10MB);
        g.staticVariable = new GCRootStaticVariable(8 * _10MB);
        // 將g置為null, 調(diào)用GC時(shí)可以回收此對象內(nèi)存
        g = null;
        System.gc();
        System.out.println("GC完成");
        printMemory();
    }

    /**
     * 打印出當(dāng)前JVM剩余空間和總的空間大小
     */
    public static void printMemory() {
        System.out.print("free is " + Runtime.getRuntime().freeMemory()/1024/1024 + " M, ");
        System.out.println("total is " + Runtime.getRuntime().totalMemory()/1024/1024 + " M, ");
    }
}

打印日志:

程序開始:
free is 242 M, total is 245 M,
GC完成
free is 163 M, total is 245 M,

可以看出:

程序剛開始運(yùn)行時(shí)內(nèi)存為 242M冈绊,并分別創(chuàng)建了 g 對象(40M),同時(shí)也初始化 g 對象內(nèi)部的靜態(tài)變量 staticVariable 對象(80M)埠啃。當(dāng)調(diào)用 GC 時(shí)死宣,只有 g 對象的 40M 被 GC 回收掉,而靜態(tài)變量 staticVariable 作為 GC Root碴开,它引用的 80M 并不會(huì)被回收毅该。

驗(yàn)證活躍線程作為 GC Root

運(yùn)行如下代碼:

public class GCRootThread{
    private int _10MB = 10 * 1024 * 1024;
    private byte[] memory = new byte[8 * _10MB];

    public static void main(String[] args) throws Exception {
        System.out.println("開始前內(nèi)存情況:");
        printMemory();
        AsyncTask at = new AsyncTask(new GCRootThread());
        Thread thread = new Thread(at);
        thread.start();
        System.gc();
        System.out.println("main方法執(zhí)行完畢,完成GC");
        printMemory();
        thread.join();
        at = null;
        System.gc();
        System.out.println("線程代碼執(zhí)行完畢潦牛,完成GC");
        printMemory();
    }

    /**
     * 打印出當(dāng)前JVM剩余空間和總的空間大小
     */
    public static void printMemory() {
        System.out.print("free is " + Runtime.getRuntime().freeMemory()/1024/1024 + " M, ");
        System.out.println("total is " + Runtime.getRuntime().totalMemory()/1024/1024 + " M, ");
    }

    private static class AsyncTask implements Runnable {
        private GCRootThread gcRootThread;

        public AsyncTask(GCRootThread gcRootThread){
            this.gcRootThread = gcRootThread;
        }
        
        @Override
        public void run() {
            try{
                Thread.sleep(500);
            } catch(Exception e){}
        }
    }
}
開始前內(nèi)存情況:
free is 242 M, total is 245 M,
main方法執(zhí)行完畢眶掌,完成GC
free is 163 M, total is 245 M,
線程代碼執(zhí)行完畢,完成GC
free is 243 M, total is 245 M,

程序剛開始時(shí)是 242M 內(nèi)存巴碗,當(dāng)調(diào)用第一次 GC 時(shí)線程并沒有執(zhí)行結(jié)束朴爬,并且它作為 GC Root,所以它所引用的 80M 內(nèi)存并不會(huì)被 GC 回收掉良价。 thread.join() 保證線程結(jié)束再調(diào)用后續(xù)代碼寝殴,所以當(dāng)調(diào)用第二次 GC 時(shí)蒿叠,線程已經(jīng)執(zhí)行完畢并被置為 null,這時(shí)線程已經(jīng)被銷毀蚣常,所以之前它所引用的 80M 此時(shí)會(huì)被 GC 回收掉市咽。

測試成員變量是否可作為 GC Root

運(yùn)行如下代碼:

public class GCRootClassVariable{
    private static int _10MB = 10 * 1024 * 1024;
    private byte[] memory;
    private GCRootClassVariable classVariable;
    public GCRootClassVariable(int size){
        memory = new byte[size];
    }
    public static void main(String[] args){
        System.out.println("程序開始:");
        printMemory();
        GCRootClassVariable g = new GCRootClassVariable(4 * _10MB);
        g.classVariable = new GCRootClassVariable(8 * _10MB);
        g = null;
        System.gc();
        System.out.println("GC完成");
        printMemory();
    }
    /**
     * 打印出當(dāng)前JVM剩余空間和總的空間大小
     */
    public static void printMemory() {
        System.out.print("free is " + Runtime.getRuntime().freeMemory()/1024/1024 + " M, ");
        System.out.println("total is " + Runtime.getRuntime().totalMemory()/1024/1024 + " M, ");
    }
}

打印日志:

程序開始:
free is 242 M, total is 245 M,
GC完成
free is 243 M, total is 245 M,

從上面日志中可以看出當(dāng)調(diào)用 GC 時(shí),因?yàn)?g 已經(jīng)置為 null抵蚊,因此 g 中的全局變量 classVariable 此時(shí)也不再被 GC Root 所引用施绎。所以最后 g(40M) 和 classVariable(80M) 都會(huì)被回收掉。這也表明全局變量同靜態(tài)變量不同贞绳,它不會(huì)被當(dāng)作 GC Root谷醉。

上面演示的這幾種情況往往也是內(nèi)存泄漏發(fā)生的場景,設(shè)想一下我們將各個(gè) Test 類換成 Android 中的 Activity 的話將導(dǎo)致 Activity 無法被系統(tǒng)回收冈闭,而一個(gè) Activity 中的數(shù)據(jù)往往是較大的俱尼,因此內(nèi)存泄漏導(dǎo)致 Activity 無法回收還是比較致命的。

如何回收垃圾

垃圾收集算法的實(shí)現(xiàn)涉及大量的程序細(xì)節(jié)萎攒,各家虛擬機(jī)廠商對其實(shí)現(xiàn)細(xì)節(jié)各不相同遇八,因此本文并不過多的討論算法的實(shí)現(xiàn),只是介紹幾種算法的思想以及優(yōu)缺點(diǎn)耍休。

標(biāo)記清除算法(Mark and Sweep GC)

從”GC Roots”集合開始刃永,將內(nèi)存整個(gè)遍歷一次,保留所有可以被 GC Roots 直接或間接引用到的對象羊精,而剩下的對象都當(dāng)作垃圾對待并回收斯够,過程分兩步。

  1. Mark 標(biāo)記階段:找到內(nèi)存中的所有 GC Root 對象喧锦,只要是和 GC Root 對象直接或者間接相連則標(biāo)記為灰色(也就是存活對象)读规,否則標(biāo)記為黑色(也就是垃圾對象)。
  2. Sweep 清除階段:當(dāng)遍歷完所有的 GC Root 之后裸违,則將標(biāo)記為垃圾的對象直接清除掖桦。

如下圖所示:

img
  • 優(yōu)點(diǎn):實(shí)現(xiàn)簡單,不需要將對象進(jìn)行移動(dòng)供汛。
  • 缺點(diǎn):這個(gè)算法需要中斷進(jìn)程內(nèi)其他組件的執(zhí)行(stop the world)枪汪,并且可能產(chǎn)生內(nèi)存碎片,提高了垃圾回收的頻率怔昨。

復(fù)制算法(Copying)

將現(xiàn)有的內(nèi)存空間分為兩快雀久,每次只使用其中一塊,在垃圾回收時(shí)將正在使用的內(nèi)存中的存活對象復(fù)制到未被使用的內(nèi)存塊中趁舀。之后赖捌,清除正在使用的內(nèi)存塊中的所有對象,交換兩個(gè)內(nèi)存的角色,完成垃圾回收越庇。

  1. 復(fù)制算法之前罩锐,內(nèi)存分為 A/B 兩塊,并且當(dāng)前只使用內(nèi)存 A卤唉,內(nèi)存的狀況如下圖所示:
img
  1. 標(biāo)記完之后涩惑,所有可達(dá)對象都被按次序復(fù)制到內(nèi)存 B 中,并設(shè)置 B 為當(dāng)前使用中的內(nèi)存桑驱。內(nèi)存狀況如下圖所示:
img
  • 優(yōu)點(diǎn):按順序分配內(nèi)存即可竭恬,實(shí)現(xiàn)簡單、運(yùn)行高效熬的,不用考慮內(nèi)存碎片痊硕。
  • 缺點(diǎn):可用的內(nèi)存大小縮小為原來的一半,對象存活率高時(shí)會(huì)頻繁進(jìn)行復(fù)制押框。

標(biāo)記-壓縮算法 (Mark-Compact)

需要先從根節(jié)點(diǎn)開始對所有可達(dá)對象做一次標(biāo)記岔绸,之后,它并不簡單地清理未標(biāo)記的對象橡伞,而是將所有的存活對象壓縮到內(nèi)存的一端亭螟。最后,清理邊界外所有的空間骑歹。因此標(biāo)記壓縮也分兩步完成:

  1. Mark 標(biāo)記階段:找到內(nèi)存中的所有 GC Root 對象,只要是和 GC Root 對象直接或者間接相連則標(biāo)記為灰色(也就是存活對象)墨微,否則標(biāo)記為黑色(也就是垃圾對象)道媚。
  2. Compact 壓縮階段:將剩余存活對象按順序壓縮到內(nèi)存的某一端。
img
  • 優(yōu)點(diǎn):這種方法既避免了碎片的產(chǎn)生翘县,又不需要兩塊相同的內(nèi)存空間最域,因此,其性價(jià)比比較高锈麸。
  • 缺點(diǎn):所謂壓縮操作镀脂,仍需要進(jìn)行局部對象移動(dòng),所以一定程度上還是降低了效率忘伞。

JVM分代回收策略

Java 虛擬機(jī)根據(jù)對象存活的周期不同薄翅,把堆內(nèi)存劃分為幾塊,一般分為新生代氓奈、老年代翘魄,這就是 JVM 的內(nèi)存分代策略。注意: 在 HotSpot 中除了新生代和老年代舀奶,還有永久代

分代回收的中心思想就是:對于新創(chuàng)建的對象會(huì)在新生代中分配內(nèi)存暑竟,此區(qū)域的對象生命周期一般較短。如果經(jīng)過多次回收仍然存活下來育勺,則將它們轉(zhuǎn)移到老年代中但荤。

新生代(Young Generation)

新生成的對象優(yōu)先存放在新生代中罗岖,新生代對象朝生夕死,存活率很低腹躁,在新生代中桑包,常規(guī)應(yīng)用進(jìn)行一次垃圾收集一般可以回收 70%~95% 的空間,回收效率很高潜慎。新生代中因?yàn)橐M(jìn)行一些復(fù)制操作捡多,所以一般采用的 GC 回收算法是復(fù)制算法。

新生代又可以繼續(xù)細(xì)分為 3 部分:Eden铐炫、Survivor0(簡稱 S0)垒手、Survivor1(簡稱S1)。這 3 部分按照 8:1:1 的比例來劃分新生代倒信。這 3 塊區(qū)域的內(nèi)存分配過程如下:

絕大多數(shù)剛剛被創(chuàng)建的對象會(huì)存放在 Eden 區(qū)科贬。如圖所示:

img

當(dāng) Eden 區(qū)第一次滿的時(shí)候,會(huì)進(jìn)行垃圾回收鳖悠。首先將 Eden區(qū)的垃圾對象回收清除榜掌,并將存活的對象復(fù)制到 S0,此時(shí) S1是空的乘综。如圖所示:

img

下一次 Eden 區(qū)滿時(shí)憎账,再執(zhí)行一次垃圾回收。此次會(huì)將 EdenS0區(qū)中所有垃圾對象清除卡辰,并將存活對象復(fù)制到 S1胞皱,此時(shí) S0變?yōu)榭铡H鐖D所示:

img

如此反復(fù)在 S0S1之間切換幾次(默認(rèn) 15 次)之后九妈,如果還有存活對象反砌。說明這些對象的生命周期較長,則將它們轉(zhuǎn)移到老年代中萌朱。如圖所示:

img

老年代(Old Generation)

一個(gè)對象如果在新生代存活了足夠長的時(shí)間而沒有被清理掉宴树,則會(huì)被復(fù)制到老年代。老年代的內(nèi)存大小一般比新生代大晶疼,能存放更多的對象酒贬。如果對象比較大(比如長字符串或者大數(shù)組),并且新生代的剩余空間不足翠霍,則這個(gè)大對象會(huì)直接被分配到老年代上同衣。

我們可以使用 -XX:PretenureSizeThreshold 來控制直接升入老年代的對象大小,大于這個(gè)值的對象會(huì)直接分配在老年代上壶运。老年代因?yàn)閷ο蟮纳芷谳^長耐齐,不需要過多的復(fù)制操作,所以一般采用標(biāo)記壓縮的回收算法。

注意:對于老年代可能存在這么一種情況埠况,老年代中的對象有時(shí)候會(huì)引用到新生代對象耸携。這時(shí)如果要執(zhí)行新生代 GC,則可能需要查詢整個(gè)老年代上可能存在引用新生代的情況辕翰,這顯然是低效的夺衍。所以,老年代中維護(hù)了一個(gè) 512 byte 的 card table喜命,所有老年代對象引用新生代對象的信息都記錄在這里沟沙。每當(dāng)新生代發(fā)生 GC 時(shí),只需要檢查這個(gè) card table 即可壁榕,大大提高了性能矛紫。

GC Log 分析

為了讓上層應(yīng)用開發(fā)人員更加方便的調(diào)試 Java 程序,JVM 提供了相應(yīng)的 GC 日志牌里。在 GC 執(zhí)行垃圾回收事件的過程中颊咬,會(huì)有各種相應(yīng)的 log 被打印出來。其中新生代和老年代所打印的日志是有區(qū)別的牡辽。

  • 新生代 GC:這一區(qū)域的 GC 叫作 Minor GC喳篇。因?yàn)?Java 對象大多都具備朝生夕滅的特性,所以 Minor GC 非常頻繁态辛,一般回收速度也比較快麸澜。
  • 老年代 GC:發(fā)生在這一區(qū)域的 GC 也叫作 Major GC 或者 Full GC。當(dāng)出現(xiàn)了 Major GC奏黑,經(jīng)常會(huì)伴隨至少一次的 Minor GC痰憎。

注意:在有些虛擬機(jī)實(shí)現(xiàn)中,Major GC 和 Full GC 還是有一些區(qū)別的攀涵。Major GC 只是代表回收老年代的內(nèi)存,而 Full GC 則代表回收整個(gè)堆中的內(nèi)存洽沟,也就是新生代 + 老年代以故。

接下來就通過幾個(gè)案例來分析如何查看 GC Log,分析這些 GC Log 的過程中也能再加深對 JVM 分代策略的理解裆操。

首先我們需要理解幾個(gè) Java 命令的參數(shù):

img

使用如下代碼怒详,在內(nèi)存中創(chuàng)建 4 個(gè) byte 類型數(shù)組來演示內(nèi)存分配與 GC 的詳細(xì)過程。代碼如下:

/**
* VM agrs: -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails
* -XX:SurvivorRatio=8
*/
public class MinorGCTest {
    private static final int _1MB = 1024 * 1024;
    public static void testAllocation() {
        byte[] a1, a2, a3, z4;
        a1 = new byte[2 * _1MB];
        a2 = new byte[2 * _1MB];
        a3 = new byte[2 * _1MB];
        a4 = new byte[1 * _1MB];
    }
    public static void main(String[] agrs) {
        testAllocation();
    }
}

通過上面的參數(shù)踪区,可以看出堆內(nèi)存總大小為 20M昆烁,其中新生代占 10M,剩下的 10M 會(huì)自動(dòng)分配給老年代缎岗。執(zhí)行上述代碼打印日志如下:

Heap
PSYoungGen      total 9216K, used 8003K [0x00000007bf600000, 0x00000007c0000000, 0x00000007c0000000)
  eden space 8192K, 97% used [0x00000007bf600000,0x00000007bfdd0ed8,0x00000007bfe00000)
  from space 1024K, 0% used [0x00000007bff00000,0x00000007bff00000,0x00000007c0000000)
  to   space 1024K, 0% used [0x00000007bfe00000,0x00000007bfe00000,0x00000007bff00000)
ParOldGen       total 10240K, used 0K [0x00000007bec00000, 0x00000007bf600000, 0x00000007bf600000)
  object space 10240K, 0% used [0x00000007bec00000,0x00000007bec00000,0x00000007bf600000)
Metaspace       used 2631K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 286K, capacity 386K, committed 512K, reserved 1048576K

日志中的各字段代表意義如下:

img

從日志中可以看出:程序執(zhí)行完之后静尼,a1、a2、a3鼠渺、a4 四個(gè)對象都被分配在了新生代的 Eden 區(qū)鸭巴。

如果我們將測試代碼中的 a4 初始化改為 a4 = new byte[2 * _1MB] 則打印日志如下:

[GC (Allocation Failure) [PSYoungGen: 6815K->480K(9216K)] 6815K->6632K(19456K), 0.0067344 secs] [Times: user=0.04 sys=0.00, real=0.01 secs]
Heap
PSYoungGen      total 9216K, used 2130K [0x00000007bf600000, 0x00000007c0000000, 0x00000007c0000000)
  eden space 8192K, 26% used [0x00000007bf600000,0x00000007bf814930,0x00000007bfe00000)
  from space 1024K, 0% used [0x00000007bfe00000,0x00000007bfe00000,0x00000007bff00000)
  to   space 1024K, 0% used [0x00000007bff00000,0x00000007bff00000,0x00000007c0000000)
ParOldGen       total 10240K, used 6420K [0x00000007bec00000, 0x00000007bf600000, 0x00000007bf600000)
  object space 10240K, 62% used [0x00000007bec00000,0x00000007bf2450d0,0x00000007bf600000)
Metaspace       used 2632K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 286K, capacity 386K, committed 512K, reserved 1048576K

這是因?yàn)樵诮o a4 分配內(nèi)存之前,Eden 區(qū)已經(jīng)被占用 6M拦盹。已經(jīng)無法再分配出 2M 來存儲(chǔ) a4 對象鹃祖。因此會(huì)執(zhí)行一次 Minor GC。并嘗試將存活的 a1普舆、a2恬口、a3 復(fù)制到 S1 區(qū)。但是 S1 區(qū)只有 1M 空間沼侣,所以沒有辦法存儲(chǔ) a1祖能、a2、a3 任意一個(gè)對象华临。在這種情況下 a1芯杀、a2、a3 將被轉(zhuǎn)移到老年代雅潭,最后將 a4 保存在 Eden 區(qū)揭厚。所以最終結(jié)果就是:Eden 區(qū)占用 2M(a4),老年代占用 6M(a1扶供、a2筛圆、a3)。

通過這個(gè)測試案例椿浓,我們也間接驗(yàn)證了 JVM 的內(nèi)存分配和分代回收策略太援。建議多嘗試使用各種命令參數(shù),給堆的新生代和老年代設(shè)置不同的大小來驗(yàn)證不同的結(jié)果扳碍。

再談引用

上文中已經(jīng)介紹過提岔,判斷對象是否存活我們是通過GC Roots的引用可達(dá)性來判斷的。但是JVM中的引用關(guān)系并不止一種笋敞,而是有四種碱蒙,根據(jù)引用強(qiáng)度的由強(qiáng)到弱,他們分別是:強(qiáng)引用(Strong Reference)夯巷、軟引用(Soft Reference)赛惩、弱引用(Weak Reference)、虛引用(Phantom Reference)趁餐。

四種引用做簡單對比如下:

img

平時(shí)項(xiàng)目中喷兼,尤其是Android項(xiàng)目,因?yàn)橛写罅康膱D像(Bitmap)對象后雷,使用軟引用的場景較多季惯。所以重點(diǎn)看下軟引用SoftReference的使用吠各,不當(dāng)?shù)氖褂密浺糜袝r(shí)也會(huì)導(dǎo)致系統(tǒng)異常。

軟引用常規(guī)使用

常規(guī)使用代碼如下:

img

執(zhí)行上述代碼星瘾,打印日志如下:

img

首先通過-Xmx將堆最大內(nèi)存設(shè)置為200M走孽。從日志中可以看出,當(dāng)?shù)谝淮蜧C時(shí)琳状,內(nèi)存中還有剩余可用內(nèi)存磕瓷,所以軟引用并不會(huì)被GC回收。但是當(dāng)我們再次創(chuàng)建一個(gè)120M的強(qiáng)引用時(shí)念逞,JVM可用內(nèi)存已經(jīng)不夠困食,所以會(huì)嘗試將軟引用給回收掉。

軟引用隱藏問題

需要注意的是翎承,被軟引用對象關(guān)聯(lián)的對象會(huì)自動(dòng)被垃圾回收器回收硕盹,但是軟引用對象本身也是一個(gè)對象,這些創(chuàng)建的軟引用并不會(huì)自動(dòng)被垃圾回收器回收掉叨咖。比如如下代碼:

img

上述代碼瘩例,雖然每一個(gè)SoftObject都被一個(gè)軟引用所引用,在內(nèi)存緊張時(shí)甸各,GC會(huì)將SoftObject所占用的1KB回收垛贤。但是每一個(gè)SoftReference又都被Set所引用(強(qiáng)引用)。執(zhí)行上述代碼結(jié)果如下:

img

限制堆內(nèi)存大小為4M趣倾,最終程序崩潰聘惦,但是異常的原因并不是普通的堆內(nèi)存溢出,而是"GC overhead"儒恋。之所以會(huì)拋出這個(gè)錯(cuò)誤善绎,是由于虛擬機(jī)一直在不斷回收軟引用,回收進(jìn)行的速度過快诫尽,占用的cpu過大(超過98%)禀酱,并且每次回收掉的內(nèi)存過小(小于2%),導(dǎo)致最終拋出了這個(gè)錯(cuò)誤牧嫉。

這里需要做優(yōu)化剂跟,合適的處理方式是注冊一個(gè)引用隊(duì)列,每次循環(huán)之后將引用隊(duì)列中出現(xiàn)的軟引用對象從cache中移除驹止。如下所示:

img

再次運(yùn)行修改后的代碼,結(jié)果如下:

img

可以看出優(yōu)化后观蜗,程序可以正常執(zhí)行完臊恋。并且在執(zhí)行過程中會(huì)動(dòng)態(tài)的將集合中的軟引用刪除。

更多詳細(xì) SoftReference 的介紹墓捻,可以參考 :

Java虛擬機(jī)究竟是如何處理SoftReference的 抖仅。

總結(jié):

本文學(xué)習(xí)總結(jié)了 JVM 中有關(guān)垃圾回收的相關(guān)知識(shí)點(diǎn)坊夫,其中有使用可達(dá)性分析來判斷對象是否可以被回收,以及 3 種垃圾回收算法撤卢。最后通過分析 GC Log 驗(yàn)證了 Java 虛擬機(jī)中內(nèi)存分配及分代策略的一些細(xì)節(jié)环凿。

虛擬機(jī)垃圾回收機(jī)制很多時(shí)候都是影響系統(tǒng)性能、并發(fā)能力的主要因素之一放吩。尤其是對于從事 Android 開發(fā)的工程師來說智听,有時(shí)候垃圾回收會(huì)很大程度上影響 UI 線程,并造成界面卡頓現(xiàn)象渡紫。因此理解垃圾回收機(jī)制并學(xué)會(huì)分析 GC Log 也是一項(xiàng)必不可少的技能到推。在后面的 DVM 文章中,將總結(jié) Android 虛擬機(jī)中對垃圾回收所做的優(yōu)化惕澎。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末莉测,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子唧喉,更是在濱河造成了極大的恐慌捣卤,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,544評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件八孝,死亡現(xiàn)場離奇詭異董朝,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)唆阿,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,430評論 3 392
  • 文/潘曉璐 我一進(jìn)店門益涧,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人驯鳖,你說我怎么就攤上這事闲询。” “怎么了浅辙?”我有些...
    開封第一講書人閱讀 162,764評論 0 353
  • 文/不壞的土叔 我叫張陵扭弧,是天一觀的道長。 經(jīng)常有香客問我记舆,道長鸽捻,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,193評論 1 292
  • 正文 為了忘掉前任泽腮,我火速辦了婚禮御蒲,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘诊赊。我一直安慰自己厚满,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,216評論 6 388
  • 文/花漫 我一把揭開白布碧磅。 她就那樣靜靜地躺著碘箍,像睡著了一般遵馆。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上丰榴,一...
    開封第一講書人閱讀 51,182評論 1 299
  • 那天货邓,我揣著相機(jī)與錄音,去河邊找鬼四濒。 笑死换况,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的峻黍。 我是一名探鬼主播复隆,決...
    沈念sama閱讀 40,063評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼姆涩!你這毒婦竟也來了挽拂?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,917評論 0 274
  • 序言:老撾萬榮一對情侶失蹤骨饿,失蹤者是張志新(化名)和其女友劉穎亏栈,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體宏赘,經(jīng)...
    沈念sama閱讀 45,329評論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡绒北,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,543評論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了察署。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片闷游。...
    茶點(diǎn)故事閱讀 39,722評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖贴汪,靈堂內(nèi)的尸體忽然破棺而出脐往,到底是詐尸還是另有隱情,我是刑警寧澤扳埂,帶...
    沈念sama閱讀 35,425評論 5 343
  • 正文 年R本政府宣布业簿,位于F島的核電站,受9級特大地震影響阳懂,放射性物質(zhì)發(fā)生泄漏梅尤。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,019評論 3 326
  • 文/蒙蒙 一岩调、第九天 我趴在偏房一處隱蔽的房頂上張望巷燥。 院中可真熱鬧,春花似錦号枕、人聲如沸缰揪。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,671評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽邀跃。三九已至,卻和暖如春蛙紫,著一層夾襖步出監(jiān)牢的瞬間拍屑,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,825評論 1 269
  • 我被黑心中介騙來泰國打工坑傅, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留僵驰,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,729評論 2 368
  • 正文 我出身青樓唁毒,卻偏偏與公主長得像蒜茴,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子浆西,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,614評論 2 353

推薦閱讀更多精彩內(nèi)容