JVM底層原理、四大垃圾回收算法詳解(長文警告)

注意:

  • 垃圾回收算法周陽老師講的有錯(cuò)誤,具體在p19,四大垃圾回收算法為復(fù)制算法、標(biāo)記-整理算法、標(biāo)記-清除算法、分代
    收集算法(不是引用計(jì)數(shù)算法)。這里感謝@9c0bd0ceebfa指出。
  • 關(guān)于FUll GC:Full GC為老年代的GC(周志明先生在《深入理解Java虛擬機(jī)》中也是這樣說的,第三版已改正)說法是不準(zhǔn)確的。Full GC 指的是針對(duì)新生代、老年代、永久代的全體內(nèi)存空間的垃圾回收。就是對(duì) JVM 進(jìn)行一次整體的垃圾回收,把各個(gè)內(nèi)存區(qū)域的垃圾都回收掉。老年代的GC特指Old GC,但因?yàn)镺ld GC一般伴隨著Young GC,也可以看做觸發(fā)了Full GC。
  • 注意:我們平時(shí)說的棧是指的Java棧,native method stack 里面裝的都是native方法。見下文


注意:

  • 方法區(qū)并不是存放方法的區(qū)域,其是存放類的描述信息(模板)的地方
  • Class loader只是負(fù)責(zé)class文件的加載,相當(dāng)于快遞員,這個(gè)“快遞員”并不是只有一家,Class loader有多種
  • 加載之前是“小class”,加載之后就變成了“大Class”,這是安裝java.lang.Class模板生成了一個(gè)實(shí)例?!按驝lass”就裝載在方法區(qū),模板實(shí)例化之后就得到n個(gè)相同的對(duì)象
  • JVM并不是通過檢查文件后綴是不是.class來判斷是否需要加載的,而是通過文件開頭的特定文件標(biāo)志
    文件開頭的特殊標(biāo)識(shí)

注意:

  • Class loader有多種,可以說三個(gè),也可以說是四個(gè)(第四個(gè)為自己定義的加載器,繼承 ClassLoader),系統(tǒng)自帶的三個(gè)分別為:
  1. 啟動(dòng)類加載器(Bootstrap) ,C++所寫
  2. 擴(kuò)展類加載器(Extension) ,Java所寫
  3. 應(yīng)用程序類加載器(AppClassLoader)。

我們自己new的時(shí)候創(chuàng)建的是應(yīng)用程序類加載器(AppClassLoader)。

import com.gmail.fxding2019.T;

public class  Test{
    //Test:查看類加載器
    public static void main(String[] args) {

        Object object = new Object();
        //查看是那個(gè)“ClassLoader”(快遞員把Object加載進(jìn)來的)
        System.out.println(object.getClass().getClassLoader());
        //查看Object的加載器的上一層
        // error Exception in thread "main" java.lang.NullPointerException(已經(jīng)是祖先了)
        //System.out.println(object.getClass().getClassLoader().getParent());

        System.out.println();

        Test t = new Test();
        System.out.println(t.getClass().getClassLoader().getParent().getParent());
        System.out.println(t.getClass().getClassLoader().getParent());
        System.out.println(t.getClass().getClassLoader());
    }
}

/*
*output:
* null
* 
* null
* sun.misc.Launcher$ExtClassLoader@4554617c
* sun.misc.Launcher$AppClassLoader@18b4aac2
* */

注意:

  • 如果是JDK自帶的類(Object、String、ArrayList等),其使用的加載器是Bootstrap加載器;如果自己寫的類,使用的是AppClassLoader加載器;Extension加載器是負(fù)責(zé)將把java更新的程序包的類加載進(jìn)行
  • 輸出中,sun.misc.Launcher是JVM相關(guān)調(diào)用的入口程序
  • Java加載器個(gè)數(shù)為3+1。前三個(gè)是系統(tǒng)自帶的,用戶可以定制類的加載方式,通過繼承Java. lang. ClassLoader

注意:

  • 雙親委派機(jī)制:“我爸是李剛,有事找我爹”。
    例如:需要用一個(gè)A.java這個(gè)類,首先去頂部Bootstrap根加載器去找,找得到你就用,找不到再下降一層,去Extension加載器去找,找得到就用,找不到再將一層,去AppClassLoader加載器去找,找得到就用,找不到就會(huì)報(bào)"CLASS NOT FOUND EXCEPTION"。
//測(cè)試加載器的加載順序
package java.lang;

public class String {

    public static void main(String[] args) {

        System.out.println("hello world!");

    }
}

/*
* output:
* 錯(cuò)誤: 在類 java.lang.String 中找不到 main 方法
* */

上面代碼是為了測(cè)試加載器的順序:首先加載的是Bootstrap加載器,由于JVM中有java.lang.String這個(gè)類,所以會(huì)首先加載這個(gè)類,而不是自己寫的類,而這個(gè)類中并無main方法,所以會(huì)報(bào)“在類 java.lang.String 中找不到 main 方法”。

這個(gè)問題就涉及到,如果有兩個(gè)相同的類,那么java到底會(huì)用哪一個(gè)?如果使用用戶自己定義的java.lang.String,那么別使用這個(gè)類的程序會(huì)去全部出錯(cuò),所以,為了保證用戶寫的源代碼不污染java出廠自帶的源代碼,而提供了一種“雙親委派”機(jī)制,保證“沙箱安全”。即先找到先使用。


Thread類的start方法如下:

public synchronized void start() {
        /**
         * This method is not invoked for the main method thread or "system"
         * group threads created/set up by the VM. Any new functionality added
         * to this method in the future may have to also be added to the VM.
         *
         * A zero status value corresponds to state "NEW".
         */
        if (threadStatus != 0)
            throw new IllegalThreadStateException();

        /* Notify the group that this thread is about to be started
         * so that it can be added to the group's list of threads
         * and the group's unstarted count can be decremented. */
        group.add(this);

        boolean started = false;
        try {
            start0();
            started = true;
        } finally {
            try {
                if (!started) {
                    group.threadStartFailed(this);
                }
            } catch (Throwable ignore) {
                /* do nothing. If start0 threw a Throwable then
                  it will be passed up the call stack */
            }
        }
    }

    private native void start0();

Thread類中竟然有一個(gè)只有聲明沒有實(shí)現(xiàn)的方法,并使用native關(guān)鍵字。用native表示,也此方法是系統(tǒng)級(jí)(底層操作系統(tǒng)或第三方C語言)的,而不是語言級(jí)的,java并不能對(duì)其進(jìn)行操作。native方法裝載在native method stack中。

  • 注意:native方法不歸java管,所以計(jì)數(shù)器是空的

上面圖中是亮色的地方有兩個(gè)特點(diǎn):

    1. 所有線程共享(灰色是線程私有)
    1. 亮色地方存在垃圾回收

注意:

  • 方法區(qū):絕對(duì)不是放方法的地方,他是存儲(chǔ)的每一個(gè)類的結(jié)構(gòu)信息(比如static)
  • 永久代和元空間的解釋:
    方法區(qū)是一種規(guī)范,類似于接口定義的規(guī)范:List list = new ArrayList();
    把這種比喻用到方法區(qū)則有:
    1. java 7中:方法區(qū) f = new 永久代();
    2. java 8中:方法去 f = new 元空間();

注意:

  • 棧管運(yùn)行,堆管存儲(chǔ)
  • 棧是線程私有,不存在垃圾回收
  • 棧幀的概念:java中的方法被扔進(jìn)虛擬機(jī)的??臻g之后就成為“棧幀”,比如main方法,是程序的入口,被壓棧之后就成為棧幀。

public class  Test{

    public static  void  m(){
        m();
    }

    public static void main(String[] args) {

        System.out.println("111");
        //Exception in thread "main" java.lang.StackOverflowError
        m();
        System.out.println("222");

    }
}

/*
*output:
* 111
* Exception in thread "main" java.lang.StackOverflowError
* */

注意:

  • StackOverflowError是一個(gè)“”錯(cuò)誤,而不是“異常”。

注意:

  • HotSpot:如果沒有明確指明,JDK的名字就叫HotSpot


  • 元數(shù)據(jù):描述數(shù)據(jù)的數(shù)據(jù)(即模板,也就是“大Class”)
    上面的關(guān)系圖的一個(gè)實(shí)例為下圖:


注意:

  • Java 7之前和圖上一模一樣,Java 8永久區(qū)換成了元空間
  • 堆邏輯上由”新生+養(yǎng)老+元空間“三個(gè)部分組成,物理上由”新生+養(yǎng)老“兩個(gè)部分組成
  • 當(dāng)執(zhí)行new Person();時(shí),其實(shí)是new在新生區(qū)的伊甸園區(qū),然后往下走,走到養(yǎng)老區(qū),但是并未到元空間。

注意:

  • GC發(fā)生在伊甸園區(qū),當(dāng)對(duì)象快占滿新生代時(shí),就會(huì)發(fā)生YGC(Young GC,輕量級(jí)GC)操作,伊甸園區(qū)基本全部清空
  • 幸存者0區(qū)(S0),別名“from區(qū)”。伊甸園區(qū)沒有被YGC清空的對(duì)象將移至幸存者0區(qū),幸存者1區(qū)別名“to 區(qū)”
  • 每次進(jìn)行YGC操作,幸存的對(duì)象就會(huì)從伊甸園區(qū)移到幸存者0區(qū),如果幸存者0區(qū)滿了,就會(huì)繼續(xù)往下移,如果經(jīng)歷數(shù)次YGC操作對(duì)象還沒有消亡,最終會(huì)來到養(yǎng)老區(qū)
  • 如果到最后,養(yǎng)老區(qū)也滿了,那么就對(duì)養(yǎng)老區(qū)進(jìn)行FGC(Full GC,重GC),對(duì)養(yǎng)老區(qū)進(jìn)行清洗
  • 如果進(jìn)行了多次FGC之后,還是無法騰出養(yǎng)老區(qū)的空間,就會(huì)報(bào)OOM(out of Memory)異常
  • from區(qū)和to區(qū)位置和名分不是固定的,每次GC過后都會(huì)交換,GC交換后,誰空誰是to區(qū)

注意:

  • 整個(gè)堆分為新生區(qū)和養(yǎng)老區(qū),新生區(qū)占整個(gè)堆的1/3,養(yǎng)老區(qū)占2/3。新生區(qū)又分為3份:伊甸園區(qū):幸存者0區(qū)(from區(qū)):幸存者1區(qū)(to區(qū)) = 8:1:1
  • 每次從伊甸園區(qū)經(jīng)過GC幸存的對(duì)象,年齡(代數(shù))會(huì)+1

注意:

  • 臨時(shí)對(duì)象就是說明,其在伊甸園區(qū)生,也在伊甸園區(qū)死。
  • 堆邏輯上由”新生+養(yǎng)老+元空間“三個(gè)部分組成,物理上由”新生+養(yǎng)老“兩個(gè)部分組成,元空間也叫方法區(qū)
  • 永久代(方法區(qū))幾乎沒有垃圾回收,里面存放的都是加載的rt.jar等,讓你隨時(shí)可用

注意

  • 上面的圖展示的是物理上的堆,分為兩塊,新生區(qū)和養(yǎng)老區(qū)。
  • 堆的參數(shù)主要有兩個(gè):-Xms,Xmx
    1. -Xms堆的初始化的大小
    2. Xmx堆的最大化
  • Young Gen(新生代)有一個(gè)參數(shù)-Xmn,這個(gè)參數(shù)可以調(diào)新生區(qū)和養(yǎng)老區(qū)的比例。但是,這個(gè)參數(shù)一般不調(diào)。
  • 永久代也有兩個(gè)參數(shù):-XX:PermSize,-XX:MaxPermSize,可以分別調(diào)永久帶的初始值和最大值。Java 8 后沒有這兩個(gè)參數(shù)啦,因?yàn)镴ava 8后元空間不在虛擬機(jī)內(nèi)啦,而是在本機(jī)物理內(nèi)存中
//查看自己機(jī)器上的默認(rèn)堆內(nèi)存和最大堆內(nèi)存
public class  Test{

    public static void main(String[] args) {
        System.out.println(Runtime.getRuntime().availableProcessors());
        //返回 Java虛擬機(jī)試圖使用的最大內(nèi)存量。物理內(nèi)存的1/4(-Xmx)
        long maxMemory = Runtime.getRuntime().maxMemory() ;
        //返回 Java虛擬機(jī)中的內(nèi)存總量(初始值)。物理內(nèi)存的1/64(-Xms)
        long totalMemory = Runtime.getRuntime().totalMemory() ;
        System.out.println("MAX_MEMORY =" + maxMemory +"(字節(jié))、" + (maxMemory / (double)1024 / 1024) + "MB");
        System.out.println("DEFALUT_MEMORY = " + totalMemory + " (字節(jié))、" + (totalMemory / (double)1024 / 1024) + "MB");

    }
}

/*
*   8
    MAX_MEMORY =1868038144(字節(jié))、1781.5MB
    TOTAL_MEMORY = 126877696 (字節(jié))、121.0MB
* */
  • 注意:JVM參數(shù)調(diào)優(yōu),平時(shí)可以隨便挑初始大小和最大大小,但是實(shí)際工作中,初始大小和最大大小應(yīng)該是一致的,原因是避免內(nèi)存忽高忽低產(chǎn)生停頓
  • IDEA 的JVM內(nèi)存配置
    1. 點(diǎn)擊Run列表下的Edit Configuration


    2. 在VM Options中輸入以下參數(shù):-Xms1024m -Xmx1024m -XX:+PrintGCDetails。
    3. 運(yùn)行程序查看結(jié)果


  • 把堆內(nèi)存調(diào)成10M后,再一直new對(duì)象,導(dǎo)致Full GC也無法處理,直至撐爆堆內(nèi)存,查看堆溢出錯(cuò)誤(OOM),程序及結(jié)果如下:




GC收集日志信息詳解

  • 第一次進(jìn)行YGC相關(guān)參數(shù):
    [PSYoungGen: 2008K->482K(2560K)] 2008K->782K(9728K), 0.0011440 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]


  • 最后一次進(jìn)行FGC相關(guān)參數(shù):
    [Full GC (Allocation Failure) [PSYoungGen: 0K->0K(2048K)] [ParOldGen: 4025K->4005K(7168K)] 4025K->4005K(9216K), [Metaspace: 3289K->3289K(1056768K)], 0.0082055 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]



面試題:GC是什么(分代收集算法)

  • 次數(shù)上頻繁收集Young區(qū)
  • 次數(shù)上較少收集Old區(qū)
  • 基本不動(dòng)元空間

面試題:GC的四大算法(后有詳解)

  • 1.復(fù)制算法(Copying)
    1. 標(biāo)記清除(Mark-Sweep)
    1. 標(biāo)記壓縮(Mark-Compact)
    1. 分代收集算法

面試題:下面程序中,有幾個(gè)線程在運(yùn)行


Answer:有兩個(gè)線程,一個(gè)是main線程,一個(gè)是后臺(tái)的gc線程。

GC算法概述

知識(shí)點(diǎn):

  • JVM在進(jìn)行GC時(shí),并非每次都對(duì)上面三個(gè)內(nèi)存區(qū)域一起回收的,大部分時(shí)候回收的都是指新生代。因此GC按照回收的區(qū)域又分了兩種類型,一種是普通GC(minor GC or Young GC),一種是全局GC(major GC or Full GC)
  • Minor GC和Full GC的區(qū)別
      普通GC(minor GC):只針對(duì)新生代區(qū)域的GC,指發(fā)生在新生代的垃圾收集動(dòng)作,因?yàn)榇蠖鄶?shù)Java對(duì)象存活率都不高,所以Minor GC非常頻繁,一般回收速度也比較快。
      全局GC(major GC or Full GC):指發(fā)生在老年代的垃圾收集動(dòng)作,出現(xiàn)了Major GC,經(jīng)常會(huì)伴隨至少一次的Minor GC(但并不是絕對(duì)的)。Major GC的速度一般要比Minor GC慢上10倍以上 (因?yàn)轲B(yǎng)老區(qū)比較大,占堆的2/3)

GC四大算法詳解:

首先看一下判斷Java中對(duì)象存活的算法:

  • 1.引用計(jì)數(shù)算法:引用計(jì)數(shù)器算法是給每個(gè)對(duì)象設(shè)置一個(gè)計(jì)數(shù)器,當(dāng)有地方引用這個(gè)對(duì)象的時(shí)候,計(jì)數(shù)器+1,當(dāng)引用失效的時(shí)候,計(jì)數(shù)器-1,當(dāng)計(jì)數(shù)器為0的時(shí)候,JVM就認(rèn)為對(duì)象不再被使用,是“垃圾”了。
    引用計(jì)數(shù)器實(shí)現(xiàn)簡單,效率高;但是不能解決循環(huán)引用問問題(A對(duì)象引用B對(duì)象,B對(duì)象又引用A對(duì)象,但是A,B對(duì)象已不被任何其他對(duì)象引用),同時(shí)每次計(jì)數(shù)器的增加和減少都帶來了很多額外的開銷,所以在JDK1.1之后,這個(gè)算法已經(jīng)不再使用了。
  • 2.根搜索方法:根搜索方法是通過一些“GCRoots”對(duì)象作為起點(diǎn),從這些節(jié)點(diǎn)開始往下搜索,搜索通過的路徑成為引用鏈(ReferenceChain),當(dāng)一個(gè)對(duì)象沒有被GCRoots的引用鏈連接的時(shí)候,說明這個(gè)對(duì)象是不可用的。
    GCRoots對(duì)象包括:
    1. 虛擬機(jī)棧(棧幀中的本地變量表)中的引用的對(duì)象。
    2. 方法區(qū)域中的類靜態(tài)屬性引用的對(duì)象。
    3. 方法區(qū)域中常量引用的對(duì)象。
    4. 方法棧中JNI(Native方法)的引用的對(duì)象。

1. 復(fù)制算法(Copying)

年輕代中使用的是Minor GC(YGC),這種GC算法采用的是復(fù)制算法(Copying)。

Minor GC會(huì)把Eden中的所有活的對(duì)象都移到Survivor區(qū)域中,如果Survivor區(qū)中放不下,那么剩下的活的對(duì)象就被移到Old generation中,也即一旦收集后,Eden是就變成空的了。

當(dāng)對(duì)象在 Eden ( 包括一個(gè) Survivor 區(qū)域,這里假設(shè)是 from 區(qū)域 ) 出生后,在經(jīng)過一次 Minor GC 后,如果對(duì)象還存活,并且能夠被另外一塊 Survivor 區(qū)域所容納( 上面已經(jīng)假設(shè)為 from 區(qū)域,這里應(yīng)為 to 區(qū)域,即 to 區(qū)域有足夠的內(nèi)存空間來存儲(chǔ) Eden 和 from 區(qū)域中存活的對(duì)象 ),則使用復(fù)制算法將這些仍然還存活的對(duì)象復(fù)制到另外一塊 Survivor 區(qū)域 ( 即 to 區(qū)域 ) 中,然后清理所使用過的 Eden 以及 Survivor 區(qū)域 ( 即 from 區(qū)域 ),并且將這些對(duì)象的年齡設(shè)置為1,以后對(duì)象在 Survivor 區(qū)每熬過一次 Minor GC,就將對(duì)象的年齡 + 1,當(dāng)對(duì)象的年齡達(dá)到某個(gè)值時(shí) ( 默認(rèn)是 15 歲,通過-XX:MaxTenuringThreshold 來設(shè)定參數(shù)),這些對(duì)象就會(huì)成為老年代。

-XX:MaxTenuringThreshold — 設(shè)置對(duì)象在新生代中存活的次數(shù)

年輕代中的GC,主要是復(fù)制算法(Copying)。 HotSpot JVM把年輕代分為了三部分:1個(gè)Eden區(qū)和2個(gè)Survivor區(qū)(分別叫from和to)。默認(rèn)比例為8:1:1,一般情況下,新創(chuàng)建的對(duì)象都會(huì)被分配到Eden區(qū)(一些大對(duì)象特殊處理),這些對(duì)象經(jīng)過第一次Minor GC后,如果仍然存活,將會(huì)被移到Survivor區(qū)。對(duì)象在Survivor區(qū)中每熬過一次Minor GC,年齡就會(huì)增加1歲,當(dāng)它的年齡增加到一定程度時(shí),就會(huì)被移動(dòng)到年老代中。因?yàn)槟贻p代中的對(duì)象基本都是朝生夕死的(90%以上),所以在年輕代的垃圾回收算法使用的是復(fù)制算法,復(fù)制算法的基本思想就是將內(nèi)存分為兩塊,每次只用其中一塊(from),當(dāng)這一塊內(nèi)存用完,就將還活著的對(duì)象復(fù)制到另外一塊上面。復(fù)制算法的優(yōu)點(diǎn)是不會(huì)產(chǎn)生內(nèi)存碎片,缺點(diǎn)是耗費(fèi)空間

在GC開始的時(shí)候,對(duì)象只會(huì)存在于Eden區(qū)和名為“From”的Survivor區(qū),Survivor區(qū)“To”是空的。緊接著進(jìn)行GC,Eden區(qū)中所有存活的對(duì)象都會(huì)被復(fù)制到“To”,而在“From”區(qū)中,仍存活的對(duì)象會(huì)根據(jù)他們的年齡值來決定去向。年齡達(dá)到一定值(年齡閾值,可以通過-XX:MaxTenuringThreshold來設(shè)置)的對(duì)象會(huì)被移動(dòng)到年老代中,沒有達(dá)到閾值的對(duì)象會(huì)被復(fù)制到“To”區(qū)域。經(jīng)過這次GC后,Eden區(qū)和From區(qū)已經(jīng)被清空。這個(gè)時(shí)候,“From”和“To”會(huì)交換他們的角色,也就是新的“To”就是上次GC前的“From”,新的“From”就是上次GC前的“To”。不管怎樣,都會(huì)保證名為To的Survivor區(qū)域是空的。Minor GC會(huì)一直重復(fù)這樣的過程,直到“To”區(qū)被填滿,“To”區(qū)被填滿之后,會(huì)將所有對(duì)象移動(dòng)到年老代中。

因?yàn)镋den區(qū)對(duì)象一般存活率較低,一般的,使用兩塊10%的內(nèi)存作為空閑和活動(dòng)區(qū)間,而另外80%的內(nèi)存,則是用來給新建對(duì)象分配內(nèi)存的。一旦發(fā)生GC,將10%的from活動(dòng)區(qū)間與另外80%中存活的eden對(duì)象轉(zhuǎn)移到10%的to空閑區(qū)間,接下來,將之前90%的內(nèi)存全部釋放,以此類推。

蜜汁動(dòng)畫:看不懂請(qǐng)忽略

上面動(dòng)畫中,Area空閑代表to,Area激活代表from,綠色代表不被回收的,紅色代表被回收的。

復(fù)制算法它的缺點(diǎn)也是相當(dāng)明顯的:

    1. 它浪費(fèi)了一半的內(nèi)存,這太要命了。
    1. 如果對(duì)象的存活率很高,我們可以極端一點(diǎn),假設(shè)是100%存活,那么我們需要將所有對(duì)象都復(fù)制一遍,并將所有引用地址重置一遍。復(fù)制這一工作所花費(fèi)的時(shí)間,在對(duì)象存活率達(dá)到一定程度時(shí),將會(huì)變的不可忽視。 所以從以上描述不難看出,復(fù)制算法要想使用,最起碼對(duì)象的存活率要非常低才行,而且最重要的是,我們必須要克服50%內(nèi)存的浪費(fèi)。

2 .標(biāo)記清除(Mark-Sweep)

復(fù)制算法的缺點(diǎn)就是費(fèi)空間,其是用在年輕代的,老年代一般是由標(biāo)記清除或者是標(biāo)記清除與標(biāo)記整理的混合實(shí)現(xiàn)。


用通俗的話解釋一下標(biāo)記清除算法,就是當(dāng)程序運(yùn)行期間,若可以使用的內(nèi)存被耗盡的時(shí)候,GC線程就會(huì)被觸發(fā)并將程序暫停,隨后將要回收的對(duì)象標(biāo)記一遍,最終統(tǒng)一回收這些對(duì)象,完成標(biāo)記清理工作接下來便讓應(yīng)用程序恢復(fù)運(yùn)行。

主要進(jìn)行兩項(xiàng)工作,第一項(xiàng)則是標(biāo)記,第二項(xiàng)則是清除。

  • 標(biāo)記:從引用根節(jié)點(diǎn)開始標(biāo)記遍歷所有的GC Roots, 先標(biāo)記出要回收的對(duì)象。
  • 清除:遍歷整個(gè)堆,把標(biāo)記的對(duì)象清除。

缺點(diǎn):此算法需要暫停整個(gè)應(yīng)用,會(huì)產(chǎn)生內(nèi)存碎片

標(biāo)記清除算法動(dòng)態(tài)版

標(biāo)記清除算法小結(jié):

  • 1、首先,它的缺點(diǎn)就是效率比較低(遞歸與全堆對(duì)象遍歷),而且在進(jìn)行GC的時(shí)候,需要停止應(yīng)用程序,這會(huì)導(dǎo)致用戶體驗(yàn)非常差勁
  • 2、其次,主要的缺點(diǎn)則是這種方式清理出來的空閑內(nèi)存是不連續(xù)的,這點(diǎn)不難理解,我們的死亡對(duì)象都是隨即的出現(xiàn)在內(nèi)存的各個(gè)角落的,現(xiàn)在把它們清除之后,內(nèi)存的布局自然會(huì)亂七八糟。而為了應(yīng)付這一點(diǎn),JVM就不得不維持一個(gè)內(nèi)存的空閑列表,這又是一種開銷。而且在分配數(shù)組對(duì)象的時(shí)候,尋找連續(xù)的內(nèi)存空間會(huì)不太好找。

3. 標(biāo)記壓縮(Mark-Compact)

標(biāo)記壓縮(Mark-Compact)又叫標(biāo)記清除壓縮(Mark-Sweep-Compact),或者標(biāo)記清除整理算法。老年代一般是由標(biāo)記清除或者是標(biāo)記清除與標(biāo)記整理的混合實(shí)現(xiàn)

標(biāo)記清除整理動(dòng)態(tài)版

4. 分代收集算法

當(dāng)前商業(yè)虛擬機(jī)都是采用分代收集算法,它根據(jù)對(duì)象存活周期的不同將內(nèi)存劃分為幾塊,一般是把Java堆分為新生代和老年代,然后根據(jù)各個(gè)年代的特點(diǎn)采用最適當(dāng)?shù)氖占惴?,在新生代中,每次垃圾收集都發(fā)現(xiàn)有大批對(duì)象死去,只有少量存活,就選用復(fù)制算法,而老年代因?yàn)閷?duì)象存活率高,沒有額外空間對(duì)它進(jìn)行分配擔(dān)保,就必須使用“標(biāo)記清理”或者“標(biāo)記整理”算法來進(jìn)行回收。

分代收集

圖的左半部分是未回收前的內(nèi)存區(qū)域,右半部分是回收后的內(nèi)存區(qū)域。

面試題:四種算法那個(gè)好
Answer:沒有那個(gè)算法是能一次性解決所有問題的,因?yàn)镴VM垃圾回收使用的是分代收集算法,沒有最好的算法,只有根據(jù)每一代他的垃圾回收的特性用對(duì)應(yīng)的算法。新生代使用復(fù)制算法,老年代使用標(biāo)記清除和標(biāo)記整理算法。沒有最好的垃圾回收機(jī)制,只有最合適的。

面試題:請(qǐng)說出各個(gè)垃圾回收算法的優(yōu)缺點(diǎn)

  • 內(nèi)存效率:復(fù)制算法>標(biāo)記清除算法>標(biāo)記整理算法(此處的效率只是簡單的對(duì)比時(shí)間復(fù)雜度,實(shí)際情況不一定如此)。
  • 內(nèi)存整齊度:復(fù)制算法=標(biāo)記整理算法>標(biāo)記清除算法。
  • 內(nèi)存利用率:標(biāo)記整理算法=標(biāo)記清除算法>復(fù)制算法。

可以看出,效率上來說,復(fù)制算法是當(dāng)之無愧的老大,但是卻浪費(fèi)了太多內(nèi)存,而為了盡量兼顧上面所提到的三個(gè)指標(biāo),標(biāo)記/整理算法相對(duì)來說更平滑一些,但效率上依然不盡如人意,它比復(fù)制算法多了一個(gè)標(biāo)記的階段,又比標(biāo)記/清除多了一個(gè)整理內(nèi)存的過程

難道就沒有一種最優(yōu)算法嗎?Java 9 之后出現(xiàn)了G1垃圾回收器(使用分代收集),能夠解決以上問題,有興趣參考這篇文章


總結(jié):

  • 年輕代(Young Gen)

年輕代特點(diǎn)是區(qū)域相對(duì)老年代較小,對(duì)像存活率低。

這種情況復(fù)制算法的回收整理,速度是最快的。復(fù)制算法的效率只和當(dāng)前存活對(duì)像大小有關(guān),因而很適用于年輕代的回收。而復(fù)制算法內(nèi)存利用率不高的問題,通過hotspot中的兩個(gè)survivor的設(shè)計(jì)得到緩解。

  • 老年代(Tenure Gen)

老年代的特點(diǎn)是區(qū)域較大,對(duì)像存活率高。

這種情況,存在大量存活率高的對(duì)像,復(fù)制算法明顯變得不合適。一般是由標(biāo)記清除或者是標(biāo)記清除與標(biāo)記整理的混合實(shí)現(xiàn)。

Mark階段的開銷與存活對(duì)像的數(shù)量成正比,這點(diǎn)上說來,對(duì)于老年代,標(biāo)記清除或者標(biāo)記整理有一些不符,但可以通過多核/線程利用,對(duì)并發(fā)、并行的形式提標(biāo)記效率。

Sweep階段的開銷與所管理區(qū)域的大小形正相關(guān),但Sweep“就地處決”的特點(diǎn),回收的過程沒有對(duì)像的移動(dòng)。使其相對(duì)其它有對(duì)像移動(dòng)步驟的回收算法,仍然是效率最好的。但是需要解決內(nèi)存碎片問題。

Compact階段的開銷與存活對(duì)像的數(shù)據(jù)成開比,如上一條所描述,對(duì)于大量對(duì)像的移動(dòng)是很大開銷的,做為老年代的第一選擇并不合適。

基于上面的考慮,老年代一般是由標(biāo)記清除或者是標(biāo)記清除與標(biāo)記整理的混合實(shí)現(xiàn)。以hotspot中的CMS回收器為例,CMS是基于Mark-Sweep實(shí)現(xiàn)的,對(duì)于對(duì)像的回收效率很高,而對(duì)于碎片問題,CMS采用基于Mark-Compact算法的Serial Old回收器做為補(bǔ)償措施:當(dāng)內(nèi)存回收不佳(碎片導(dǎo)致的Concurrent Mode Failure時(shí)),將采用Serial Old執(zhí)行Full GC以達(dá)到對(duì)老年代內(nèi)存的整理。


最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
禁止轉(zhuǎn)載,如需轉(zhuǎn)載請(qǐng)通過簡信或評(píng)論聯(lián)系作者。

相關(guān)閱讀更多精彩內(nèi)容

  • 0、前言 讀完本文,你將了解到: 一、為什么說Jabalpur語言是跨平臺(tái)的 二、Java虛擬機(jī)啟動(dòng)、加載類過程分...
    vivi_wong閱讀 1,383評(píng)論 0 10
  • ClassLoader翻譯過來就是類加載器,普通的java開發(fā)者其實(shí)用到的不多,但對(duì)于某些框架開發(fā)者來說卻非常常見...
    時(shí)待吾閱讀 1,167評(píng)論 0 1
  • 我們都知道,Java的類包含屬性和方法,類先要進(jìn)行實(shí)例化,然后才是方法的調(diào)用,在這之前,還需要了解一個(gè)類是如何被J...
    無為無悔閱讀 927評(píng)論 0 2
  • JVM類加載器ClassLoader JAVA類裝載方式 1.隱式裝載, 程序在運(yùn)行過程中當(dāng)碰到通過new 等方式...
    步二小哥閱讀 468評(píng)論 0 1
  • 類加載器是 Java 語言的一個(gè)創(chuàng)新,也是 Java 語言流行的重要原因之一。它使得 Java 類可以被動(dòng)態(tài)加載到...
    CHSmile閱讀 1,672評(píng)論 0 12

友情鏈接更多精彩內(nèi)容