一文解析JVM虛擬機

什麼是JVM虛擬機

首先我們需要了解什麼是虛擬機,爲什麼虛擬機可以實現誇平臺,虛擬機在計算機中扮演一個什麼樣的角色。

虛擬機作用說明

(從下向上看)

看上圖的操作系統與虛擬機層,可以看到,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.String:   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)。可以位於物理上不連續的空間,但是邏輯上要連續。也是我們在開發過程中主要使用的地方。

Heap的數據是二叉樹實現,每個分配的地址會存儲內存地址、與對象長度。

1.8前Heap內存模型

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

1.8之後heap內存模型

爲什麼內存要分代?

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

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

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

用戶會有不同大小的貨物要寄存,我們不做特殊處理,就是誰先來了按照固定的順序存放。如下圖

初始倉庫

但過了一段時間之後,用戶會不定期拿走自己的貨物

倉庫-取走

這時在我們倉庫中就會產生大小不同的空位,如果這時還有用戶來存入貨物時,就會發現我們需要拿着貨物在倉庫中找到合適的空位放進去(像俄羅斯方塊),但用戶的貨物不一定會正好放到對應的空位中,就會產生不同大小的空位,而且不好找。

新貨物存入

如果在有貨物取走之後我們就整理一次的話,又會非常累也耗時。

這時我們就會發現,如果我們不對倉庫做有效的劃分管理的話,我們的使用效率非常低。

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

  • 最常用: 用戶所有的貨物都先進入到這裏,如果用戶只是臨時存放,可以快速從這裏取走。除非貨物大小超過倉庫剩餘空間(或我們認定的大貨物)。
  • 臨時緩衝1、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三個選項。

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

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

https://my.oschina.net/u/1019754/blog/3116798

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)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章