2K+超詳細文字配上詳圖帶你解析Java虛擬機各大運行時數據區域!!

1. 瓦解JVM的五大運行時數據區域

說在前面:JVM~JVM,一個強敵,衆多Java工程師的噩夢,衆多大神們的必修祕籍之一,今天我就要向大神們看齊,但願能撈到一點經驗。此文僅表明做者我的觀點,在文中不時引入其它參考書籍的資料並引入少部分我的觀點,若有嚴重錯誤,但願能堅決果斷地指出並狠狠地diss我!html

先放一張做者對虛擬機的「自畫像」,能夠看到在咱們某個Java程序運行的過程當中,在JVM中主要有以下的區域,跟着我一塊兒來一個個剝開這些區域的皮。下面這張圖你收好,若是對你有用,點贊是給予我最大的支持!java

JDK1.7運行時數據區

在上圖能夠看出,整個運行時數據區域分爲兩個部分:線程共享部分、線程私有部分數組

線程共享:顧名思義,就是全部線程都會享用這些空間,你用我也用、你們一塊兒用緩存

線程私有:每一個線程都會獨佔本身本應該擁有的區域,河水不犯井水測試

從簡單的開始看起~spa

線程私有部分

下面先對線程私有部分的區域做簡略的描述,讓咱們先對這三個區域有個大概的瞭解~線程

程序計數器:記錄某個線程下一步應該執行的字節碼指令設計

虛擬機棧:當方法被調用時,就會產生一個棧幀並放入虛擬機棧中,而後方法結束後,該棧幀就會彈出虛擬機棧。指針

本地方法棧:和虛擬機棧很是類似,只不過它只存放被調用的native方法,什麼是native方法?待會就知道了code

是否是感受一頭霧水~嗯...我一開始也是這樣的,別急,立刻開始他們的自傳秀

程序計數器

《深刻理解Java虛擬機》原文:程序計數器(Program Counter Register)是一塊較小的內存空間,它能夠看做是當前線程所執行的字節碼的行號指示器

這部份內容並無介紹太多,程序計數器就是根據程序邏輯指示下一條該執行的語句是什麼,程序的執行邏輯有順序執行、if語句、循環語句、方法調用·····它就是用來指明當前線程下一步該往哪一條語句執行。

虛擬機棧和本地方法棧

《深刻理解Java虛擬機》原文:Java虛擬機棧(Java Virtual Machine Stack)描述的是Java方法執行的內存模型:每一個方法被執行的時候,Java虛擬機棧會同步建立一個棧幀(Stack Frame)用於存儲局部變量表、操做數棧、動態鏈接、方法出口等信息。

局部變量表:存放八大基本數據類型(byte/short/int······)和對象引用指針(指向對象在堆內存中的起始地址),還有最後返回的returnAddress類型。returnAddress就是指向下一條應該執行的字節碼指令,如下面代碼爲例子解釋對其的理解。

public class Test{
    public void funcA(){
        System.out.println("success execute funcA");
    }
    public void funcB(){
        funcA();
        System.out.println("success execute funcB");
    }
    public static void main(String[] args){
        Test test = new Test();
        test.funcB();
    }
}
複製代碼

調用funcB時,方法funcB會使用returnAddress類型記錄調用funcB的字節碼指令位置,當funcB執行完畢時,就會調用returnAddress類型返回到調用funcB的字節碼指令的位置,繼續往下執行;一樣,當funcB調用funcA時,funcA內的returnAddress也會記錄調用者的字節碼指令位置,當funcA執行完後返回到funcB調用的位置,繼續往下執行。

本地方法棧:與Java虛擬機棧發揮的做用類似,兩者的區別在於執行的方法類別不一樣,本地方法棧專門爲調用本地方法服務。

什麼是本地方法?

本地方法是指由其餘語言(如C、C++ 或其餘彙編語言)編寫,編譯成和處理器相關的代碼。至於如何加載和運行本地方法的,這裏就再也不展開了,下面還有很長很長,繼續繼續~

線程共享部分

方法區/元空間(JDK8)

方法區用於存儲已被加載的類型信息、常量、靜態變量、即時編譯器編譯後的代碼緩存·····

方法區也會被別人稱做爲永久代,由於它內部也是採用分代收集的回收方式進行GC,而在方法區中垃圾收集行爲是比較少見的,這部分區域的垃圾回收主要是針對常量池的回收和對類型的卸載,通常來講這部分區域的回收效果很難使人滿意,由於知足垃圾收集的條件太苛刻了,因此處於方法區中的數據已經幾乎永久存在的了。

那爲何會把方法區變成元空間了呢?

  1. Oracle將HotSpot與JRockit虛擬機實現的方法區看齊
  2. GC的效率太低

堆內存是全部對象和數組的分配區域,這個區域是GC的主要區域

現代大部分虛擬機都是基於分代收集理論設計的,因此不少虛擬機實現的堆上都會有「新生代」、「老年代」、「Eden」、「To Survivor」、「from Survivor」等概念出現,實際上這不是堆固有的東西

如何加速對象的內存分配?

在HotSpot虛擬機實現中採用了TLAB(本地線程分配緩衝)能夠加速對象在堆上的內存分配效率,TLAB就是在堆上給每一個線程開闢一小段內存緩衝區,線程建立對象時直接在本身TLAB中分配對象,當TLAB用完後,再繼續向堆申請內存,申請內存的過程當中須要同步機制

邂逅內存溢出

堆內存溢出

/** * @author Zeng * @date 2020/4/5 15:34 * VM Args: -Xms20m -Xmx20m -XX:+HeapDumpOutOfMemoryError * -Xms:堆的最小內存容量 * -Xmx:堆的最大內存容量 * -XX:虛擬機啓動時指定的一些參數 */
public class HeapOOM {
    static class OOMObject{}
    public static void main(String[] args) {
        List<OOMObject> list = new ArrayList<>();
        while (true){
            list.add(new OOMObject());
        }
    }
}
複製代碼

堆內存溢出

當堆上的對象一直分配分配分配·····直到對象佔用的總內存大於堆最大可容納對象的內存就會發生內存溢出

虛擬機棧和本地方法棧內存溢出

首先測試將棧的容量減少,使用-Xss參數測試

/** * @author Zeng * @date 2020/4/5 15:47 * VM Args: -Xss128k * -Xss:棧的最小內存容量 * 虛擬機棧溢出 */
public class JavaVMStackSOF {
    private int stackLength = 1;
    public void stackLeak(){
        stackLength++;
        //無限月讀!!!!
        stackLeak();
    }
    public static void main(String[] args) {
        JavaVMStackSOF sof = new JavaVMStackSOF();
        try {
            sof.stackLeak();
        } catch (Exception e) {
            System.out.println("stack length:" + sof.stackLength);
            throw e;
        }
    }
}
複製代碼

虛擬機棧內存溢出

如上圖所示拋出了StackOverFlowError異常,棧幀不斷地申請虛擬機棧內的內存,當虛擬機棧沒有足夠的內存放入這個棧幀時,就會發生棧內存溢出,比較常見的就是死遞歸

而後測試一個棧幀的局部變量表內存大於虛擬機棧的內存容量,也就是說一個棧幀擠爆一個虛擬機棧,爽~

public class JavaVMStackSOF2 {
    private static int stackLength = 0;

    public static void test() {
    	long unused1, unused2···unused100 ;
        unused1, unused2···unused100 = 0;
        stackLength++;
        test();
    }
}
複製代碼

虛擬機棧內存溢出2

兩次測試能夠看到,不管是虛擬機棧內存過小,仍是棧幀太大,都會致使虛擬機棧的內存溢出。

運行時常量池溢出

從JDK1.6開始,JDK1.六、JDK1.7和JDK1.8三個版本的HotSpot虛擬機中的方法區(JDK1.8已被替代爲元空間)所包含的數據是不同的,下面逐一進行驗證,因爲筆者沒有JDK1.6和JDK1.7的版本,藉助網上其餘做者的例子展現:

import java.util.HashSet;
import java.util.Set;

/** * @author Zeng * @date 2020/4/5 16:05 * VM Args: -XX:PermSize=6M -XX:MaxPermSize=6M * PermSize: 方法區的最小容量 * MaxPermSize:方法區的最大容量 * 在JDK1.6之後沒法形成方法區內存溢出,由於常量池再也不處於方法區當中而在堆上分配 * -Xmx6M 指定堆的最大內存容量爲6M則會由於常量池因內存不足而拋出堆內存溢出 * */
public class RuntimeConstantPoolOOM {

    public void createString(){
        Set<String> set = new HashSet<String>();
        int i = 0;
        while (true){
            set.add(String.valueOf(i++).intern());
        }
    }

    public static void main(String[] args) {
        RuntimeConstantPoolOOM constantPoolOOM = new RuntimeConstantPoolOOM();
        constantPoolOOM.createString();
    }

}
複製代碼

JDK1.6運行結果:

JDK1.6

JDK1.7運行結果:

JDK1.7

JDK1.8運行結果:

JDK1.8

從上面三幅圖能夠看出,JDK1.6版本中,運行時常量池會致使PermGen space溢出,也就是永久代空間溢出;而在JDK1.7中能夠發現溢出的區域是而不是永久代了,因此能夠驗證從JDK1.7開始運行時常量池從方法區搬移至了。

參考資料:www.cnblogs.com/paddix/p/53…

結尾

若是這篇文章能給你帶來一點點幫助,但願可以獲得你的一個點贊,你的一個贊會讓我開心好久,會讓我更加努力的持續去輸出好文章分享給大家!感謝你的閱讀!

相關文章
相關標籤/搜索