擊穿JVM虛擬機

什麼是JVM虛擬機

首先咱們須要瞭解什麼是虛擬機,爲何虛擬機能夠實現誇平臺,虛擬機在計算機中扮演一個什麼樣的角色。java

虛擬機做用說明

(從下向上看)web

看上圖的操做系統與虛擬機層,能夠看到,JVM是在操做系統之上的。他幫咱們解決了操做系統差別性操做問題,因此能夠幫咱們實現誇操做系統。算法

JVM是若是實現誇操做系統的呢?

接着向上看,來到虛擬機可解析執行文件這裏,虛擬機就是根據這個.class的規範來實現誇平臺的。數組

在向上到語言層,不一樣的語言能夠有本身的語法、實現方式,但最終都要編譯爲一個知足.class規範的文件,來讓虛擬機執行。安全

因此理論上,任何語言想使用JVM虛擬機實現誇平臺的操做,均可以根據規範生成.class文件,就可使用JVM,並實現「一次編譯,屢次運行」。網絡

虛擬機具體幫咱們都作了哪些工做?

  1. 字節碼規範(.class)
  2. 內存管理

第一點已經在上邊說過,不在重複。數據結構

第二點內存管理也是咱們接下來主要講的內容。在沒有JVM的時代,在C/C++時期,寫代碼中除了寫正常的業務代碼以外,有很大一部分代碼是內存分配與銷燬相關的代碼。稍有不慎就會形成內存泄露。而使用虛擬機以後關於內存的分配、銷燬操做就都由虛擬機來管理了。多線程

相對的確定會形成虛擬機佔用更多內存,在性能上與C/C++對比會較差,但隨着虛擬機的慢慢成熟性能差距正在縮小。架構

JVM架構

Jvm虛擬機主要分爲五大模塊:類裝載子系統、運行時數據區、執行引擎、本地方法接口和垃圾收集模塊。併發

JVM架構圖

ClassLoader(類加載)

類的加載過程包含如下7步:

加載 -->校驗-->準備-->解析-->初始化-->使用-->卸載

其中鏈接校驗、準備-解析能夠統稱爲鏈接。

類加載過程

加載

1. 經過Class的全限定名獲取Class的二進制字節流
2. 將Class的二進制內容加載到虛擬機的方法區
3. 在內存中生成一個java.lang.Class對象表示這個Class

獲取Class的二進制字節流這個步驟有多種方式:

1. 從zip中讀取,如:從jar、war、ear等格式的文件中讀取Class文件內容
2. 從網絡中獲取,如:Applet
3. 動態生成,如:動態代理、ASM框架等都是基於此方式
4. 由其餘文件生成,典型的是從jsp文件生成相應的Class
類加載器

有兩種類型的類加載器

  • 虛擬機自帶的類加載器

    1. BootStrap ClassLoader(根加載器)
    該類加載器沒有父加載器,他負責加載虛擬機的核心類庫。
    如:java.lang.*等。
    根類加載器從系統屬性sun.boot.class.path所指定的目錄中加載類庫。
    根類加載器的實現依賴於底層操做系統,屬於虛擬機的實現的一部分,他並無繼承java.lang.ClassLoader類。
    如:java.lang.Object就是由根類加載器加載的。
    1. Extension ClassLoader(擴展類加載器)
    它的父類加載器爲根類加載器。
    他從java.ext.dirs系統屬性所指定的目錄中加載類庫,或者從JDK的安裝目錄的jre\lib\ext子目錄(擴展目錄)下加載類庫
    若是把用戶建立的JAR文件放在這個目錄下,也會自動有擴展類加載器加載。
    擴展類加載器是純java類,是java.lang.ClassLoader類的子類。
    1. App ClassLoader(系統<應用>類加載器)
    也稱爲應用加載器,他的父類加載器爲擴展類加載器。
    他從環境變量classpath或者系統屬性java.class.path所指定的目錄中加載類。
    他是用戶自定義的類加載器的默認父加載器。
    系統類加載器是純java類,是java.lang.ClassLoader子類。
  • 用戶自定義的類加載器

    1. 其必定是java.lang.ClassLoader抽象類(這個類自己就是提供給自定義加載器繼承的)的子類
    2. 用戶能夠定製的加載方式

類加載器

注意: 《類加載器的子父關係》非《子父類繼承關係》,而是一種數據結構,能夠比作一個鏈表形式或樹型結構。

代碼:

public class SystemClassLoader {
    public static void main(String[] args) {
        ClassLoader classLoader = ClassLoader.getSystemClassLoader();
        System.out.println(classLoader);

        while (classLoader != null){
            classLoader = classLoader.getParent();
            System.out.println(classLoader);
        }
    }
}

輸出:

sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@7a7b0070
null

得到類加載器的方法

方式 說明
clazz.getClassLoader(); 得到當前類的ClassLoader,clazz爲類的類對象,而不是普通對象
Thread.currentThread().getContextClassLoader(); 得到當先線程上下文的ClassLoader
ClassLoader.getSystemClassLoader(); 得到系統的ClassLoader
DriverManager.getCallerClssLoader(); 得到調用者的ClassLoader
/**
     * 獲取字符串的類加載器
     * 返回爲null表示使用的BootStrap ClassLoader
     */
    public static void getStringClassLoader(){
        Class clazz;
        try {
            clazz = Class.forName("java.lang.String");
            System.out.println("java.lang.String:   " + clazz.getClassLoader());
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }

輸出:

java.lang.List:   null

表示使用BootStrap ClassLoader加載
雙親委派機制(類加載器)

除了根加載器,每一個加載器被委託加載任務時,都是第一時間選擇讓其父加載器來執行加載操做,最終老是讓根類加載器來嘗試加載,若是加載失敗,則再依次返回加載,只要這個過程有一個加載器加載成功,那麼就會執行完成(這是Oracle公司Hotpot虛擬機默認執行的類加載機制,而且大部分虛擬機都是如此執行的),整個過程以下圖所示:

類加載過程

自定義類加載器:

public class FreeClassLoader extends ClassLoader {

    private File classPathFile;

    public FreeClassLoader(){
        String classPath = FreeClassLoader.class.getResource("").getPath();
        this.classPathFile = new File(classPath);
    }


    @Override
    protected Class<?> findClass(String name){
        if(classPathFile == null)
        {
            return null;
        }
        File classFile = new File(classPathFile,name.replaceAll("\\.","/") + ".class");
        if(!classFile.exists()){
            return null;
        }
        String className = FreeClassLoader.class.getPackage().getName() + "." + name;

        Class<?> clazz = null;
        try(FileInputStream in = new FileInputStream(classFile);
            ByteArrayOutputStream out = new ByteArrayOutputStream()){

            byte [] buff = new byte[1024];
            int len;
            while ((len = in.read(buff)) != -1){
                out.write(buff,0,len);
            }
            clazz = defineClass(className,out.toByteArray(),0,out.size());
        }catch (Exception e){
            e.printStackTrace();
        }
        return clazz;
    }

    /**
     * 測試加載
     * @param args
     */
    public static void main(String[] args) {
        FreeClassLoader classLoader = new FreeClassLoader();
        Class<?> clazz = classLoader.findClass("SystemClassLoader");
        try {
            Constructor constructor = clazz.getConstructor();
            Object obj = constructor.newInstance();
            System.out.println("當前:" + obj.getClass().getClassLoader());

            ClassLoader classLoader1 = obj.getClass().getClassLoader();

            while (classLoader1 != null){
                classLoader1 = classLoader1.getParent();
                System.out.println("父:" + classLoader1);
            }

            SystemClassLoader.getClassLoader("com.freecloud.javabasics.classload.SystemClassLoader");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

輸出:

當前:com.freecloud.javabasics.classload.FreeClassLoader@e6ea0c6
父:sun.misc.Launcher$AppClassLoader@18b4aac2
父:sun.misc.Launcher$ExtClassLoader@1c6b6478
父:null
com.freecloud.javabasics.classload.SystemClassLoader:   sun.misc.Launcher$AppClassLoader@18b4aac2

校驗

驗證一個Class的二進制內容是否合法

1. 文件格式驗證,確保文件格式符合Class文件格式的規範。
   如:驗證魔數、版本號等。
2. 元數據驗證,確保Class的語義描述符合Java的Class規範。
   如:該Class是否有父類、是否錯誤繼承了final類、是否一個合法的抽象類等。
3. 字節碼驗證,經過分析數據流和控制流,確保程序語義符合邏輯。
   如:驗證類型轉換是合法的。
4. 符號引用驗證,發生於符號引用轉換爲直接引用的時候(轉換髮生在解析階段)。
   如:驗證引用的類、成員變量、方法的是否能夠被訪問(IllegalAccessError),當前類是否存在相應的方法、成員等(NoSuchMethodError、NoSuchFieldError)。

使用記事本或文本工具打開任意.class文件就會看到以下字節碼內容:

Class文件

左邊方框內容表示魔數: cafe babe(做用是肯定這個文件是否爲一個能被虛擬機接收的Class文件)
  右邊方框表示版本號 :0000 0034 (16進制轉爲10進製爲52表示JDK1.8)

class文件說明

準備

在準備階段,虛擬機會在方法區中爲Class分配內存,並設置static成員變量的初始值爲默認值。

注意這裏僅僅會爲static變量分配內存(static變量在方法區中),而且初始化static變量的值爲其所屬類型的默認值。
如:int類型初始化爲0,引用類型初始化爲null。
即便聲明瞭這樣一個static變量:

public static int a = 123;

在準備階段後,a在內存中的值仍然是0, 賦值123這個操做會在中初始化階段執行,所以在初始化階段產生了對應的Class對象以後a的值纔是123 。
public class Test{
   private static int a =1;
   public static long b;
   public static String str;
   
   static{
       b = 2;
       str = "hello world"
   }
}

爲int類型的靜態變量 a 分配4個字節(32位)的內存空間,並賦值爲默認值0;
爲long類的靜態變量 b 分配8個字節(64位)的內存空間,並默認賦值爲0;
爲String類型的靜態變量 str 默認賦值爲null。

解析

解析階段,虛擬機會將常量池中的符號引用替換爲直接引用,解析主要針對的是類、接口、方法、成員變量等符號引用。在轉換成直接引用後,會觸發校驗階段的符號引用驗證,驗證轉換以後的直接引用是否能找到對應的類、方法、成員變量等。這裏也可見類加載的各個階段在實際過程當中,多是交錯執行。

public class DynamicLink {

    static class Super{
        public void test(){
            System.out.println("super");
        }
    }

    static class Sub1 extends Super{

        @Override
        public void test(){
            System.out.println("Sub1");
        }
    }
    static class Sub2 extends Super {
        @Override
        public void test() {
            System.out.println("Sub2");
        }
    }

    public static void main(String[] args) {
        Super super1 = new Sub1();
        Super super2 = new Sub2();

        super1.test();
        super2.test();
    }
}

在解析階段,虛擬機會把類的二進制數據中的符號引用替換爲直接引用。

類解析,引用替換

初始化

初始化階段即開始在內存中構造一個Class對象來表示該類,即執行類構造器<clinit>()的過程。須要注意下,<clinit>()不等同於建立類實例的構造方法<init>()

1. <clinit>()方法中執行的是對static變量進行賦值的操做,以及static語句塊中的操做。
2. 虛擬機會確保先執行父類的<clinit>()方法。
3. 若是一個類中沒有static的語句塊,也沒有對static變量的賦值操做,那麼虛擬機不會爲這個類生成<clinit>()方法。
4. 虛擬機會保證<clinit>()方法的執行過程是線程安全的。

使用

Java程序對類的使用方式能夠分爲兩種

  1. 主動使用
  2. 被動使用

主動使用類的七中方式,即類的初始化時機:

1. 建立類的實例;
2. 訪問某個類或接口的靜態變量(無重寫的變量繼承,變量其屬於父類,而不屬於子類),或者對該靜態變量賦值(靜態的read/write操做);
3. 調用類的靜態方法;
4. 反射(如:Class.forName("com.test.Test"));
5. 初始化一個類的子類(Chlidren 繼承了Parent類,若是僅僅初始化一個Children類,那麼Parent類也是被主動使用了);
6. Java虛擬機啓動時被標明爲啓動類的類(換句話說就是包含main方法的那個類,並且自己main方法就是static的);
7. JDK1.7開始提供的動態語言的支持:java.lang.invoke.MethodHandle實例的解析結果REF_getStatic,REF_public,REF_invokeStatic句柄對應的類沒有初始化,則初始化;

除了上述所講七種狀況,其餘使用Java類的方式都被看做是對類的被動使用,都不會致使類的初始化,好比:調用ClassLoader類的loadClass()方法加載一個類,並非對類的主動使用,不會致使類的初始化。

注意: 
初始化單單是上述類加載、鏈接、初始化過程當中的第三步,被動使用並不會規定前面兩個步驟被使用與否
也就是說即便被動使用只是不會引發類的初始化,可是徹底能夠進行類的加載以及鏈接。
例如:調用ClassLoader類的loadClass方法加載一個類,這並非對類的主動使用,不會致使類的初始化。

須要銘記於心的一點:
只有當程序訪問的靜態變量或靜態變量確實在當前類或當前接口中定義時,才能夠認爲是對類或接口的主動使用,經過子類調用繼承過來的靜態變量算做父類的主動使用。

卸載

JVM中的Class只有知足如下三個條件,才能被被卸載(unload)

1. 該類全部的實例都已經被GC,也就是JVM中不存在該Class的任何實例。
2. 加載該類的ClassLoader已經被GC。
3. 該類的java.lang.Class 對象沒有在任何地方被引用。
   如:不能在任何地方經過反射訪問該類的方法。

運行時數據區(虛擬機的內存模型)

運行時數據區

運行時數據區主要分兩大塊:
  線程共享:方法區(常量池、類信息、靜態常量等)、堆(存儲實例對象)
  線程獨佔:程序計數器、虛擬機棧、本地方法棧

程序計數器(PC寄存器)

程序計數器是一塊較小的內存空間,它的做用能夠看做是當前線程所執行的字節碼的行號指示器。在虛擬機的概念模型裏字節碼解釋器工做時就是經過改變這個計數器的值來選取下一條須要執行的字節碼指令,分支、循環、跳轉、異常處理、線程恢復等基礎功能都須要依賴這個計數器來完成。

特色:
  1. 若是線程正在執行的是Java 方法,則這個計數器記錄的是正在執行的虛擬機字節碼指令地址
  2. 若是正在執行的是Native 方法,則這個技術器值爲空(Undefined)
  3. 此內存區域是惟一一個在Java虛擬機規範中沒有規定任何OutOfMemoryError狀況的區域
public class ProgramCounterJavap {
    public static void main(String[] args) {
        int a = 1;
        int b = 10;
        int c = 100;
        System.out.println( a + b * c);
    }
}

使用javap反彙編工具可看到以下圖: 程序計數器

圖中紅框位置就是字節碼指令的偏移地址,當執行到main(java.lang.String[])時在當前線程中會建立相應的程序計數器,在計數器中存放執行地址(紅框中內容)。

這也說明程序在運行過程當中計數器改變的只是值,而不是隨着程序的運行須要更大的空間,也就不會發生溢出狀況。

虛擬機棧

一個方法表示一個棧,遵循先進後出的方式。每一個棧中又分局部變量表、操做數棧、動態鏈表、返回地址等等。

虛擬機棧是線程隔離的,即每一個線程都有本身獨立的虛擬機棧。

局部變量:存儲方法參數和方法內部定義的局部變量名
  操做數棧:棧針指令集(表達式棧)
  動態連接:保存指向運行時常量池中該指針所屬方法的引用 。做用是運行期將符號引用轉化爲直接引用
  返回地址:保留退出方法時,上層方法執行狀態信息

虛擬機棧

虛擬機棧

虛擬機棧的StackOverflowError

單個線程請求的棧深度大於虛擬機容許的深度,則會拋出StackOverflowError(棧溢出錯誤)

JVM會爲每一個線程的虛擬機棧分配必定的內存大小(-Xss參數),所以虛擬機棧可以容納的棧幀數量是有限的,若棧幀不斷進棧而不出棧,最終會致使當前線程虛擬機棧的內存空間耗盡,典型如一個無結束條件的遞歸函數調用,代碼見下:

/**
 * 虛擬機棧的StackOverflowError
 * JVM參數:-Xss160k
 * @Author: maomao
 * @Date: 2019-11-12 09:48
 */
public class JVMStackSOF {
    private int count = 0;
    /**
     * 經過遞歸調用形成StackOverFlowError
     */
    public void stackLeak() {
        count++;
        stackLeak();
    }
    public static void main(String[] args) {
        JVMStackSOF oom = new JVMStackSOF();
        try {
            oom.stackLeak();
        }catch (Throwable e){
            System.out.println("stack count : " + oom.count);
            e.printStackTrace();
        }
    }
}

設置單個線程的虛擬機棧內存大小爲160K,執行main方法後,拋出了StackOverflow異常

stack count : 771
java.lang.StackOverflowError
	at com.freecloud.javabasics.jvm.JVMStackSOF.stackLeak(JVMStackSOF.java:18)
	at com.freecloud.javabasics.jvm.JVMStackSOF.stackLeak(JVMStackSOF.java:19)
	at com.freecloud.javabasics.jvm.JVMStackSOF.stackLeak(JVMStackSOF.java:19)
	at com.freecloud.javabasics.jvm.JVMStackSOF.stackLeak(JVMStackSOF.java:19)
	at com.freecloud.javabasics.jvm.JVMStackSOF.stackLeak(JVMStackSOF.java:19)

虛擬機棧的OutOfMemoryError

不一樣於StackOverflowError,OutOfMemoryError指的是當整個虛擬機棧內存耗盡,而且沒法再申請到新的內存時拋出的異常。

JVM未提供設置整個虛擬機棧佔用內存的配置參數。虛擬機棧的最大內存大體上等於「JVM進程能佔用的最大內存(依賴於具體操做系統) - 最大堆內存 - 最大方法區內存 - 程序計數器內存(能夠忽略不計) - JVM進程自己消耗內存」。當虛擬機棧可以使用的最大內存被耗盡後,便會拋出OutOfMemoryError,能夠經過不斷開啓新的線程來模擬這種異常,代碼以下:

/**
 * java棧溢出OutOfMemoryError
 * JVM參數:-Xms20M -Xmx20M -Xmn10M -Xss2m -verbose:gc -XX:+PrintGCDetails
 * @Author: maomao
 * @Date: 2019-11-12 10:10
 */
public class JVMStackOOM {

    private void dontStop() {
        try {
            Thread.sleep(24 * 60 * 60 * 1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    /**
     * 經過不斷的建立新的線程使Stack內存耗盡
     */
    public void stackLeakByThread(){
        while (true){
            Thread thread = new Thread(() -> dontStop());
            thread.start();
        }
    }

    public static void main(String[] args) {
        JVMStackOOM oom = new JVMStackOOM();
        oom.stackLeakByThread();
    }
}

本地方法棧

方法區(Method Area)

方法區,主要存放已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據。

字節碼常量池

常亮池中的值是在類加載階段時,經過靜態方法塊加載到內存中

靜態方法

Heap(堆)

對於絕大多數應用來講,這塊區域是 JVM 所管理的內存中最大的一塊。線程共享,主要是存放對象實例和數組。內部會劃分出多個線程私有的分配緩衝區(Thread Local Allocation Buffer, TLAB)。能夠位於物理上不連續的空間,可是邏輯上要連續。也是咱們在開發過程當中主要使用的地方。

1.8前Heap內存模型

在jdk 1.8以前的版本heap分新生代、老年帶、永久代,但在1.8以後永久代修改成元空間,本質與永久代相似,都是對JVM規範中方法區的實現。元空間不在虛擬機中,而是在本地內存中。

1.8以後heap內存模型

爲何內存要分代?

咱們使用下面一個生活中的例子來講明:

首先咱們把整個內存處理過程比做一個倉庫管理,用戶會有不一樣的東西要在咱們倉庫作存取。

倉庫中的貨物比做咱們內存中的實例,用戶會不肯定時間來咱們這作存取操做,如今讓咱們來管理這個倉庫,咱們如何作到效率最大化。

用戶會有不一樣大小的貨物要寄存,咱們不作特殊處理,就是誰先來了按照固定的順序存放。以下圖

初始倉庫

但過了一段時間以後,用戶會不按期拿走本身的貨物

倉庫-取走

這時在咱們倉庫中就會產生大小不一樣的空位,若是這時還有用戶來存入貨物時,就會發現咱們須要拿着貨物在倉庫中找到合適的空位放進去(像俄羅斯方塊),但用戶的貨物不必定會正好放到對應的空位中,就會產生不一樣大小的空位,並且很差找。

新貨物存入

若是在有貨物取走以後咱們就整理一次的話,又會很是累也耗時。

這時咱們就會發現,若是咱們不對倉庫作有效的劃分管理的話,咱們的使用效率很是低。

咱們將倉庫邏輯的劃分爲:

  • 最經常使用: 用戶全部的貨物都先進入到這裏,若是用戶只是臨時存放,能夠快速從這裏取走。除非貨物大小超過倉庫剩餘空間(或咱們認定的大貨物)。
  • 臨時緩衝一、2: 臨時緩衝存放,存放小於必定天數的貨物暫時放到這裏,當超出天數還未取走再放到後臺倉庫中。
  • 後臺倉庫: 存放大貨物與長期無人取的貨物

倉庫劃分

上圖劃分了倆大區域,左邊比較小的是經常使用區域,用戶在存入貨物時最早放到這裏,對於臨時存取的貨物能夠很是快的處理。 右邊比較大的區域作爲後臺倉庫,存放長時間無人取的或者經常使用區沒法放下的大貨物。

倉庫劃分(取)

倉庫劃分(存)

經過這樣的劃分咱們就能夠把存取快的小貨物在一個較小的區域中處理,而不須要到大倉庫中去找,能夠極大的提高倉庫效率。

垃圾回收算法

JVM的垃圾回收算法是對內存空間管理的一種實現算法,是在逐漸演進中的內存管理算法。

標記-清除

標記-清除算法,就像他的名字同樣,分爲「標記」和「清除」兩個階段。首先遍歷全部內存,將存活對象進行標記。清除階段遍歷堆中全部沒被標記的對象進行所有清除。在整個過程當中會形成整個程序的stop the world。

缺點:

  1. 形成stop the world(暫停整個程序)
  2. 產生內存碎片
  3. 效率低

爲何要stop the world?

舉個簡單的例子,假設咱們的程序與GC線程是一塊兒運行的,試想這樣一個場景。

假設咱們剛標記完的A對象(非存活對象),此時在程序當中又new了一個新的對象B,且A對象能夠到達B對象。
  但因爲此時A對象在標記階段已被標記爲非存活對象,B對象錯過了標記階段。所以到清除階段時,新對象會將B對象清除掉。如此一來GC線程會致使程序沒法正常工做。
  咱們剛new了一個對象,通過一次GC,變爲了null,會嚴重影響程序運行。

產生內存碎片

內存被清理完以後就會產生像下圖3中(像俄羅斯方框遊戲同樣),空閒的位置不連續,若是須要爲新的對象分配內存空間時,沒法建立連續較大的空間,甚至在建立時還須要搜索整個內存空間哪有空餘空間能夠分配。

效率低

也就是上邊兩個缺點的集合,會形成程序stop the world影響程序執行,產生內存碎片勢必在分配時會須要更多的時間去找合適的位置來分配。

標記清除算法

複製

爲解決標記清除算法的缺點,提高效率,「複製」收集算法出現了。它將可用的內存空間按容量劃分爲大小相等的兩塊,每次只使用其中一塊。當這一塊內存用完了,就將還存活的對象複製到另一快上,而後把已使用過的內存空間一次清理掉。

這樣使每次都是對其中一塊進行內存回收,內存分配也不用考慮內存碎片等複雜狀況,只要移動指針按順序分配內存就能夠了,實現簡單運行高效。

缺點:

  1. 在存活對象較多時,複製操做次數多,效率低。
  2. 內存縮小了一半

複製算法

標記-整理

針對以上兩種算法的問題,又出現了「標記-整理」算法,看名字與「標記-清除」算法類似,不一樣的地方就是在「整理」階段。

在《深刻理解Java虛擬機》中對「整理」階段的說明是:"讓全部存活對象都向一端移動,而後直接清理掉端邊界之外的內存"

沒有找到具體某一個使用的方案,我分別畫了3張圖來表示個人理解:

標記-移動-清除

標記-移動-清除

相似冒泡排序,把存活對象像最左側移動

疑問:

  1. 若是肯定邊界?記錄最後一個存活對象移動的位置,後邊的所有清除?

  2. 爲何不是遇到可回收對象先回收再移動,這樣能夠減小移動可回收對象的操做(除非回收須要的性能比移動還高)

標記-移動-清除 2

標記-移動-清除 2

劃分移動區域,將存活對象暫時放到該區域,而後一次清理使用過的內存,最後再將存活對象一次移動

疑問:

  1. 如何分配邏輯足夠存活對象的連續內存空間?

  2. 若是空間不足怎麼辦?

標記-清除-整理

標記-清除-整理

以上我對標記-整理算法理解,若有不對的地方還請指正。

垃圾收集算法對比

參考資料:

https://liujiacai.net/blog/2018/07/08/mark-sweep/

https://www.azul.com/files/Understanding_Java_Garbage_Collection_v41.pdf

分代收集

分代收集不是一種新的算法,是針對對象的存活週期的不一樣將內存劃分爲幾塊。當前商業虛擬機的垃圾收集都採用「分代收集」。

GC分代的基本假設:絕大部分對象的生命週期都很是短暫,存活時間短。

把Java堆分爲新生代和老年代,這樣就能夠根據各個年代的特色採用最適當的收集算法。

  • 新生代 每次垃圾收集時都發現有大批對象死去,只有少許存活,那就選用複製算法,只須要付出少許存活對象的複製成本就能夠完成收集。

  • 老年代 由於對象存活率高、沒有額外空間對它進行分配擔保,就必須使用「標記-清理」或「標記-整理」算法來進行回收。

垃圾收集器

垃圾收集器,就是針對垃圾回收算法的具體實現。

下圖是對收集器的推薦組合關係圖,有連線的說明能夠搭配使用。沒有最好的收集器,也沒有萬能的收集器,只有最合適的收集器。

垃圾收集器,推薦組合

Serial

  • 特色:

    - 單線程、簡單高效(與其餘收集器的單線程相比),對於限定單個CPU的環境來講,Serial收集器因爲沒有線程交互的開銷,專心作垃圾收集天然能夠得到最高的單線程收集效率。
    - 收集器進行垃圾回收時,必須暫停其餘全部的工做線程,直到它結束(Stop The World)。
  • 應用場景:

    適用於Client模式下的虛擬機

Serial / Serial Old 收集器運行示意圖

ParNew

ParNew收集器其實就是Serial收集器的多線程版本。

除了使用多線程外其他行爲均和Serial收集器如出一轍(參數控制、收集算法、Stop The World、對象分配規則、回收策略等)

  • 特色:

    - 多線程、ParNew收集器默認開啓的收集線程數與CPU的數量相同,在CPU很是多的環境中,可使用-XX:ParallelGCThreads參數來限制垃圾收集的線程數。
    - 與Serial收集器同樣存在Stop The World問題
  • 應用場景:

    ParNew收集器是許多運行在Server模式下的虛擬機中首選的新生代收集器,由於它是除了Serial收集器外,惟一一個能與CMS收集器配合工做的。

Parallel Scavenge

與吞吐量關係密切,故也稱爲吞吐量優先收集器。 除了使用多線程外其他行爲均和Serial收集器如出一轍(參數控制、收集算法、Stop The World、對象分配規則、回收策略等)

  • 特色:

    屬於新生代收集器也是採用複製算法的收集器,又是並行的多線程收集器(與ParNew收集器相似)。

該收集器的目標是達到一個可控制的吞吐量。還有一個值得關注的點是:GC自適應調節策略(與ParNew收集器最重要的一個區別)

  • GC自適應調節策略:

    Parallel Scavenge收集器可設置-XX:+UseAdptiveSizePolicy參數。
    當開關打開時不須要手動指定新生代的大小(-Xmn)、Eden與Survivor區的比例(-XX:SurvivorRation)、晉升老年代的對象年齡(-XX:PretenureSizeThreshold)等。
    虛擬機會根據系統的運行情況收集性能監控信息,動態設置這些參數以提供最優的停頓時間和最高的吞吐量,這種調節方式稱爲GC的自適應調節策略。
    
    
    Parallel Scavenge收集器使用兩個參數控制吞吐量:
         XX:MaxGCPauseMillis 控制最大的垃圾收集停頓時間
         XX:GCRatio 直接設置吞吐量的大小。

Serial Old

Serial Old是Serial收集器的老年代版本。

  • 特色:一樣是單線程收集器,採用標記-整理算法。

  • 應用場景:主要也是使用在Client模式下的虛擬機中。也可在Server模式下使用。

Server模式下主要的兩大用途

1.在JDK1.5以及之前的版本中與Parallel Scavenge收集器搭配使用。
  2.做爲CMS收集器的後備方案,在併發收集Concurent Mode Failure時使用。

Serial / Serial Old 收集器運行示意圖

CMS

一種以獲取最短回收停頓時間爲目標的收集器。

  • 特色:基於標記-清除算法實現。併發收集、低停頓。

  • 應用場景:

適用於注重服務的響應速度,但願系統停頓時間最短,給用戶帶來更好的體驗等場景下。如web程序、b/s服務。

  • CMS收集器的運行過程分爲下列4步:

    初始標記:標記GC Roots能直接到的對象。速度很快可是仍存在Stop The World問題。
    併發標記:進行GC Roots Tracing 的過程,找出存活對象且用戶線程可併發執行。
    從新標記:爲了修正併發標記期間因用戶程序繼續運行而致使標記產生變更的那一部分對象的標記記錄。仍然存在Stop The World問題。
    併發清除:對標記的對象進行清除回收。

CMS收集器的內存回收過程是與用戶線程一塊兒併發執行的。

CMS收集器的缺點:

  1. 對CPU資源很是敏感。
  2. 沒法處理浮動垃圾,可能出現Concurrent Model Failure失敗而致使另外一次Full GC的產生。
  3. 由於採用標記-清除算法因此會存在空間碎片的問題,致使大對象沒法分配空間,不得不提早觸發一次Full GC。

CMS 收集器運行示意圖

G1

一款面向服務端應用的垃圾收集器。再也不是將整個內存區域按代總體劃分,他根據,將每個內存單元獨立爲Region區,每一個Region仍是按代劃分。 以下圖:

G1 Heap Allocation

  • 特色:

    - 並行與併發:G1能充分利用多CPU、多核環境下的硬件優點,使用多個CPU來縮短Stop-The-World停頓時間。
    部分收集器本來須要停頓Java線程來執行GC動做,G1收集器仍然能夠經過併發的方式讓Java程序繼續運行。
    
    - 分代收集:G1可以獨自管理整個Java堆,而且採用不一樣的方式去處理新建立的對象和已經存活了一段時間、熬過屢次GC的舊對象以獲取更好的收集效果。
    
    - 空間整合:G1運做期間不會產生空間碎片,收集後能提供規整的可用內存。
    
    - 可預測的停頓:G1除了追求低停頓外,還能創建可預測的停頓時間模型。能讓使用者明確指定在一個長度爲M毫秒的時間段內,消耗在垃圾收集上的時間不得超過N毫秒。

G1爲何能創建可預測的停頓時間模型?

由於它有計劃的避免在整個Java堆中進行全區域的垃圾收集。G1跟蹤各個Region裏面的垃圾堆積的大小,在後臺維護一個優先列表,每次根據容許的收集時間,優先回收價值最大的Region。這樣就保證了在有限的時間內能夠獲取儘量高的收集效率。

G1與其餘收集器的區別:

其餘收集器的工做範圍是整個新生代或者老年代、G1收集器的工做範圍是整個Java堆。在使用G1收集器時,它將整個Java堆劃分爲多個大小相等的獨立區域(Region)。雖然也保留了新生代、老年代的概念,但新生代和老年代再也不是相互隔離的,他們都是一部分Region(不須要連續)的集合。

G1收集器存在的問題:

Region不多是孤立的,分配在Region中的對象能夠與Java堆中的任意對象發生引用關係。在採用可達性分析算法來判斷對象是否存活時,得掃描整個Java堆才能保證準確性。其餘收集器也存在這種問題(G1更加突出而已)。會致使Minor GC效率降低。

G1收集器是如何解決上述問題的?

採用Remembered Set來避免整堆掃描。G1中每一個Region都有一個與之對應的Remembered Set,虛擬機發現程序在對Reference類型進行寫操做時,會產生一個Write Barrier暫時中斷寫操做,檢查Reference引用對象是否處於多個Region中(即檢查老年代中是否引用了新生代中的對象),若是是,便經過CardTable把相關引用信息記錄到被引用對象所屬的Region的Remembered Set中。當進行內存回收時,在GC根節點的枚舉範圍中加入Remembered Set便可保證不對全堆進行掃描也不會有遺漏。

若是不計算維護 Remembered Set 的操做,G1收集器大體可分爲以下步驟:

- 初始標記:僅標記GC Roots能直接到的對象,而且修改TAMS(Next Top at Mark Start)的值,讓下一階段用戶程序併發運行時,能在正確可用的Region中建立新對象。(須要線程停頓,但耗時很短。)

  - 併發標記:從GC Roots開始對堆中對象進行可達性分析,找出存活對象。(耗時較長,但可與用戶程序併發執行)

  - 最終標記:爲了修正在併發標記期間因用戶程序執行而致使標記產生變化的那一部分標記記錄。且對象的變化記錄在線程Remembered Set  Logs裏面,把Remembered Set  Logs裏面的數據合併到Remembered Set中。(須要線程停頓,但可並行執行。)

  - 篩選回收:對各個Region的回收價值和成本進行排序,根據用戶所指望的GC停頓時間來制定回收計劃。(可併發執行)

G1 收集器運行示意圖

如何肯定某個對象是垃圾?

上邊詳細說了垃圾收集相關的內容,那有很重要的一點沒有說,就是如何肯定某個對象是垃圾對象,可被回收呢? 有下邊兩種方式,虛擬機中使用的是可達性分析算法。

引用計數法

給對象添加一個引用計數器,每當有一個地方引用他的時候,計數器的數值就+1,當引用失效時,計數器就-1。

任什麼時候候計數器的數值都爲0的對象時不可能再被使用的。

可達性分析算法 (java使用)

以GC Roots的對象做爲起始點,從這些起始點開始向下搜索,搜索所搜過的路徑稱爲引用鏈Reference Chain,當一個對象到GC Roots沒有任何引用鏈相鏈接時,則證實此對象時不可用的。

什麼是GC Roots?

在虛擬機中可做爲GC Roots的對象有如下幾種:

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

彙編指令

彙編指令是指可被虛擬機識別指令,咱們平時看到的.class字節碼文件中就存放着咱們某個類的彙編指令,經過了解彙編指令,能夠幫助咱們更深刻了解虛擬機的工做機制與內存分配方式。

使用javap查看到指令集

javap是jdk自帶的反解析工具。它的做用就是根據class字節碼文件,反解析出當前類對應的code區(彙編指令)、本地變量表、異常表和代碼行偏移量映射表、常量池等等信息。

固然這些信息中,有些信息(如本地變量表、指令和代碼行偏移量映射表、常量池中方法的參數名稱等等)須要在使用javac編譯成class文件時,指定參數才能輸出,好比,你直接javac xx.java,就不會在生成對應的局部變量表等信息,若是你使用javac -g xx.java就能夠生成全部相關信息了。

javap的用法格式: javap <options> <classes>

用法與參數:

-help  --help  -?        輸出此用法消息
 -version                 版本信息,實際上是當前javap所在jdk的版本信息,不是class在哪一個jdk下生成的。
 -v  -verbose             輸出附加信息(包括行號、本地變量表,反彙編等詳細信息)
 -l                         輸出行號和本地變量表
 -public                    僅顯示公共類和成員
 -protected               顯示受保護的/公共類和成員
 -package                 顯示程序包/受保護的/公共類 和成員 (默認)
 -p  -private             顯示全部類和成員
 -c                       對代碼進行反彙編
 -s                       輸出內部類型簽名
 -sysinfo                 顯示正在處理的類的系統信息 (路徑, 大小, 日期, MD5 散列)
 -constants               顯示靜態最終常量
 -classpath <path>        指定查找用戶類文件的位置
 -bootclasspath <path>    覆蓋引導類文件的位置

通常經常使用的是-v -l -c三個選項。

下面經過一個簡單例子說明一下彙編指令,具體說明會以註釋形式說明。

具體指令做用與意思可參考該地址:

http://www.javashuo.com/article/p-kcgbkvmp-mr.html

package com.freecloud.javabasics.javap;

/**
 * @Author: maomao
 * @Date: 2019-11-01 09:57
 */
public class StringJavap {

    /**
     * String與StringBuilder
     */
    public void StringAndStringBuilder(){
        String s1 = "111" +  "222";
        StringBuilder s2 = new StringBuilder("111").append("222");

        System.out.println(s1);
        System.out.println(s2);
    }

    public void StringStatic(){
        String s1 = "333";
        String s2 = "444";
        String s3 = s1 + s2;
        String s4 = s1 + "555";
    }

    private static final String STATIC_STRING = "staticString";
    public void StringStatic2(){
        String s1 = "111";
        String s2 = STATIC_STRING + 111;
    }
}

彙編指令

//文件地址
Classfile /Users/workspace/free-cloud-test/free-javaBasics/javap/target/classes/com/freecloud/javabasics/javap/StringJavap.class
  //最後修改日期與文件大小
  Last modified 2019-11-5; size 1432 bytes
  MD5 checksum 1c6892dd51b214a205eae9612124535d
  Compiled from "StringJavap.java"
  //類信息
public class com.freecloud.javabasics.javap.StringJavap
  minor version: 0
  //編譯版本號(jdk1.8)
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
  //常量池
Constant pool:
   #1 = Methodref          #18.#45        // java/lang/Object."<init>":()V
   #2 = String             #46            // 111222
   #3 = Class              #47            // java/lang/StringBuilder
   #4 = String             #48            // 111
   #5 = Methodref          #3.#49         // java/lang/StringBuilder."<init>":(Ljava/lang/String;)V
   #6 = String             #50            // 222
   #7 = Methodref          #3.#51         // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
   #8 = Fieldref           #52.#53        // java/lang/System.out:Ljava/io/PrintStream;
   #9 = Methodref          #54.#55        // java/io/PrintStream.println:(Ljava/lang/String;)V
  #10 = Methodref          #54.#56        // java/io/PrintStream.println:(Ljava/lang/Object;)V
  #11 = String             #57            // 333
  #12 = String             #58            // 444
  #13 = Methodref          #3.#45         // java/lang/StringBuilder."<init>":()V
  #14 = Methodref          #3.#59         // java/lang/StringBuilder.toString:()Ljava/lang/String;
  #15 = String             #60            // 555
  #16 = Class              #61            // com/freecloud/javabasics/javap/StringJavap
  #17 = String             #62            // staticString111
  #18 = Class              #63            // java/lang/Object
  #19 = Utf8               STATIC_STRING
  #20 = Utf8               Ljava/lang/String;
  #21 = Utf8               ConstantValue
  #22 = String             #64            // staticString
  #23 = Utf8               <init>
  #24 = Utf8               ()V
  #25 = Utf8               Code
  #26 = Utf8               LineNumberTable
  #27 = Utf8               LocalVariableTable
  #28 = Utf8               this
  #29 = Utf8               Lcom/freecloud/javabasics/javap/StringJavap;
  #30 = Utf8               main
  #31 = Utf8               ([Ljava/lang/String;)V
  #32 = Utf8               args
  #33 = Utf8               [Ljava/lang/String;
  #34 = Utf8               MethodParameters
  #35 = Utf8               StringAndStringBuilder
  #36 = Utf8               s1
  #37 = Utf8               s2
  #38 = Utf8               Ljava/lang/StringBuilder;
  #39 = Utf8               StringStatic
  #40 = Utf8               s3
  #41 = Utf8               s4
  #42 = Utf8               StringStatic2
  #43 = Utf8               SourceFile
  #44 = Utf8               StringJavap.java
  #45 = NameAndType        #23:#24        // "<init>":()V
  #46 = Utf8               111222
  #47 = Utf8               java/lang/StringBuilder
  #48 = Utf8               111
  #49 = NameAndType        #23:#65        // "<init>":(Ljava/lang/String;)V
  #50 = Utf8               222
  #51 = NameAndType        #66:#67        // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  #52 = Class              #68            // java/lang/System
  #53 = NameAndType        #69:#70        // out:Ljava/io/PrintStream;
  #54 = Class              #71            // java/io/PrintStream
  #55 = NameAndType        #72:#65        // println:(Ljava/lang/String;)V
  #56 = NameAndType        #72:#73        // println:(Ljava/lang/Object;)V
  #57 = Utf8               333
  #58 = Utf8               444
  #59 = NameAndType        #74:#75        // toString:()Ljava/lang/String;
  #60 = Utf8               555
  #61 = Utf8               com/freecloud/javabasics/javap/StringJavap
  #62 = Utf8               staticString111
  #63 = Utf8               java/lang/Object
  #64 = Utf8               staticString
  #65 = Utf8               (Ljava/lang/String;)V
  #66 = Utf8               append
  #67 = Utf8               (Ljava/lang/String;)Ljava/lang/StringBuilder;
  #68 = Utf8               java/lang/System
  #69 = Utf8               out
  #70 = Utf8               Ljava/io/PrintStream;
  #71 = Utf8               java/io/PrintStream
  #72 = Utf8               println
  #73 = Utf8               (Ljava/lang/Object;)V
  #74 = Utf8               toString
  #75 = Utf8               ()Ljava/lang/String;
{
  //默認構造方法
  public com.freecloud.javabasics.javap.StringJavap();
   //輸入參數(該處表示無參)
    descriptor: ()V
    flags: ACC_PUBLIC
  //指令代碼《也是執行代碼,重點關注》
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
	//指令與代碼中的行號關係
      LineNumberTable:
        line 7: 0
	//本地變量表
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/freecloud/javabasics/javap/StringJavap;
  // 對應StringAndStringBuilder方法
  public void StringAndStringBuilder();
    descriptor: ()V
	//描述方法關鍵字
    flags: ACC_PUBLIC
    Code:
	  //stack()  locals(本地變量數/方法內使用的變量數) args_size(入參數,全部方法都有一個this因此參數至少爲1)
      stack=3, locals=3, args_size=1
	     //經過#2可在常量池中找到111222字符串,表示在編譯時就把本來的"111" + "222"合併爲一個常量
         0: ldc           #2                  // String 111222
         2: astore_1
         3: new           #3                  // class java/lang/StringBuilder
         6: dup
         7: ldc           #4                  // String 111
         9: invokespecial #5                  // Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V
        12: ldc           #6                  // String 222
        14: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        17: astore_2
        18: getstatic     #8                  // Field java/lang/System.out:Ljava/io/PrintStream;
        21: aload_1
        22: invokevirtual #9                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        25: getstatic     #8                  // Field java/lang/System.out:Ljava/io/PrintStream;
        28: aload_2
        29: invokevirtual #10                 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
		//返回指針,不管方法是否有返回值,都會有該指令,做用是出棧
        32: return
      LineNumberTable:
        line 19: 0
        line 20: 3
        line 22: 18
        line 23: 25
        line 24: 32
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      33     0  this   Lcom/freecloud/javabasics/javap/StringJavap;
            3      30     1    s1   Ljava/lang/String;
           18      15     2    s2   Ljava/lang/StringBuilder;

  public void StringStatic();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=5, args_size=1
         0: ldc           #11                 // String 333
         2: astore_1
         3: ldc           #12                 // String 444
         5: astore_2
         6: new           #3                  // class java/lang/StringBuilder
         9: dup
        10: invokespecial #13                 // Method java/lang/StringBuilder."<init>":()V
        13: aload_1
        14: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        17: aload_2
        18: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        21: invokevirtual #14                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        24: astore_3
        25: new           #3                  // class java/lang/StringBuilder
        28: dup
        29: invokespecial #13                 // Method java/lang/StringBuilder."<init>":()V
        32: aload_1
        33: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        36: ldc           #15                 // String 555
        38: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        41: invokevirtual #14                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        44: astore        4
        46: return
      LineNumberTable:
        line 27: 0
        line 28: 3
        line 29: 6
        line 30: 25
        line 31: 46
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      47     0  this   Lcom/freecloud/javabasics/javap/StringJavap;
            3      44     1    s1   Ljava/lang/String;
            6      41     2    s2   Ljava/lang/String;
           25      22     3    s3   Ljava/lang/String;
           46       1     4    s4   Ljava/lang/String;

  public void StringStatic2();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=3, args_size=1
         0: ldc           #4                  // String 111
         2: astore_1
         3: ldc           #17                 // String staticString111
         5: astore_2
         6: return
      LineNumberTable:
        line 35: 0
        line 36: 3
        line 37: 6
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       7     0  this   Lcom/freecloud/javabasics/javap/StringJavap;
            3       4     1    s1   Ljava/lang/String;
            6       1     2    s2   Ljava/lang/String;
}
SourceFile: "StringJavap.java"

能夠在指令集中明確看到咱們上邊講解的內存運行時數據區的一些影子。

好比常量池、本地變量表、虛擬機棧(每一個方法能夠理解爲一個棧,具體方法內就是Code區)、返回地址(return)

相關文章
相關標籤/搜索