【修煉內功】[JVM] 淺談虛擬機內存模型

本文已收錄 【修煉內功】躍遷之路

JVM

不論作技術仍是作業務,對於Java開發人員來說,理解JVM各類原理的重要性沒必要再多言java

對於C/C++而言,能夠輕易地操做任意地址的內存,而對於已申請內存數據的生命週期,又要擔負起維護的責任。不知各位在初學C語言時,是否經歷過因爲內存泄漏致使系統內存不足,又或者由於誤操做系統關鍵內存致使強制關機……segmentfault

對於Java使用者來講,內存由虛擬機直接管理,不容易出現內存泄漏或內存溢出等問題,將開發人員解放出來,使得更多的精力能夠用於具體實現上。也正是所以,一旦出現內存泄漏或溢出問題,若是不瞭解JVM的內存管理原理,那麼將會對問題的排查帶來極大的困難。數組

JVM在執行Java程序的過程當中,會將所管理的內存劃分爲不一樣的區域,這些區域各自都有本身的用途、可見性及生命週期,根據《Java虛擬機規範》的規定,JVM所管理的內存包含以下幾個區域jvm

jvm1

0x00 程序計數器

程序計數器是一個很小的內存區域,不在RAM上,而是直接劃分在CPU上,用於JVM在解釋執行字節碼時,存儲當前線程執行的字節碼行號,每條線程都擁有一個獨立的程序計數器,各條線程之間計數器互不影響,獨立存儲函數

字節碼解釋器工做時,就是經過改變程序計數器的值來選取下一條須要執行的字節碼指令,分支、循環、跳轉、異常等基礎功能都須要依賴計數器來完成測試

若是線程正在執行的是一個Java方法,則程序計數器記錄的是正在執行的虛擬機字節碼指令地址;若是執行的是native方法,則計數器的值爲空。此內存區是惟一一個在虛擬機規範中沒有規定任何OutOfMemoryError的區域優化

0x01 堆

Java堆,是平常工做中最常接觸的、也是虛擬機所管理的最大的一塊內存區域,其被全部線程共享,在虛擬機啓動時建立,此區域惟一的目的就是存放對象實例ui

《深刻理解Java虛擬機》

全部的對象實例以及數組都要在堆上分配,可是隨着JIT編譯器的發展及逃逸分析技術逐漸成熟,棧上分配、標量替換優化技術將會致使一些微妙的變化發生,全部的對象都分配在對上也逐漸變得不是那麼"絕對"了spa

從內存回收角度,Java堆分爲新生代和老年代,新生代又分爲E(den)空間和S(urvivor)0空間、S(urvivor)1空間操作系統

從內存分配角度,Java堆可能分爲多個線程私有的分配緩衝區

若是存在實例未完成堆內存分配,且堆沒法再擴展時(經過-Xmx及-Xms控制),將會拋出OutOfMemoryError異常

對於堆上各區域的分配、回收等細節,將在《[JVM] 虛擬機垃圾收集器》系列文章中詳述

Java堆溢出

只要不斷建立對象,而且保證GC Roots到對象之間有可達路徑來避免GC回收,那麼在對象數量達到堆的最大容量限制後就會產生內存溢出異常

/**
 * VM Args: -Xms5m -Xmx5m -XX:+HeapDumpOnOutOfMemoryError
 *
 * @author manerfan
 */
public class HeapOOM {
    static class OOMObject {
        private int i;
        private long l;
        private double d;
    }

    public static void main(String[] args) {
        List<OOMObject> list = new LinkedList<>();
        while (true) {
            list.add(new OOMObject());
        }
    }
}

指定堆大小固定爲5MB且不能擴展,運行結果

java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid71020.hprof ...
Heap dump file created [9186606 bytes in 0.069 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
    at HeapOOM.main(HeapOOM.java:19)

當Java堆內存溢出時,異常堆棧信息"java.lang.OutOfMemoryError"會跟着進一步提示"Java heap space"

對Dump出來的堆轉儲快照進行分析(如Eclipse Memory Analyzer),能夠確認內存中的對象是不是必要的,能夠清楚究竟是內存泄漏(Memory Leak)仍是內存溢出(Memory Overflow)

觀察堆使用狀況,以下圖

HeapOOm_x16

0x02 虛擬機棧

虛擬機棧也是線程私有的,它的生命週期與線程相同,每一個方法在執行的同時都會建立一個棧幀(Stack Frame)用於存儲局部變量表、操做數棧、動態連接、方法出口等信息,方法執行時棧幀入棧,方法結束時棧幀出棧

局部變量表存放編譯器可知的各類基本數據類型、對象引用及returnAddress類型,局部變量表所需的內存空間在編譯期間肯定,運行期間不會再改變,具體的分析會在《[JVM] 虛擬機棧及字節碼基礎》中介紹

虛擬機棧規定了兩種異常:若是線程請求的棧深度大於虛擬機容許的最大棧深度,則會拋出StackOverflow異常;若是虛擬機能夠動態擴展棧深度,在擴展時沒法申請足夠內存,則會拋出OutOfMemoryError異常

Java棧溢出

StackOverflow

可使用遞歸,無限增長棧的深度

/**
 * StackSOF
 *
 * @author Maner.Fan
 */
public class StackSOF {
    private int stackLen = 1;

    public void stackLeak() {
        stackLen++;
        stackLeak();
    }

    public static void main(String[] args) {
        StackSOF stackSOF = new StackSOF();
        try {
            stackSOF.stackLeak();
        } catch (Throwable e) {
            System.out.println("statck length: " + stackSOF.stackLen);
            throw e;
        }
    }
}

運行結果

statck length: 18455
Exception in thread "main" java.lang.StackOverflowError
    at StackSOF.stackLeak(StackSOF.java:13)
    at StackSOF.stackLeak(StackSOF.java:13)
    at StackSOF.stackLeak(StackSOF.java:13)
    at ...

OutOfMemoryError

對於棧空間的OutOfMemoryError,不管是減小最大堆容量、仍是減小最大棧容量、仍是增長局部變量大小、仍是無限建立線程,都沒有模擬出棧空間的OutOfMemoryError,卻是在堆空間比較小的時候會產生java.lang.OutOfMemoryError: Java heap space堆異常

環境

java version "1.8.0_212"
Java(TM) SE Runtime Environment (build 1.8.0_212-b10)
Java HotSpot(TM) 64-Bit Server VM (build 25.212-b10, mixed mode)

macOS Mojave 10.14.4
2.2GHz Intel Core i7
16GB 1600 MHZ DDR3

思路

/**
 * VM Args: -Xms20M -Xmx20M -Xss512K
 *
 * @author Maner.Fan
 */
public class StackOOM {
    private void dontStop() {
        long l0 = 0L;
        long l1 = 1L;
        long l2 = 2L;
        long l3 = 3L;
        long l4 = 4L;
        long l5 = 5L;
        long l6 = 6L;
        long l7 = 7L;
        long l8 = 8L;
        long l9 = 9L;
        long l10 = 10L;
        long l11 = 11L;
        long l12 = 12L;
        long l13 = 13L;
        long l14 = 14L;
        long l15 = 15L;
        long l16 = 16L;
        long l17 = 17L;
        long l18 = 18L;
        long l19 = 19L;
        while(true) {}
    }

    public void stackLeak() {
        while (true) {
            new Thread(() -> dontStop()).start();
        }
    }

    public static void main(String[] args) {
        StackOOM stackOOM = new StackOOM();
        stackOOM.stackLeak();
    }
}

0x03 本地方法棧

本地方法棧與虛擬機棧的運行運行機制一致,用於存儲每一個Native方法的執行狀態,惟一區別在於虛擬機棧爲執行Java方法服務,而本地方法棧爲執行Native方法服務,不少虛擬機直接將本地方法棧與虛擬機棧合二爲一

同虛擬機棧同樣,本地方法棧也會拋出StackOverflow及OutOfMemoryError異常

0x04 方法區/元空間

Method Area

在Java7及其以前,虛擬機中存在一塊內存區域叫方法區(Method Area),一樣爲線程共享,其主要用於存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據,有時候會將該區域稱之爲永久代(Permanent Generation),但本質上二者並不等價

相對而言,GC行爲在這個區域是比較少出現的,但並不是數據進入了方法區就意味着"永久"存在,該區域的GC目標主要是針對常量池的回收及類型的卸載,但這個區域的回收成績比較難以使人滿意,尤爲是對類型的卸載

當方法區沒法知足內存分配需求時,將拋出OutofmemoryError異常

在Java7中,常量池已經從方法區移到了堆中,到了Java8及以後的版本,方法區已經被永久移除,取而代之的是元空間(Metaspace)

爲何要移除Method Area

This is part of the JRockit and Hotspot convergence effort. JRockit customers do.

一方面,移除方法區是爲了和JRockit進行融合;另外一方面,方法區大小受到-XX: PermSize -XX: MaxPermSize兩個參數的限制,而這兩個參數又受到JVM設定的內存大小限制,這就致使在使用過程當中可能出現方法區內存溢出的問題

Metaspace

Metaspace並不在虛擬機內存中,而是使用本地內存,所以Metaspace具體大小理論上取決於系統的可用內存,一樣也能夠經過參數進行配置(-XX:MetaspaceSize -XX:MaxMetaspaceSize)

固然,Metaspace也是有OutOfMemoryError風險的,可是因爲Metaspace使用本機內存,所以只要不要代碼裏面犯過低級的錯誤,OOM的機率基本是不存在的

Java元空間溢出

因爲Java8以後,方法區被永久移除,這裏咱們再也不測試方法區(永久代)的內存溢出

最簡單的模擬Metaspace內存溢出,咱們只須要無限生成類信息便可,類佔據的空間老是會超過Metaspace指定的空間大小的,這裏藉助Cglib來模擬類的不斷加載

/**
 * VM Args: -XX:MetaspaceSize=8M -XX:MaxMetaspaceSize=16M
 *
 * @author Maner.Fan
 */
public class MetaspaceOOM {
    public static void main(String[] args) throws InterruptedException {
        System.out.println("MetaspaceOOM.java");
        while (true) {
            Enhancer enhancer = new Enhancer();
            enhancer.setSuperclass(OOMObject.class);
            enhancer.setUseCache(false);
            enhancer.setCallback(
                (MethodInterceptor)(obj, method, args1, methodProxy) -> methodProxy.invokeSuper(obj, args1)
            );
            enhancer.create();
        }
    }

    static class OOMObject {}
}

運行結果

Exception in thread "main" java.lang.OutOfMemoryError: Metaspace
    at net.sf.cglib.core.AbstractClassGenerator.generate(AbstractClassGenerator.java:348)
    at net.sf.cglib.proxy.Enhancer.generate(Enhancer.java:492)
    at net.sf.cglib.core.AbstractClassGenerator$ClassLoaderData.get(AbstractClassGenerator.java:117)
    at net.sf.cglib.core.AbstractClassGenerator.create(AbstractClassGenerator.java:294)
    at net.sf.cglib.proxy.Enhancer.createHelper(Enhancer.java:480)
    at net.sf.cglib.proxy.Enhancer.create(Enhancer.java:305)
    at MetaspaceOOM.main(MetaspaceOOM.java:19)

當Java元空間內存溢出時,異常堆棧信息"java.lang.OutOfMemoryError"會跟着進一步提示"Metaspace"

觀察元空間使用狀況,以下圖

MatespaceOOM_x4

0x05 直接內存

直接內存並非虛擬機運行時數據區的一部分,最典型的示例即是NIO,其引入了一種基於通道(Channel)與緩衝區(Buffer)的I/O方式,使用Native函數庫直接分配堆外內存,經過一個存儲在隊中的DirectByteBuffer對象做爲這塊內存的引用進行操做

直接內存的分配不會受到Java堆大小的限制,但會受到本機總內存大小及尋址空間的限制,一旦本機內存不足以分配堆外內存時,一樣會拋出OutOfMemoryError異常

0x06 對象的訪問定位

對象的建立是爲了使用,Java程序執行時須要經過棧上的reference數據來找到堆上的具體對象數據進行操做,目前主流的訪問方式有兩種:句柄訪問、直接指針訪問

句柄訪問

Java堆中將分配一塊內存做爲句柄池,棧中的reference存儲對象實例句柄的地址

句柄包含兩個指針,一個指針記錄對象實例的內存地址,另外一個記錄對象類型數據的地址

使用句柄的方式訪問對象數據,須要進行兩次指針定位,但其優勢在於,在GC過程當中對象被移動時,只須要修改句柄中對象實例數據指針便可

jvm2

直接指針訪問

棧中reference直接存儲堆中對象實例數據的內存地址,而對象類型數據的地址存放在對象實例數據中

使用直接指針訪問的好處在於訪問速度快,其只須要一次指針定位,但在GC過程當中對象被移動時,須要將全部指向該對象實例的reference值修改成移動後的內存地址

jvm3


參考:
深刻理解Java虛擬機

訂閱號

相關文章
相關標籤/搜索