JVM底層原理、四大垃圾回收算法詳解

jvm是一個比較高深的技術,本人也是緊跟周陽老師的視頻走的,java

  此文章轉 https://www.jianshu.com/p/9e6841a895b4web

 
 
 
 
  • 注意:咱們平時說的棧是指的Java棧,native method stack 裏面裝的都是native方法。見下文面試


     
     

注意:算法

  • 方法區並非存放方法的區域,其是存放類的描述信息(模板)的地方
  • Class loader只是負責class文件的加載,至關於快遞員,這個「快遞員」並非只有一家,Class loader有多種
  • 加載以前是「小class」,加載以後就變成了「大Class」,這是安裝java.lang.Class模板生成了一個實例。「大Class」就裝載在方法區,模板實例化以後就獲得n個相同的對象
  • JVM並非經過檢查文件後綴是否是.class來判斷是否須要加載的,而是經過文件開頭的特定文件標誌
     
    文件開頭的特殊標識
 
 

注意:數組

  • Class loader有多種,能夠說三個,也能夠說是四個(第四個爲本身定義的加載器,繼承 ClassLoader),系統自帶的三個分別爲:
  1. 啓動類加載器(Bootstrap) ,C++所寫
  2. 擴展類加載器(Extension) ,Java所寫
  3. 應用程序類加載器(AppClassLoader)。

咱們本身new的時候建立的是應用程序類加載器(AppClassLoader)。安全

import com.gmail.fxding2019.T;

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

        Object object = new Object();
        //查看是那個「ClassLoader」(快遞員把Object加載進來的)
        System.out.println(object.getClass().getClassLoader());
        //查看Object的加載器的上一層
        // error Exception in thread "main" java.lang.NullPointerException(已是祖先了)
        //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加載器是負責將把java更新的程序包的類加載進行
  • 輸出中,sun.misc.Launcher是JVM相關調用的入口程序
  • Java加載器個數爲3+1。前三個是系統自帶的,用戶能夠定製類的加載方式,經過繼承Java. lang. ClassLoader
 
 

注意:jvm

  • 雙親委派機制:「我爸是李剛,有事找我爹」。
    例如:須要用一個A.java這個類,首先去頂部Bootstrap根加載器去找,找獲得你就用,找不到再降低一層,去Extension加載器去找,找獲得就用,找不到再將一層,去AppClassLoader加載器去找,找獲得就用,找不到就會報"CLASS NOT FOUND EXCEPTION"。
//測試加載器的加載順序
package java.lang;

public class String {

    public static void main(String[] args) {

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

    }
}

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

  

上面代碼是爲了測試加載器的順序:首先加載的是Bootstrap加載器,因爲JVM中有java.lang.String這個類,因此會首先加載這個類,而不是本身寫的類,而這個類中並沒有main方法,因此會報「在類 java.lang.String 中找不到 main 方法」。ide

這個問題就涉及到,若是有兩個相同的類,那麼java到底會用哪個?若是使用用戶本身定義的java.lang.String,那麼別使用這個類的程序會去所有出錯,因此,爲了保證用戶寫的源代碼不污染java出廠自帶的源代碼,而提供了一種「雙親委派」機制,保證「沙箱安全」。即先找到先使用。佈局

 
 
 
 

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類中居然有一個只有聲明沒有實現的方法,並使用native關鍵字。用native表示,也此方法是系統級(底層操做系統或第三方C語言)的,而不是語言級的,java並不能對其進行操做。native方法裝載在native method stack中。

 
 
  • 注意:native方法不歸java管,因此計數器是空的
 
 

上面圖中是亮色的地方有兩個特色:

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

注意:

  • 方法區:絕對不是放方法的地方,他是存儲的每個類的結構信息(好比static)
  • 永久代和元空間的解釋:
    方法區是一種規範,相似於接口定義的規範:List list = new ArrayList();
    把這種比喻用到方法區則有:
    1. java 7中:方法區 f = new 永久代();
    2. java 8中:方法去 f = new 元空間();
 
 

注意:

  • 棧管運行,堆管存儲
  • 棧是線程私有,不存在垃圾回收
  • 棧幀的概念:java中的方法被扔進虛擬機的棧空間以後就成爲「棧幀」,好比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是一個「」錯誤,而不是「異常」
     
     
 
 

注意:

  • HotSpot:若是沒有明確指明,JDK的名字就叫HotSpot


     
     
  • 元數據:描述數據的數據(即模板,也就是「大Class」)
    上面的關係圖的一個實例爲下圖:


     
     
 
 
 
 

注意:

  • Java 7以前和圖上如出一轍,Java 8永久區換成了元空間
  • 堆邏輯上由」新生+養老+元空間「三個部分組成,物理上由」新生+養老「兩個部分組成
  • 當執行new Person();時,實際上是new在新生區的伊甸園區,而後往下走,走到養老區,可是並未到元空間。
 
 

注意:

  • GC發生在伊甸園區,當對象快佔滿新生代時,就會發生YGC(Young GC,輕量級GC)操做,伊甸園區基本所有清空
  • 倖存者0區(S0),別名「from區」。伊甸園區沒有被YGC清空的對象將移至倖存者0區,倖存者1區別名「to 區」
  • 每次進行YGC操做,倖存的對象就會從伊甸園區移到倖存者0區,若是倖存者0區滿了,就會繼續往下移,若是經歷數次YGC操做對象尚未消亡,最終會來到養老區
  • 若是到最後,養老區也滿了,那麼就對養老區進行FGC(Full GC,重GC),對養老區進行清洗
  • 若是進行了屢次FGC以後,仍是沒法騰出養老區的空間,就會報OOM(out of Memory)異常
  • from區和to區位置和名分不是固定的,每次GC事後都會交換,GC交換後,誰空誰是to區
 
 

注意:

  • 整個堆分爲新生區和養老區,新生區佔整個堆的1/3,養老區佔2/3。新生區又分爲3份:伊甸園區:倖存者0區(from區):倖存者1區(to區) = 8:1:1
  • 每次從伊甸園區通過GC倖存的對象,年齡(代數)會+1
 
 
 
 

注意:

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

注意

  • 上面的圖展現的是物理上的堆,分爲兩塊,新生區和養老區。
  • 堆的參數主要有兩個:-XmsXmx
    1. -Xms堆的初始化的大小
    2. Xmx堆的最大化
  • Young Gen(新生代)有一個參數-Xmn,這個參數能夠調新生區和養老區的比例。可是,這個參數通常不調。
  • 永久代也有兩個參數:-XX:PermSize-XX:MaxPermSize,能夠分別調永久帶的初始值和最大值。Java 8 後沒有這兩個參數啦,由於Java 8後元空間不在虛擬機內啦,而是在本機物理內存中
 
 
 
 
//查看本身機器上的默認堆內存和最大堆內存
public class  Test{

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

    }
}

/*
*   8
    MAX_MEMORY =1868038144(字節)、1781.5MB
    TOTAL_MEMORY = 126877696 (字節)、121.0MB
* */

  

  • 注意:JVM參數調優,平時能夠隨便挑初始大小和最大大小,可是實際工做中,初始大小和最大大小應該是一致的,緣由是避免內存忽高忽低產生停頓
  • IDEA 的JVM內存配置
    1. 點擊Run列表下的Edit Configuration


       
       
    2. 在VM Options中輸入如下參數:-Xms1024m -Xmx1024m -XX:+PrintGCDetails
       
       
    3. 運行程序查看結果


       
       
  • 把堆內存調成10M後,再一直new對象,致使Full GC也沒法處理,直至撐爆堆內存,查看堆溢出錯誤(OOM),程序及結果以下:


     
     

     
     

     
     

GC收集日誌信息詳解

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


     
     
  • 最後一次進行FGC相關參數:
    [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是什麼(分代收集算法)

  • 次數上頻繁收集Young區
  • 次數上較少收集Old區
  • 基本不動元空間

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

    1. 引用計數法
    1. 複製算法(Copying)
    1. 標記清除(Mark-Sweep)
    1. 標記壓縮(Mark-Compact)

面試題:下面程序中,有幾個線程在運行

 
 

Answer:有兩個線程,一個是main線程,一個是後臺的gc線程。

 

 
GC算法概述

知識點:

  • JVM在進行GC時,並不是每次都對上面三個內存區域一塊兒回收的,大部分時候回收的都是指新生代。所以GC按照回收的區域又分了兩種類型,一種是普通GC(minor GC or Young GC),一種是全局GC(major GC or Full GC)
  • Minor GC和Full GC的區別
      普通GC(minor GC):只針對新生代區域的GC,指發生在新生代的垃圾收集動做,由於大多數Java對象存活率都不高,因此Minor GC很是頻繁,通常回收速度也比較快。
      全局GC(major GC or Full GC):指發生在老年代的垃圾收集動做,出現了Major GC,常常會伴隨至少一次的Minor GC(但並非絕對的)。Major GC的速度通常要比Minor GC慢上10倍以上 (由於養老區比較大,佔堆的2/3)

GC四大算法詳解:

1. 引用計數法(如今通常不採用)

 
 

代碼示例以下:雖然objectA和objectB都置空,可是他們以前曾發生過相互引用,因此調用system.gc(手動版喚醒GC,後臺也開着自動檔)並不能進行垃圾回收。而且,system.gc執行完以後也不是馬上執行垃圾回收。

 
 

注意:在實際工做中,禁用system.gc() !!!

2. 複製算法(Copying)

年輕代中使用的是Minor GC(YGC),這種GC算法採用的是複製算法(Copying)。

Minor GC會把Eden中的全部活的對象都移到Survivor區域中,若是Survivor區中放不下,那麼剩下的活的對象就被移到Old generation中,也即一旦收集後,Eden是就變成空的了。

當對象在 Eden ( 包括一個 Survivor 區域,這裏假設是 from 區域 ) 出生後,在通過一次 Minor GC 後,若是對象還存活,而且可以被另一塊 Survivor 區域所容納( 上面已經假設爲 from 區域,這裏應爲 to 區域,即 to 區域有足夠的內存空間來存儲 Eden 和 from 區域中存活的對象 ),則使用複製算法將這些仍然還存活的對象複製到另一塊 Survivor 區域 ( 即 to 區域 ) 中,而後清理所使用過的 Eden 以及 Survivor 區域 ( 即 from 區域 ),而且將這些對象的年齡設置爲1,之後對象在 Survivor 區每熬過一次 Minor GC,就將對象的年齡 + 1,當對象的年齡達到某個值時 ( 默認是 15 歲,經過-XX:MaxTenuringThreshold 來設定參數),這些對象就會成爲老年代。

-XX:MaxTenuringThreshold — 設置對象在新生代中存活的次數

 
 

年輕代中的GC,主要是複製算法(Copying)。 HotSpot JVM把年輕代分爲了三部分:1個Eden區和2個Survivor區(分別叫from和to)。默認比例爲8:1:1,通常狀況下,新建立的對象都會被分配到Eden區(一些大對象特殊處理),這些對象通過第一次Minor GC後,若是仍然存活,將會被移到Survivor區。對象在Survivor區中每熬過一次Minor GC,年齡就會增長1歲,當它的年齡增長到必定程度時,就會被移動到年老代中。由於年輕代中的對象基本都是朝生夕死的(90%以上),因此在年輕代的垃圾回收算法使用的是複製算法,複製算法的基本思想就是將內存分爲兩塊,每次只用其中一塊(from),當這一塊內存用完,就將還活着的對象複製到另一塊上面。複製算法的優勢是不會產生內存碎片,缺點是耗費空間

在GC開始的時候,對象只會存在於Eden區和名爲「From」的Survivor區,Survivor區「To」是空的。緊接着進行GC,Eden區中全部存活的對象都會被複制到「To」,而在「From」區中,仍存活的對象會根據他們的年齡值來決定去向。年齡達到必定值(年齡閾值,能夠經過-XX:MaxTenuringThreshold來設置)的對象會被移動到年老代中,沒有達到閾值的對象會被複制到「To」區域。通過此次GC後,Eden區和From區已經被清空。這個時候,「From」和「To」會交換他們的角色,也就是新的「To」就是上次GC前的「From」,新的「From」就是上次GC前的「To」。無論怎樣,都會保證名爲To的Survivor區域是空的。Minor GC會一直重複這樣的過程,直到「To」區被填滿,「To」區被填滿以後,會將全部對象移動到年老代中。

 
 

由於Eden區對象通常存活率較低,通常的,使用兩塊10%的內存做爲空閒和活動區間,而另外80%的內存,則是用來給新建對象分配內存的。一旦發生GC,將10%的from活動區間與另外80%中存活的eden對象轉移到10%的to空閒區間,接下來,將以前90%的內存所有釋放,以此類推。

 
蜜汁動畫:看不懂請忽略

上面動畫中,Area空閒表明to,Area激活表明from,綠色表明不被回收的,紅色表明被回收的。

複製算法它的缺點也是至關明顯的:

    1. 它浪費了一半的內存,這太要命了。
    1. 若是對象的存活率很高,咱們能夠極端一點,假設是100%存活,那麼咱們須要將全部對象都複製一遍,並將全部引用地址重置一遍。複製這一工做所花費的時間,在對象存活率達到必定程度時,將會變的不可忽視。 因此從以上描述不難看出,複製算法要想使用,最起碼對象的存活率要很是低才行,並且最重要的是,咱們必需要克服50%內存的浪費。

3 .標記清除(Mark-Sweep)

複製算法的缺點就是費空間,其是用在年輕代的,老年代通常是由標記清除或者是標記清除與標記整理的混合實現。

 
 

 
 

用通俗的話解釋一下標記清除算法,就是當程序運行期間,若可使用的內存被耗盡的時候,GC線程就會被觸發並將程序暫停,隨後將要回收的對象標記一遍,最終統一回收這些對象,完成標記清理工做接下來便讓應用程序恢復運行。

主要進行兩項工做,第一項則是標記,第二項則是清除。

  • 標記:從引用根節點開始標記遍歷全部的GC Roots, 先標記出要回收的對象。
  • 清除:遍歷整個堆,把標記的對象清除。

缺點:此算法須要暫停整個應用,會產生內存碎片

 
標記清除算法動態版

標記清除算法小結:

  • 一、首先,它的缺點就是效率比較低(遞歸與全堆對象遍歷),並且在進行GC的時候,須要中止應用程序,這會致使用戶體驗很是差勁
  • 二、其次,主要的缺點則是這種方式清理出來的空閒內存是不連續的,這點不難理解,咱們的死亡對象都是隨即的出如今內存的各個角落的,如今把它們清除以後,內存的佈局天然會亂七八糟。而爲了應付這一點,JVM就不得不維持一個內存的空閒列表,這又是一種開銷。並且在分配數組對象的時候,尋找連續的內存空間會不太好找。

4. 標記壓縮(Mark-Compact)

標記壓縮(Mark-Compact)又叫標記清除壓縮(Mark-Sweep-Compact),或者標記清除整理算法。老年代通常是由標記清除或者是標記清除與標記整理的混合實現

 
 
 
 
 
 
 
標記清除整理動態版

面試題:四種算法那個好
Answer:沒有那個算法是能一次性解決全部問題的,由於JVM垃圾回收使用的是分代收集算法,沒有最好的算法,只有根據每一代他的垃圾回收的特性用對應的算法。新生代使用複製算法,老年代使用標記清除和標記整理算法。沒有最好的垃圾回收機制,只有最合適的。

面試題:請說出各個垃圾回收算法的優缺點

  • 內存效率:複製算法>標記清除算法>標記整理算法(此處的效率只是簡單的對比時間複雜度,實際狀況不必定如此)。
  • 內存整齊度:複製算法=標記整理算法>標記清除算法。
  • 內存利用率:標記整理算法=標記清除算法>複製算法。

能夠看出,效率上來講,複製算法是當之無愧的老大,可是卻浪費了太多內存,而爲了儘可能兼顧上面所提到的三個指標,標記/整理算法相對來講更平滑一些,但效率上依然不盡如人意,它比複製算法多了一個標記的階段,又比標記/清除多了一個整理內存的過程

難道就沒有一種最優算法嗎?Java 9 以後出現了G1垃圾回收器,可以解決以上問題,有興趣參考這篇文章


總結:

  • 年輕代(Young Gen)

年輕代特色是區域相對老年代較小,對像存活率低。

這種狀況複製算法的回收整理,速度是最快的。複製算法的效率只和當前存活對像大小有關,於是很適用於年輕代的回收。而複製算法內存利用率不高的問題,經過hotspot中的兩個survivor的設計獲得緩解。

  • 老年代(Tenure Gen)

老年代的特色是區域較大,對像存活率高。

這種狀況,存在大量存活率高的對像,複製算法明顯變得不合適。通常是由標記清除或者是標記清除與標記整理的混合實現。

Mark階段的開銷與存活對像的數量成正比,這點上說來,對於老年代,標記清除或者標記整理有一些不符,但能夠經過多核/線程利用,對併發、並行的形式提標記效率。

Sweep階段的開銷與所管理區域的大小形正相關,但Sweep「就地處決」的特色,回收的過程沒有對像的移動。使其相對其它有對像移動步驟的回收算法,仍然是效率最好的。可是須要解決內存碎片問題。

Compact階段的開銷與存活對像的數據成開比,如上一條所描述,對於大量對像的移動是很大開銷的,作爲老年代的第一選擇並不合適。

基於上面的考慮,老年代通常是由標記清除或者是標記清除與標記整理的混合實現。以hotspot中的CMS回收器爲例,CMS是基於Mark-Sweep實現的,對於對像的回收效率很高,而對於碎片問題,CMS採用基於Mark-Compact算法的Serial Old回收器作爲補償措施:當內存回收不佳(碎片致使的Concurrent Mode Failure時),將採用Serial Old執行Full GC以達到對老年代內存的整理。

 

相關文章
相關標籤/搜索