溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

怎樣解析JVM虛擬機(jī)

發(fā)布時(shí)間:2021-10-23 16:25:57 來(lái)源:億速云 閱讀:156 作者:柒染 欄目:編程語(yǔ)言

本篇文章給大家分享的是有關(guān)怎樣解析JVM虛擬機(jī),小編覺得挺實(shí)用的,因此分享給大家學(xué)習(xí),希望大家閱讀完這篇文章后可以有所收獲,話不多說(shuō),跟著小編一起來(lái)看看吧。

什么是JVM虛擬機(jī)

首先我們需要了解什么是虛擬機(jī),為什么虛擬機(jī)可以實(shí)現(xiàn)夸平臺(tái),虛擬機(jī)在計(jì)算機(jī)中扮演一個(gè)什么樣的角色。

怎樣解析JVM虛擬機(jī)

(從下向上看)

看上圖的操作系統(tǒng)與虛擬機(jī)層,可以看到,JVM是在操作系統(tǒng)之上的。他幫我們解決了操作系統(tǒng)差異性操作問(wèn)題,所以可以幫我們實(shí)現(xiàn)夸操作系統(tǒng)。

JVM是如果實(shí)現(xiàn)夸操作系統(tǒng)的呢?

接著向上看,來(lái)到虛擬機(jī)可解析執(zhí)行文件這里,虛擬機(jī)就是根據(jù)這個(gè).class的規(guī)范來(lái)實(shí)現(xiàn)夸平臺(tái)的。

在向上到語(yǔ)言層,不同的語(yǔ)言可以有自己的語(yǔ)法、實(shí)現(xiàn)方式,但最終都要編譯為一個(gè)滿足.class規(guī)范的文件,來(lái)讓虛擬機(jī)執(zhí)行。

所以理論上,任何語(yǔ)言想使用JVM虛擬機(jī)實(shí)現(xiàn)夸平臺(tái)的操作,都可以根據(jù)規(guī)范生成.class文件,就可以使用JVM,并實(shí)現(xiàn)“一次編譯,多次運(yùn)行”。

虛擬機(jī)具體幫我們都做了哪些工作?
  1. 字節(jié)碼規(guī)范(.class)

  2. 內(nèi)存管理

第一點(diǎn)已經(jīng)在上邊說(shuō)過(guò),不在重復(fù)。

第二點(diǎn)內(nèi)存管理也是我們接下來(lái)主要講的內(nèi)容。在沒(méi)有JVM的時(shí)代,在C/C++時(shí)期,寫代碼中除了寫正常的業(yè)務(wù)代碼之外,有很大一部分代碼是內(nèi)存分配與銷毀相關(guān)的代碼。稍有不慎就會(huì)造成內(nèi)存泄露。而使用虛擬機(jī)之后關(guān)于內(nèi)存的分配、銷毀操作就都由虛擬機(jī)來(lái)管理了。

相對(duì)的肯定會(huì)造成虛擬機(jī)占用更多內(nèi)存,在性能上與C/C++對(duì)比會(huì)較差,但隨著虛擬機(jī)的慢慢成熟性能差距正在縮小。

JVM架構(gòu)

Jvm虛擬機(jī)主要分為五大模塊:類裝載子系統(tǒng)、運(yùn)行時(shí)數(shù)據(jù)區(qū)、執(zhí)行引擎、本地方法接口和垃圾收集模塊。

怎樣解析JVM虛擬機(jī)

ClassLoader(類加載)

類的加載過(guò)程包含以下7步:

加載 -->校驗(yàn)-->準(zhǔn)備-->解析-->初始化-->使用-->卸載

其中連接校驗(yàn)、準(zhǔn)備-解析可以統(tǒng)稱為連接。

怎樣解析JVM虛擬機(jī)

加載
1. 通過(guò)Class的全限定名獲取Class的二進(jìn)制字節(jié)流
2. 將Class的二進(jìn)制內(nèi)容加載到虛擬機(jī)的方法區(qū)
3. 在內(nèi)存中生成一個(gè)java.lang.Class對(duì)象表示這個(gè)Class

獲取Class的二進(jìn)制字節(jié)流這個(gè)步驟有多種方式:

1. 從zip中讀取,如:從jar、war、ear等格式的文件中讀取Class文件內(nèi)容
2. 從網(wǎng)絡(luò)中獲取,如:Applet
3. 動(dòng)態(tài)生成,如:動(dòng)態(tài)代理、ASM框架等都是基于此方式
4. 由其他文件生成,典型的是從jsp文件生成相應(yīng)的Class
類加載器

有兩種類型的類加載器

  • 虛擬機(jī)自帶的類加載器

    該類加載器沒(méi)有父加載器,他負(fù)責(zé)加載虛擬機(jī)的核心類庫(kù)。
    如:java.lang.*等。
    根類加載器從系統(tǒng)屬性sun.boot.class.path所指定的目錄中加載類庫(kù)。
    根類加載器的實(shí)現(xiàn)依賴于底層操作系統(tǒng),屬于虛擬機(jī)的實(shí)現(xiàn)的一部分,他并沒(méi)有繼承java.lang.ClassLoader類。
    如:java.lang.Object就是由根類加載器加載的。

     

    它的父類加載器為根類加載器。
    他從java.ext.dirs系統(tǒng)屬性所指定的目錄中加載類庫(kù),或者從JDK的安裝目錄的jre\lib\ext子目錄(擴(kuò)展目錄)下加載類庫(kù)
    如果把用戶創(chuàng)建的JAR文件放在這個(gè)目錄下,也會(huì)自動(dòng)有擴(kuò)展類加載器加載。
    擴(kuò)展類加載器是純java類,是java.lang.ClassLoader類的子類。

     

    也稱為應(yīng)用加載器,他的父類加載器為擴(kuò)展類加載器。
    他從環(huán)境變量classpath或者系統(tǒng)屬性java.class.path所指定的目錄中加載類。
    他是用戶自定義的類加載器的默認(rèn)父加載器。
    系統(tǒng)類加載器是純java類,是java.lang.ClassLoader子類。


    1. App ClassLoader(系統(tǒng)<應(yīng)用>類加載器)

    1. Extension ClassLoader(擴(kuò)展類加載器)

    1. BootStrap ClassLoader(根加載器)

  • 用戶自定義的類加載器

    1. 其一定是java.lang.ClassLoader抽象類(這個(gè)類本身就是提供給自定義加載器繼承的)的子類

    2. 用戶可以定制的加載方式

怎樣解析JVM虛擬機(jī)

注意: 《類加載器的子父關(guān)系》非《子父類繼承關(guān)系》,而是一種數(shù)據(jù)結(jié)構(gòu),可以比做一個(gè)鏈表形式或樹型結(jié)構(gòu)。

代碼:

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

獲得類加載器的方法

方式說(shuō)明
clazz.getClassLoader();獲得當(dāng)前類的ClassLoader,clazz為類的類對(duì)象,而不是普通對(duì)象
Thread.currentThread().getContextClassLoader();獲得當(dāng)先線程上下文的ClassLoader
ClassLoader.getSystemClassLoader();獲得系統(tǒng)的ClassLoader
DriverManager.getCallerClssLoader();獲得調(diào)用者的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加載
雙親委派機(jī)制(類加載器)

除了根加載器,每個(gè)加載器被委托加載任務(wù)時(shí),都是第一時(shí)間選擇讓其父加載器來(lái)執(zhí)行加載操作,最終總是讓根類加載器來(lái)嘗試加載,如果加載失敗,則再依次返回加載,只要這個(gè)過(guò)程有一個(gè)加載器加載成功,那么就會(huì)執(zhí)行完成(這是Oracle公司Hotpot虛擬機(jī)默認(rèn)執(zhí)行的類加載機(jī)制,并且大部分虛擬機(jī)都是如此執(zhí)行的),整個(gè)過(guò)程如下圖所示:

怎樣解析JVM虛擬機(jī)

自定義類加載器:

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;
    }

    /**
     * 測(cè)試加載
     * @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("當(dāng)前:" + 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();
        }
    }
}

輸出:

當(dāng)前: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
校驗(yàn)

驗(yàn)證一個(gè)Class的二進(jìn)制內(nèi)容是否合法

1. 文件格式驗(yàn)證,確保文件格式符合Class文件格式的規(guī)范。
   如:驗(yàn)證魔數(shù)、版本號(hào)等。
2. 元數(shù)據(jù)驗(yàn)證,確保Class的語(yǔ)義描述符合Java的Class規(guī)范。
   如:該Class是否有父類、是否錯(cuò)誤繼承了final類、是否一個(gè)合法的抽象類等。
3. 字節(jié)碼驗(yàn)證,通過(guò)分析數(shù)據(jù)流和控制流,確保程序語(yǔ)義符合邏輯。
   如:驗(yàn)證類型轉(zhuǎn)換是合法的。
4. 符號(hào)引用驗(yàn)證,發(fā)生于符號(hào)引用轉(zhuǎn)換為直接引用的時(shí)候(轉(zhuǎn)換發(fā)生在解析階段)。
   如:驗(yàn)證引用的類、成員變量、方法的是否可以被訪問(wèn)(IllegalAccessError),當(dāng)前類是否存在相應(yīng)的方法、成員等(NoSuchMethodError、NoSuchFieldError)。

使用記事本或文本工具打開任意.class文件就會(huì)看到如下字節(jié)碼內(nèi)容:

怎樣解析JVM虛擬機(jī)

  左邊方框內(nèi)容表示魔數(shù): cafe babe(作用是確定這個(gè)文件是否為一個(gè)能被虛擬機(jī)接收的Class文件)
  右邊方框表示版本號(hào) :0000 0034 (16進(jìn)制轉(zhuǎn)為10進(jìn)制為52表示JDK1.8)

怎樣解析JVM虛擬機(jī)

準(zhǔn)備

在準(zhǔn)備階段,虛擬機(jī)會(huì)在方法區(qū)中為Class分配內(nèi)存,并設(shè)置static成員變量的初始值為默認(rèn)值。

注意這里僅僅會(huì)為static變量分配內(nèi)存(static變量在方法區(qū)中),并且初始化static變量的值為其所屬類型的默認(rèn)值。
如:int類型初始化為0,引用類型初始化為null。
即使聲明了這樣一個(gè)static變量:

public static int a = 123;

在準(zhǔn)備階段后,a在內(nèi)存中的值仍然是0, 賦值123這個(gè)操作會(huì)在中初始化階段執(zhí)行,因此在初始化階段產(chǎn)生了對(duì)應(yīng)的Class對(duì)象之后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類型的靜態(tài)變量 a 分配4個(gè)字節(jié)(32位)的內(nèi)存空間,并賦值為默認(rèn)值0;
為long類的靜態(tài)變量 b 分配8個(gè)字節(jié)(64位)的內(nèi)存空間,并默認(rèn)賦值為0;
為String類型的靜態(tài)變量 str 默認(rèn)賦值為null。
解析

解析階段,虛擬機(jī)會(huì)將常量池中的符號(hào)引用替換為直接引用,解析主要針對(duì)的是類、接口、方法、成員變量等符號(hào)引用。在轉(zhuǎn)換成直接引用后,會(huì)觸發(fā)校驗(yàn)階段的符號(hào)引用驗(yàn)證,驗(yàn)證轉(zhuǎn)換之后的直接引用是否能找到對(duì)應(yīng)的類、方法、成員變量等。這里也可見類加載的各個(gè)階段在實(shí)際過(guò)程中,可能是交錯(cuò)執(zhí)行。

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();
    }
}

在解析階段,虛擬機(jī)會(huì)把類的二進(jìn)制數(shù)據(jù)中的符號(hào)引用替換為直接引用。

怎樣解析JVM虛擬機(jī)

初始化

初始化階段即開始在內(nèi)存中構(gòu)造一個(gè)Class對(duì)象來(lái)表示該類,即執(zhí)行類構(gòu)造器<clinit>()的過(guò)程。需要注意下,<clinit>()不等同于創(chuàng)建類實(shí)例的構(gòu)造方法<init>()

1. <clinit>()方法中執(zhí)行的是對(duì)static變量進(jìn)行賦值的操作,以及static語(yǔ)句塊中的操作。
2. 虛擬機(jī)會(huì)確保先執(zhí)行父類的<clinit>()方法。
3. 如果一個(gè)類中沒(méi)有static的語(yǔ)句塊,也沒(méi)有對(duì)static變量的賦值操作,那么虛擬機(jī)不會(huì)為這個(gè)類生成<clinit>()方法。
4. 虛擬機(jī)會(huì)保證<clinit>()方法的執(zhí)行過(guò)程是線程安全的。
使用

Java程序?qū)︻惖氖褂梅绞娇梢苑譃閮煞N

  1. 主動(dòng)使用

  2. 被動(dòng)使用

主動(dòng)使用類的七中方式,即類的初始化時(shí)機(jī):

1. 創(chuàng)建類的實(shí)例;
2. 訪問(wèn)某個(gè)類或接口的靜態(tài)變量(無(wú)重寫的變量繼承,變量其屬于父類,而不屬于子類),或者對(duì)該靜態(tài)變量賦值(靜態(tài)的read/write操作);
3. 調(diào)用類的靜態(tài)方法;
4. 反射(如:Class.forName("com.test.Test"));
5. 初始化一個(gè)類的子類(Chlidren 繼承了Parent類,如果僅僅初始化一個(gè)Children類,那么Parent類也是被主動(dòng)使用了);
6. Java虛擬機(jī)啟動(dòng)時(shí)被標(biāo)明為啟動(dòng)類的類(換句話說(shuō)就是包含main方法的那個(gè)類,而且本身main方法就是static的);
7. JDK1.7開始提供的動(dòng)態(tài)語(yǔ)言的支持:java.lang.invoke.MethodHandle實(shí)例的解析結(jié)果REF_getStatic,REF_public,REF_invokeStatic句柄對(duì)應(yīng)的類沒(méi)有初始化,則初始化;

除了上述所講七種情況,其他使用Java類的方式都被看作是對(duì)類的被動(dòng)使用,都不會(huì)導(dǎo)致類的初始化,比如:調(diào)用ClassLoader類的loadClass()方法加載一個(gè)類,并不是對(duì)類的主動(dòng)使用,不會(huì)導(dǎo)致類的初始化。

注意: 
初始化單單是上述類加載、連接、初始化過(guò)程中的第三步,被動(dòng)使用并不會(huì)規(guī)定前面兩個(gè)步驟被使用與否
也就是說(shuō)即使被動(dòng)使用只是不會(huì)引起類的初始化,但是完全可以進(jìn)行類的加載以及連接。
例如:調(diào)用ClassLoader類的loadClass方法加載一個(gè)類,這并不是對(duì)類的主動(dòng)使用,不會(huì)導(dǎo)致類的初始化。

需要銘記于心的一點(diǎn):
只有當(dāng)程序訪問(wèn)的靜態(tài)變量或靜態(tài)變量確實(shí)在當(dāng)前類或當(dāng)前接口中定義時(shí),才可以認(rèn)為是對(duì)類或接口的主動(dòng)使用,通過(guò)子類調(diào)用繼承過(guò)來(lái)的靜態(tài)變量算作父類的主動(dòng)使用。
卸載

JVM中的Class只有滿足以下三個(gè)條件,才能被被卸載(unload)

1. 該類所有的實(shí)例都已經(jīng)被GC,也就是JVM中不存在該Class的任何實(shí)例。
2. 加載該類的ClassLoader已經(jīng)被GC。
3. 該類的java.lang.Class 對(duì)象沒(méi)有在任何地方被引用。
   如:不能在任何地方通過(guò)反射訪問(wèn)該類的方法。

運(yùn)行時(shí)數(shù)據(jù)區(qū)(虛擬機(jī)的內(nèi)存模型)

怎樣解析JVM虛擬機(jī)

  運(yùn)行時(shí)數(shù)據(jù)區(qū)主要分兩大塊:
  線程共享:方法區(qū)(常量池、類信息、靜態(tài)常量等)、堆(存儲(chǔ)實(shí)例對(duì)象)
  線程獨(dú)占:程序計(jì)數(shù)器、虛擬機(jī)棧、本地方法棧

程序計(jì)數(shù)器(PC寄存器)

程序計(jì)數(shù)器是一塊較小的內(nèi)存空間,它的作用可以看作是當(dāng)前線程所執(zhí)行的字節(jié)碼的行號(hào)指示器。在虛擬機(jī)的概念模型里字節(jié)碼解釋器工作時(shí)就是通過(guò)改變這個(gè)計(jì)數(shù)器的值來(lái)選取下一條需要執(zhí)行的字節(jié)碼指令,分支、循環(huán)、跳轉(zhuǎn)、異常處理、線程恢復(fù)等基礎(chǔ)功能都需要依賴這個(gè)計(jì)數(shù)器來(lái)完成。

  特點(diǎn):
  1. 如果線程正在執(zhí)行的是Java 方法,則這個(gè)計(jì)數(shù)器記錄的是正在執(zhí)行的虛擬機(jī)字節(jié)碼指令地址
  2. 如果正在執(zhí)行的是Native 方法,則這個(gè)技術(shù)器值為空(Undefined)
  3. 此內(nèi)存區(qū)域是唯一一個(gè)在Java虛擬機(jī)規(guī)范中沒(méi)有規(guī)定任何OutOfMemoryError情況的區(qū)域
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反匯編工具可看到如下圖: 怎樣解析JVM虛擬機(jī)

圖中紅框位置就是字節(jié)碼指令的偏移地址,當(dāng)執(zhí)行到main(java.lang.String[])時(shí)在當(dāng)前線程中會(huì)創(chuàng)建相應(yīng)的程序計(jì)數(shù)器,在計(jì)數(shù)器中存放執(zhí)行地址(紅框中內(nèi)容)。

這也說(shuō)明程序在運(yùn)行過(guò)程中計(jì)數(shù)器改變的只是值,而不是隨著程序的運(yùn)行需要更大的空間,也就不會(huì)發(fā)生溢出情況。

虛擬機(jī)棧

一個(gè)方法表示一個(gè)棧,遵循先進(jìn)后出的方式。每個(gè)棧中又分局部變量表、操作數(shù)棧、動(dòng)態(tài)鏈表、返回地址等等。

虛擬機(jī)棧是線程隔離的,即每個(gè)線程都有自己獨(dú)立的虛擬機(jī)棧。

  局部變量:存儲(chǔ)方法參數(shù)和方法內(nèi)部定義的局部變量名
  操作數(shù)棧:棧針指令集(表達(dá)式棧)
  動(dòng)態(tài)鏈接:保存指向運(yùn)行時(shí)常量池中該指針?biāo)鶎俜椒ǖ囊?nbsp;。作用是運(yùn)行期將符號(hào)引用轉(zhuǎn)化為直接引用
  返回地址:保留退出方法時(shí),上層方法執(zhí)行狀態(tài)信息

怎樣解析JVM虛擬機(jī)

怎樣解析JVM虛擬機(jī)

虛擬機(jī)棧的StackOverflowError

單個(gè)線程請(qǐng)求的棧深度大于虛擬機(jī)允許的深度,則會(huì)拋出StackOverflowError(棧溢出錯(cuò)誤)

JVM會(huì)為每個(gè)線程的虛擬機(jī)棧分配一定的內(nèi)存大小(-Xss參數(shù)),因此虛擬機(jī)棧能夠容納的棧幀數(shù)量是有限的,若棧幀不斷進(jìn)棧而不出棧,最終會(huì)導(dǎo)致當(dāng)前線程虛擬機(jī)棧的內(nèi)存空間耗盡,典型如一個(gè)無(wú)結(jié)束條件的遞歸函數(shù)調(diào)用,代碼見下:

/**
 * 虛擬機(jī)棧的StackOverflowError
 * JVM參數(shù):-Xss160k
 * @Author: maomao
 * @Date: 2019-11-12 09:48
 */
public class JVMStackSOF {
    private int count = 0;
    /**
     * 通過(guò)遞歸調(diào)用造成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();
        }
    }
}

設(shè)置單個(gè)線程的虛擬機(jī)棧內(nèi)存大小為160K,執(zhí)行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)

虛擬機(jī)棧的OutOfMemoryError

不同于StackOverflowError,OutOfMemoryError指的是當(dāng)整個(gè)虛擬機(jī)棧內(nèi)存耗盡,并且無(wú)法再申請(qǐng)到新的內(nèi)存時(shí)拋出的異常。

JVM未提供設(shè)置整個(gè)虛擬機(jī)棧占用內(nèi)存的配置參數(shù)。虛擬機(jī)棧的最大內(nèi)存大致上等于“JVM進(jìn)程能占用的最大內(nèi)存(依賴于具體操作系統(tǒng)) - 最大堆內(nèi)存 - 最大方法區(qū)內(nèi)存 - 程序計(jì)數(shù)器內(nèi)存(可以忽略不計(jì)) - JVM進(jìn)程本身消耗內(nèi)存”。當(dāng)虛擬機(jī)棧能夠使用的最大內(nèi)存被耗盡后,便會(huì)拋出OutOfMemoryError,可以通過(guò)不斷開啟新的線程來(lái)模擬這種異常,代碼如下:

/**
 * java棧溢出OutOfMemoryError
 * JVM參數(shù):-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();
        }
    }

    /**
     * 通過(guò)不斷的創(chuàng)建新的線程使Stack內(nèi)存耗盡
     */
    public void stackLeakByThread(){
        while (true){
            Thread thread = new Thread(() -> dontStop());
            thread.start();
        }
    }

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

本地方法棧

方法區(qū)(Method Area)

方法區(qū),主要存放已被虛擬機(jī)加載的類信息、常量、靜態(tài)變量、即時(shí)編譯器編譯后的代碼等數(shù)據(jù)。

怎樣解析JVM虛擬機(jī)

常亮池中的值是在類加載階段時(shí),通過(guò)靜態(tài)方法塊加載到內(nèi)存中

怎樣解析JVM虛擬機(jī)

Heap(堆)

對(duì)于絕大多數(shù)應(yīng)用來(lái)說(shuō),這塊區(qū)域是 JVM 所管理的內(nèi)存中最大的一塊。線程共享,主要是存放對(duì)象實(shí)例和數(shù)組。內(nèi)部會(huì)劃分出多個(gè)線程私有的分配緩沖區(qū)(Thread Local Allocation Buffer, TLAB)??梢晕挥谖锢砩喜贿B續(xù)的空間,但是邏輯上要連續(xù)。也是我們?cè)陂_發(fā)過(guò)程中主要使用的地方。

Heap的數(shù)據(jù)是二叉樹實(shí)現(xiàn),每個(gè)分配的地址會(huì)存儲(chǔ)內(nèi)存地址、與對(duì)象長(zhǎng)度。

怎樣解析JVM虛擬機(jī)

在jdk 1.8之前的版本heap分新生代、老年帶、永久代,但在1.8之后永久代修改為元空間,本質(zhì)與永久代類似,都是對(duì)JVM規(guī)范中方法區(qū)的實(shí)現(xiàn)。元空間不在虛擬機(jī)中,而是在本地內(nèi)存中。

怎樣解析JVM虛擬機(jī)

為什么內(nèi)存要分代?

我們使用下面一個(gè)生活中的例子來(lái)說(shuō)明:

首先我們把整個(gè)內(nèi)存處理過(guò)程比作一個(gè)倉(cāng)庫(kù)管理,用戶會(huì)有不同的東西要在我們倉(cāng)庫(kù)做存取。

倉(cāng)庫(kù)中的貨物比作我們內(nèi)存中的實(shí)例,用戶會(huì)不確定時(shí)間來(lái)我們這做存取操作,現(xiàn)在讓我們來(lái)管理這個(gè)倉(cāng)庫(kù),我們?nèi)绾巫龅叫首畲蠡?/p>

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

怎樣解析JVM虛擬機(jī)

但過(guò)了一段時(shí)間之后,用戶會(huì)不定期拿走自己的貨物

怎樣解析JVM虛擬機(jī)

這時(shí)在我們倉(cāng)庫(kù)中就會(huì)產(chǎn)生大小不同的空位,如果這時(shí)還有用戶來(lái)存入貨物時(shí),就會(huì)發(fā)現(xiàn)我們需要拿著貨物在倉(cāng)庫(kù)中找到合適的空位放進(jìn)去(像俄羅斯方塊),但用戶的貨物不一定會(huì)正好放到對(duì)應(yīng)的空位中,就會(huì)產(chǎn)生不同大小的空位,而且不好找。

怎樣解析JVM虛擬機(jī)

如果在有貨物取走之后我們就整理一次的話,又會(huì)非常累也耗時(shí)。

這時(shí)我們就會(huì)發(fā)現(xiàn),如果我們不對(duì)倉(cāng)庫(kù)做有效的劃分管理的話,我們的使用效率非常低。

我們將倉(cāng)庫(kù)邏輯的劃分為:

  • 最常用: 用戶所有的貨物都先進(jìn)入到這里,如果用戶只是臨時(shí)存放,可以快速?gòu)倪@里取走。除非貨物大小超過(guò)倉(cāng)庫(kù)剩余空間(或我們認(rèn)定的大貨物)。

  • 臨時(shí)緩沖1、2: 臨時(shí)緩沖存放,存放小于一定天數(shù)的貨物暫時(shí)放到這里,當(dāng)超出天數(shù)還未取走再放到后臺(tái)倉(cāng)庫(kù)中。

  • 后臺(tái)倉(cāng)庫(kù): 存放大貨物與長(zhǎng)期無(wú)人取的貨物

怎樣解析JVM虛擬機(jī)

上圖劃分了倆大區(qū)域,左邊比較小的是常用區(qū)域,用戶在存入貨物時(shí)最先放到這里,對(duì)于臨時(shí)存取的貨物可以非??斓奶幚怼?右邊比較大的區(qū)域做為后臺(tái)倉(cāng)庫(kù),存放長(zhǎng)時(shí)間無(wú)人取的或者常用區(qū)無(wú)法放下的大貨物。

怎樣解析JVM虛擬機(jī)

怎樣解析JVM虛擬機(jī)

通過(guò)這樣的劃分我們就可以把存取快的小貨物在一個(gè)較小的區(qū)域中處理,而不需要到大倉(cāng)庫(kù)中去找,可以極大的提升倉(cāng)庫(kù)效率。

垃圾回收算法

JVM的垃圾回收算法是對(duì)內(nèi)存空間管理的一種實(shí)現(xiàn)算法,是在逐漸演進(jìn)中的內(nèi)存管理算法。

標(biāo)記-清除

標(biāo)記-清除算法,就像他的名字一樣,分為“標(biāo)記”和“清除”兩個(gè)階段。首先遍歷所有內(nèi)存,將存活對(duì)象進(jìn)行標(biāo)記。清除階段遍歷堆中所有沒(méi)被標(biāo)記的對(duì)象進(jìn)行全部清除。在整個(gè)過(guò)程中會(huì)造成整個(gè)程序的stop the world。

缺點(diǎn):

  1. 造成stop the world(暫停整個(gè)程序)

  2. 產(chǎn)生內(nèi)存碎片

  3. 效率低

為什么要stop the world?

舉個(gè)簡(jiǎn)單的例子,假設(shè)我們的程序與GC線程是一起運(yùn)行的,試想這樣一個(gè)場(chǎng)景。

  假設(shè)我們剛標(biāo)記完的A對(duì)象(非存活對(duì)象),此時(shí)在程序當(dāng)中又new了一個(gè)新的對(duì)象B,且A對(duì)象可以到達(dá)B對(duì)象。
  但由于此時(shí)A對(duì)象在標(biāo)記階段已被標(biāo)記為非存活對(duì)象,B對(duì)象錯(cuò)過(guò)了標(biāo)記階段。因此到清除階段時(shí),新對(duì)象會(huì)將B對(duì)象清除掉。如此一來(lái)GC線程會(huì)導(dǎo)致程序無(wú)法正常工作。
  我們剛new了一個(gè)對(duì)象,經(jīng)過(guò)一次GC,變?yōu)榱薾ull,會(huì)嚴(yán)重影響程序運(yùn)行。

產(chǎn)生內(nèi)存碎片

內(nèi)存被清理完之后就會(huì)產(chǎn)生像下圖3中(像俄羅斯方框游戲一樣),空閑的位置不連續(xù),如果需要為新的對(duì)象分配內(nèi)存空間時(shí),無(wú)法創(chuàng)建連續(xù)較大的空間,甚至在創(chuàng)建時(shí)還需要搜索整個(gè)內(nèi)存空間哪有空余空間可以分配。

效率低

也就是上邊兩個(gè)缺點(diǎn)的集合,會(huì)造成程序stop the world影響程序執(zhí)行,產(chǎn)生內(nèi)存碎片勢(shì)必在分配時(shí)會(huì)需要更多的時(shí)間去找合適的位置來(lái)分配。

怎樣解析JVM虛擬機(jī)

復(fù)制

為解決標(biāo)記清除算法的缺點(diǎn),提升效率,“復(fù)制”收集算法出現(xiàn)了。它將可用的內(nèi)存空間按容量劃分為大小相等的兩塊,每次只使用其中一塊。當(dāng)這一塊內(nèi)存用完了,就將還存活的對(duì)象復(fù)制到另外一快上,然后把已使用過(guò)的內(nèi)存空間一次清理掉。

這樣使每次都是對(duì)其中一塊進(jìn)行內(nèi)存回收,內(nèi)存分配也不用考慮內(nèi)存碎片等復(fù)雜情況,只要移動(dòng)指針按順序分配內(nèi)存就可以了,實(shí)現(xiàn)簡(jiǎn)單運(yùn)行高效。

缺點(diǎn):

  1. 在存活對(duì)象較多時(shí),復(fù)制操作次數(shù)多,效率低。

  2. 內(nèi)存縮小了一半

怎樣解析JVM虛擬機(jī)

標(biāo)記-整理

針對(duì)以上兩種算法的問(wèn)題,又出現(xiàn)了“標(biāo)記-整理”算法,看名字與“標(biāo)記-清除”算法相似,不同的地方就是在“整理”階段。

在《深入理解Java虛擬機(jī)》中對(duì)“整理”階段的說(shuō)明是:"讓所有存活對(duì)象都向一端移動(dòng),然后直接清理掉端邊界以外的內(nèi)存"

沒(méi)有找到具體某一個(gè)使用的方案,我分別畫了3張圖來(lái)表示我的理解:

標(biāo)記-移動(dòng)-清除

怎樣解析JVM虛擬機(jī)

類似冒泡排序,把存活對(duì)象像最左側(cè)移動(dòng)

疑問(wèn):

  1. 如果確定邊界?記錄最后一個(gè)存活對(duì)象移動(dòng)的位置,后邊的全部清除?

  2. 為什么不是遇到可回收對(duì)象先回收再移動(dòng),這樣可以減少移動(dòng)可回收對(duì)象的操作(除非回收需要的性能比移動(dòng)還高)

標(biāo)記-移動(dòng)-清除 2

怎樣解析JVM虛擬機(jī)

劃分移動(dòng)區(qū)域,將存活對(duì)象暫時(shí)放到該區(qū)域,然后一次清理使用過(guò)的內(nèi)存,最后再將存活對(duì)象一次移動(dòng)

疑問(wèn):

  1. 如何分配邏輯足夠存活對(duì)象的連續(xù)內(nèi)存空間?

  2. 如果空間不足怎么辦?

標(biāo)記-清除-整理

怎樣解析JVM虛擬機(jī)

以上我對(duì)標(biāo)記-整理算法理解,如有不對(duì)的地方還請(qǐng)指正。

怎樣解析JVM虛擬機(jī)

參考資料:

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

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

分代收集

分代收集不是一種新的算法,是針對(duì)對(duì)象的存活周期的不同將內(nèi)存劃分為幾塊。當(dāng)前商業(yè)虛擬機(jī)的垃圾收集都采用“分代收集”。

GC分代的基本假設(shè):絕大部分對(duì)象的生命周期都非常短暫,存活時(shí)間短。

把Java堆分為新生代和老年代,這樣就可以根據(jù)各個(gè)年代的特點(diǎn)采用最適當(dāng)?shù)氖占惴ā?/p>

  • 新生代 每次垃圾收集時(shí)都發(fā)現(xiàn)有大批對(duì)象死去,只有少量存活,那就選用復(fù)制算法,只需要付出少量存活對(duì)象的復(fù)制成本就可以完成收集。

  • 老年代 因?yàn)閷?duì)象存活率高、沒(méi)有額外空間對(duì)它進(jìn)行分配擔(dān)保,就必須使用“標(biāo)記-清理”或“標(biāo)記-整理”算法來(lái)進(jìn)行回收。

垃圾收集器

垃圾收集器,就是針對(duì)垃圾回收算法的具體實(shí)現(xiàn)。

下圖是對(duì)收集器的推薦組合關(guān)系圖,有連線的說(shuō)明可以搭配使用。沒(méi)有最好的收集器,也沒(méi)有萬(wàn)能的收集器,只有最合適的收集器。

怎樣解析JVM虛擬機(jī)

Serial

  • 特點(diǎn):

    - 單線程、簡(jiǎn)單高效(與其他收集器的單線程相比),對(duì)于限定單個(gè)CPU的環(huán)境來(lái)說(shuō),Serial收集器由于沒(méi)有線程交互的開銷,專心做垃圾收集自然可以獲得最高的單線程收集效率。
    - 收集器進(jìn)行垃圾回收時(shí),必須暫停其他所有的工作線程,直到它結(jié)束(Stop The World)。


  • 應(yīng)用場(chǎng)景:

    適用于Client模式下的虛擬機(jī)


怎樣解析JVM虛擬機(jī)

ParNew

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

除了使用多線程外其余行為均和Serial收集器一模一樣(參數(shù)控制、收集算法、Stop The World、對(duì)象分配規(guī)則、回收策略等)

  • 特點(diǎn):

    - 多線程、ParNew收集器默認(rèn)開啟的收集線程數(shù)與CPU的數(shù)量相同,在CPU非常多的環(huán)境中,可以使用-XX:ParallelGCThreads參數(shù)來(lái)限制垃圾收集的線程數(shù)。
    - 與Serial收集器一樣存在Stop The World問(wèn)題


  • 應(yīng)用場(chǎng)景:

    ParNew收集器是許多運(yùn)行在Server模式下的虛擬機(jī)中首選的新生代收集器,因?yàn)樗浅薙erial收集器外,唯一一個(gè)能與CMS收集器配合工作的。


Parallel Scavenge

與吞吐量關(guān)系密切,故也稱為吞吐量?jī)?yōu)先收集器。 除了使用多線程外其余行為均和Serial收集器一模一樣(參數(shù)控制、收集算法、Stop The World、對(duì)象分配規(guī)則、回收策略等)

  • 特點(diǎn):

    屬于新生代收集器也是采用復(fù)制算法的收集器,又是并行的多線程收集器(與ParNew收集器類似)。

該收集器的目標(biāo)是達(dá)到一個(gè)可控制的吞吐量。還有一個(gè)值得關(guān)注的點(diǎn)是:GC自適應(yīng)調(diào)節(jié)策略(與ParNew收集器最重要的一個(gè)區(qū)別)

  • GC自適應(yīng)調(diào)節(jié)策略:

    Parallel Scavenge收集器可設(shè)置-XX:+UseAdptiveSizePolicy參數(shù)。
    當(dāng)開關(guān)打開時(shí)不需要手動(dòng)指定新生代的大小(-Xmn)、Eden與Survivor區(qū)的比例(-XX:SurvivorRation)、晉升老年代的對(duì)象年齡(-XX:PretenureSizeThreshold)等。
    虛擬機(jī)會(huì)根據(jù)系統(tǒng)的運(yùn)行狀況收集性能監(jiān)控信息,動(dòng)態(tài)設(shè)置這些參數(shù)以提供最優(yōu)的停頓時(shí)間和最高的吞吐量,這種調(diào)節(jié)方式稱為GC的自適應(yīng)調(diào)節(jié)策略。
    
    
    Parallel Scavenge收集器使用兩個(gè)參數(shù)控制吞吐量:
         XX:MaxGCPauseMillis 控制最大的垃圾收集停頓時(shí)間
         XX:GCRatio 直接設(shè)置吞吐量的大小。


Serial Old

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

  • 特點(diǎn):同樣是單線程收集器,采用標(biāo)記-整理算法。

  • 應(yīng)用場(chǎng)景:主要也是使用在Client模式下的虛擬機(jī)中。也可在Server模式下使用。

Server模式下主要的兩大用途

  1.在JDK1.5以及以前的版本中與Parallel Scavenge收集器搭配使用。
  2.作為CMS收集器的后備方案,在并發(fā)收集Concurent Mode Failure時(shí)使用。

怎樣解析JVM虛擬機(jī)

CMS

一種以獲取最短回收停頓時(shí)間為目標(biāo)的收集器。

  • 特點(diǎn):基于標(biāo)記-清除算法實(shí)現(xiàn)。并發(fā)收集、低停頓。

  • 應(yīng)用場(chǎng)景:

適用于注重服務(wù)的響應(yīng)速度,希望系統(tǒng)停頓時(shí)間最短,給用戶帶來(lái)更好的體驗(yàn)等場(chǎng)景下。如web程序、b/s服務(wù)。

  • CMS收集器的運(yùn)行過(guò)程分為下列4步:

    初始標(biāo)記:標(biāo)記GC Roots能直接到的對(duì)象。速度很快但是仍存在Stop The World問(wèn)題。
    并發(fā)標(biāo)記:進(jìn)行GC Roots Tracing 的過(guò)程,找出存活對(duì)象且用戶線程可并發(fā)執(zhí)行。
    重新標(biāo)記:為了修正并發(fā)標(biāo)記期間因用戶程序繼續(xù)運(yùn)行而導(dǎo)致標(biāo)記產(chǎn)生變動(dòng)的那一部分對(duì)象的標(biāo)記記錄。仍然存在Stop The World問(wèn)題。
    并發(fā)清除:對(duì)標(biāo)記的對(duì)象進(jìn)行清除回收。


CMS收集器的內(nèi)存回收過(guò)程是與用戶線程一起并發(fā)執(zhí)行的。

CMS收集器的缺點(diǎn):

  1. 對(duì)CPU資源非常敏感。

  2. 無(wú)法處理浮動(dòng)垃圾,可能出現(xiàn)Concurrent Model Failure失敗而導(dǎo)致另一次Full GC的產(chǎn)生。

  3. 因?yàn)椴捎脴?biāo)記-清除算法所以會(huì)存在空間碎片的問(wèn)題,導(dǎo)致大對(duì)象無(wú)法分配空間,不得不提前觸發(fā)一次Full GC。

怎樣解析JVM虛擬機(jī)

G1

一款面向服務(wù)端應(yīng)用的垃圾收集器。不再是將整個(gè)內(nèi)存區(qū)域按代整體劃分,他根據(jù),將每一個(gè)內(nèi)存單元獨(dú)立為Region區(qū),每個(gè)Region還是按代劃分。 如下圖:

怎樣解析JVM虛擬機(jī)

  • 特點(diǎn):

    - 并行與并發(fā):G1能充分利用多CPU、多核環(huán)境下的硬件優(yōu)勢(shì),使用多個(gè)CPU來(lái)縮短Stop-The-World停頓時(shí)間。
    部分收集器原本需要停頓Java線程來(lái)執(zhí)行GC動(dòng)作,G1收集器仍然可以通過(guò)并發(fā)的方式讓Java程序繼續(xù)運(yùn)行。
    
    - 分代收集:G1能夠獨(dú)自管理整個(gè)Java堆,并且采用不同的方式去處理新創(chuàng)建的對(duì)象和已經(jīng)存活了一段時(shí)間、熬過(guò)多次GC的舊對(duì)象以獲取更好的收集效果。
    
    - 空間整合:G1運(yùn)作期間不會(huì)產(chǎn)生空間碎片,收集后能提供規(guī)整的可用內(nèi)存。
    
    - 可預(yù)測(cè)的停頓:G1除了追求低停頓外,還能建立可預(yù)測(cè)的停頓時(shí)間模型。能讓使用者明確指定在一個(gè)長(zhǎng)度為M毫秒的時(shí)間段內(nèi),消耗在垃圾收集上的時(shí)間不得超過(guò)N毫秒。


G1為什么能建立可預(yù)測(cè)的停頓時(shí)間模型?

因?yàn)樗杏?jì)劃的避免在整個(gè)Java堆中進(jìn)行全區(qū)域的垃圾收集。G1跟蹤各個(gè)Region里面的垃圾堆積的大小,在后臺(tái)維護(hù)一個(gè)優(yōu)先列表,每次根據(jù)允許的收集時(shí)間,優(yōu)先回收價(jià)值最大的Region。這樣就保證了在有限的時(shí)間內(nèi)可以獲取盡可能高的收集效率。

G1與其他收集器的區(qū)別:

其他收集器的工作范圍是整個(gè)新生代或者老年代、G1收集器的工作范圍是整個(gè)Java堆。在使用G1收集器時(shí),它將整個(gè)Java堆劃分為多個(gè)大小相等的獨(dú)立區(qū)域(Region)。雖然也保留了新生代、老年代的概念,但新生代和老年代不再是相互隔離的,他們都是一部分Region(不需要連續(xù))的集合。

G1收集器存在的問(wèn)題:

Region不可能是孤立的,分配在Region中的對(duì)象可以與Java堆中的任意對(duì)象發(fā)生引用關(guān)系。在采用可達(dá)性分析算法來(lái)判斷對(duì)象是否存活時(shí),得掃描整個(gè)Java堆才能保證準(zhǔn)確性。其他收集器也存在這種問(wèn)題(G1更加突出而已)。會(huì)導(dǎo)致Minor GC效率下降。

G1收集器是如何解決上述問(wèn)題的?

采用Remembered Set來(lái)避免整堆掃描。G1中每個(gè)Region都有一個(gè)與之對(duì)應(yīng)的Remembered Set,虛擬機(jī)發(fā)現(xiàn)程序在對(duì)Reference類型進(jìn)行寫操作時(shí),會(huì)產(chǎn)生一個(gè)Write Barrier暫時(shí)中斷寫操作,檢查Reference引用對(duì)象是否處于多個(gè)Region中(即檢查老年代中是否引用了新生代中的對(duì)象),如果是,便通過(guò)CardTable把相關(guān)引用信息記錄到被引用對(duì)象所屬的Region的Remembered Set中。當(dāng)進(jìn)行內(nèi)存回收時(shí),在GC根節(jié)點(diǎn)的枚舉范圍中加入Remembered Set即可保證不對(duì)全堆進(jìn)行掃描也不會(huì)有遺漏。

如果不計(jì)算維護(hù) Remembered Set 的操作,G1收集器大致可分為如下步驟:

  - 初始標(biāo)記:僅標(biāo)記GC Roots能直接到的對(duì)象,并且修改TAMS(Next Top at Mark Start)的值,讓下一階段用戶程序并發(fā)運(yùn)行時(shí),能在正確可用的Region中創(chuàng)建新對(duì)象。(需要線程停頓,但耗時(shí)很短。)

  - 并發(fā)標(biāo)記:從GC Roots開始對(duì)堆中對(duì)象進(jìn)行可達(dá)性分析,找出存活對(duì)象。(耗時(shí)較長(zhǎng),但可與用戶程序并發(fā)執(zhí)行)

  - 最終標(biāo)記:為了修正在并發(fā)標(biāo)記期間因用戶程序執(zhí)行而導(dǎo)致標(biāo)記產(chǎn)生變化的那一部分標(biāo)記記錄。且對(duì)象的變化記錄在線程Remembered Set  Logs里面,把Remembered Set  Logs里面的數(shù)據(jù)合并到Remembered Set中。(需要線程停頓,但可并行執(zhí)行。)

  - 篩選回收:對(duì)各個(gè)Region的回收價(jià)值和成本進(jìn)行排序,根據(jù)用戶所期望的GC停頓時(shí)間來(lái)制定回收計(jì)劃。(可并發(fā)執(zhí)行)

怎樣解析JVM虛擬機(jī)

如何確定某個(gè)對(duì)象是垃圾?

上邊詳細(xì)說(shuō)了垃圾收集相關(guān)的內(nèi)容,那有很重要的一點(diǎn)沒(méi)有說(shuō),就是如何確定某個(gè)對(duì)象是垃圾對(duì)象,可被回收呢? 有下邊兩種方式,虛擬機(jī)中使用的是可達(dá)性分析算法。

引用計(jì)數(shù)法

給對(duì)象添加一個(gè)引用計(jì)數(shù)器,每當(dāng)有一個(gè)地方引用他的時(shí)候,計(jì)數(shù)器的數(shù)值就+1,當(dāng)引用失效時(shí),計(jì)數(shù)器就-1。

任何時(shí)候計(jì)數(shù)器的數(shù)值都為0的對(duì)象時(shí)不可能再被使用的。

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

以GC Roots的對(duì)象作為起始點(diǎn),從這些起始點(diǎn)開始向下搜索,搜索所搜過(guò)的路徑稱為引用鏈Reference Chain,當(dāng)一個(gè)對(duì)象到GC Roots沒(méi)有任何引用鏈相連接時(shí),則證明此對(duì)象時(shí)不可用的。

什么是GC Roots?

在虛擬機(jī)中可作為GC Roots的對(duì)象有以下幾種:

  • 虛擬機(jī)棧中引用的對(duì)象

  • 方法區(qū)中類靜態(tài)屬性引用的對(duì)象

  • 方法區(qū)常量引用的對(duì)象

  • 本地方法棧引用的對(duì)象

匯編指令

匯編指令是指可被虛擬機(jī)識(shí)別指令,我們平時(shí)看到的.class字節(jié)碼文件中就存放著我們某個(gè)類的匯編指令,通過(guò)了解匯編指令,可以幫助我們更深入了解虛擬機(jī)的工作機(jī)制與內(nèi)存分配方式。

使用javap查看到指令集

javap是jdk自帶的反解析工具。它的作用就是根據(jù)class字節(jié)碼文件,反解析出當(dāng)前類對(duì)應(yīng)的code區(qū)(匯編指令)、本地變量表、異常表和代碼行偏移量映射表、常量池等等信息。

當(dāng)然這些信息中,有些信息(如本地變量表、指令和代碼行偏移量映射表、常量池中方法的參數(shù)名稱等等)需要在使用javac編譯成class文件時(shí),指定參數(shù)才能輸出,比如,你直接javac xx.java,就不會(huì)在生成對(duì)應(yīng)的局部變量表等信息,如果你使用javac -g xx.java就可以生成所有相關(guān)信息了。

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

用法與參數(shù):

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

一般常用的是-v -l -c三個(gè)選項(xiàng)。

下面通過(guò)一個(gè)簡(jiǎn)單例子說(shuō)明一下匯編指令,具體說(shuō)明會(huì)以注釋形式說(shuō)明。

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

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
  //編譯版本號(hào)(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;
{
  //默認(rèn)構(gòu)造方法
  public com.freecloud.javabasics.javap.StringJavap();
   //輸入?yún)?shù)(該處表示無(wú)參)
    descriptor: ()V
    flags: ACC_PUBLIC
  //指令代碼《也是執(zhí)行代碼,重點(diǎn)關(guān)注》
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
	//指令與代碼中的行號(hào)關(guān)系
      LineNumberTable:
        line 7: 0
	//本地變量表
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/freecloud/javabasics/javap/StringJavap;
  // 對(duì)應(yīng)StringAndStringBuilder方法
  public void StringAndStringBuilder();
    descriptor: ()V
	//描述方法關(guān)鍵字
    flags: ACC_PUBLIC
    Code:
	  //stack()  locals(本地變量數(shù)/方法內(nèi)使用的變量數(shù)) args_size(入?yún)?shù),所有方法都有一個(gè)this所以參數(shù)至少為1)
      stack=3, locals=3, args_size=1
	     //通過(guò)#2可在常量池中找到111222字符串,表示在編譯時(shí)就把原本的"111" + "222"合并為一個(gè)常量
         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
		//返回指針,無(wú)論方法是否有返回值,都會(huì)有該指令,作用是出棧
        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"

可以在指令集中明確看到我們上邊講解的內(nèi)存運(yùn)行時(shí)數(shù)據(jù)區(qū)的一些影子。

比如常量池、本地變量表、虛擬機(jī)棧(每個(gè)方法可以理解為一個(gè)棧,具體方法內(nèi)就是Code區(qū))、返回地址(return)

以上就是怎樣解析JVM虛擬機(jī),小編相信有部分知識(shí)點(diǎn)可能是我們?nèi)粘9ぷ鲿?huì)見到或用到的。希望你能通過(guò)這篇文章學(xué)到更多知識(shí)。更多詳情敬請(qǐng)關(guān)注億速云行業(yè)資訊頻道。

向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

jvm
AI