JVM系列(二) - JVM內存區域

前言

JVM內存區域包括PC計數器Java虛擬機棧本地方法棧方法區運行時常量池直接內存java

本文主要介紹各個內存區域的做用和特性,同時分別闡述各個區域發生內存溢出的可能性和異常類型。編程

正文

(一). JVM內存區域

Java虛擬機執行Java程序的過程當中,會把所管理的內存劃分爲若干不一樣的數據區域。這些內存區域各有各的用途,以及建立和銷燬時間。有的區域隨着虛擬機進程的啓動而存在,有的區域伴隨着用戶線程的啓動和結束而建立和銷燬。後端

JVM內存區域也稱爲Java運行時數據區域。其中包括:程序計數器虛擬機棧本地方法棧靜態方法區靜態常量池等。數組

注意:程序計數器、虛擬機棧、本地方法棧屬於每一個線程私有的;堆和方法區屬於線程共享訪問的緩存

1.1. PC計數器

程序計數器(Program Counter Register)是一塊較小的內存空間,它的做用能夠看作是當前線程所執行的字節碼行號指示器bash

  1. 當前線程所執行的字節碼行號指示器
  2. 每一個線程都有一個本身的PC計數器。
  3. 線程私有的,生命週期與線程相同,隨JVM啓動而生,JVM關閉而死。
  4. 線程執行Java方法時,記錄其正在執行的虛擬機字節碼指令地址
  5. 線程執行Native方法時,計數器記錄爲(Undefined)。
  6. 惟一在Java虛擬機規範中沒有規定任何OutOfMemoryError狀況區域。

1.2. Java虛擬機棧

線程私有內存空間,它的生命週期和線程相同。線程執行期間,每一個方法執行時都會建立一個棧幀(Stack Frame) ,用於存儲 局部變量表操做數棧動態連接方法出口 等信息。多線程

  1. 局部變量表
  2. 操做數棧
  3. 動態連接
  4. 方法出口

每個方法從調用直到執行完成的過程,就對應着一個棧幀在虛擬機棧中的入棧出棧的全過程。架構

下面依次解釋棧幀裏的四種組成元素的具體結構和功能:框架

1). 局部變量表

局部變量表是一組變量值的存儲空間,用於存儲方法參數局部變量。 在 Class 文件的方法表的 Code 屬性的 max_locals 指定了該方法所需局部變量表的最大容量異步

局部變量表在編譯期間分配內存空間,能夠存放編譯期的各類變量類型:

  1. 基本數據類型boolean, byte, char, short, int, float, long, double8種;
  2. 對象引用類型reference,指向對象起始地址引用指針
  3. 返回地址類型returnAddress,返回地址的類型。

變量槽(Variable Slot):

變量槽局部變量表最小單位,規定大小爲32位。對於64位的longdouble變量而言,虛擬機會爲其分配兩個連續Slot空間。

2). 操做數棧

操做數棧Operand Stack)也常稱爲操做棧,是一個後入先出棧。在 Class 文件的 Code 屬性的 max_stacks 指定了執行過程當中最大的棧深度。Java虛擬機的解釋執行引擎被稱爲基於棧的執行引擎 ,其中所指的就是指-操做數棧

  1. 局部變量表同樣,操做數棧也是一個以32字長爲單位的數組。
  2. 虛擬機在操做數棧中可存儲的數據類型intlongfloatdoublereferencereturnType等類型 (對於byteshort以及char類型的值在壓入到操做數棧以前,也會被轉換爲int)。
  3. 局部變量表不一樣的是,它不是經過索引來訪問,而是經過標準的棧操做壓棧出棧來訪問。好比,若是某個指令把一個值壓入到操做數棧中,稍後另外一個指令就能夠彈出這個值來使用。

虛擬機把操做數棧做爲它的工做區——大多數指令都要從這裏彈出數據,執行運算,而後把結果壓回操做數棧

begin
iload_0    // push the int in local variable 0 onto the stack
iload_1    // push the int in local variable 1 onto the stack
iadd       // pop two ints, add them, push result
istore_2   // pop int, store into local variable 2
end
複製代碼

在這個字節碼序列裏,前兩個指令 iload_0iload_1 將存儲在局部變量表中索引爲01的整數壓入操做數棧中,其後iadd指令從操做數棧中彈出那兩個整數相加,再將結果壓入操做數棧。第四條指令istore_2則從操做數棧中彈出結果,並把它存儲到局部變量表索引爲2的位置。

下圖詳細表述了這個過程當中局部變量表操做數棧的狀態變化(圖中沒有使用的局部變量表操做數棧區域以空白表示)。

3). 動態連接

每一個棧幀都包含一個指向運行時常量池中所屬的方法引用,持有這個引用是爲了支持方法調用過程當中的動態連接

Class文件的常量池中存在有大量的符號引用,字節碼中的方法調用指令就以常量池中指向方法的符號引用爲參數。這些符號引用:

  1. 靜態解析:一部分會在類加載階段或第一次使用的時候轉化爲直接引用(如finalstatic域等),稱爲靜態解析
  2. 動態解析:另外一部分將在每一次的運行期間轉化爲直接引用,稱爲動態連接
4). 方法返回地址

當一個方法開始執行之後,只有兩種方法能夠退出當前方法:

  1. 正常返回:當執行遇到返回指令,會將返回值傳遞給上層的方法調用者,這種退出的方式稱爲正常完成出口(Normal Method Invocation Completion),通常來講,調用者的PC計數器能夠做爲返回地址。
  2. 異常返回:當執行遇到異常,而且當前方法體內沒有獲得處理,就會致使方法退出,此時是沒有返回值的,稱爲異常完成出口(Abrupt Method Invocation Completion),返回地址要經過異常處理器表來肯定。

當一個方法返回時,可能依次進行如下3個操做:

  1. 恢復上層方法局部變量表操做數棧
  2. 返回值壓入調用者棧幀操做數棧
  3. PC計數器的值指向下一條方法指令位置。

小結:

注意:在Java虛擬機規範中,對這個區域規定了兩種異常。 其一:若是當前線程請求的棧深度大於虛擬機棧所容許的深度,將會拋出 StackOverflowError 異常(在虛擬機棧不容許動態擴展的狀況下);其二:若是擴展時沒法申請到足夠的內存空間,就會拋出 OutOfMemoryError 異常。

1.3. 本地方法棧

本地方法棧Java虛擬機棧發揮的做用很是類似,主要區別是Java虛擬機棧執行的是Java方法服務,而本地方法棧執行Native方法服務(一般用C編寫)。

有些虛擬機發行版本(譬如Sun HotSpot虛擬機)直接將本地方法棧Java虛擬機棧合二爲一。與虛擬機棧同樣,本地方法棧也會拋出StackOverflowErrorOutOfMemoryError異常。

1.4. 堆

Java堆是被全部線程共享最大的一塊內存區域,在虛擬機啓動時建立。此內存區域的惟一目的就是存放對象實例,幾乎全部的對象實例都在這裏分配內存。

Java中,堆被劃分紅兩個不一樣的區域:新生代 (Young Generation) 、老年代 (Old Generation) 。新生代 (Young) 又被劃分爲三個區域:一個Eden區和兩個Survivor區 - From Survivor區和To Survivor區。

簡要概括:新的對象分配是首先放在年輕代 (Young Generation) 的Eden區,Survivor區做爲Eden區和Old區的緩衝,在Survivor區的對象經歷若干次收集仍然存活的,就會被轉移到老年代Old中。

這樣劃分的目的是爲了使JVM可以更好的管理堆內存中的對象,包括內存的分配以及回收。

1.5. 方法區

方法區和Java堆同樣,爲多個線程共享,它用於存儲類信息常量靜態常量即時編譯後的代碼等數據。

1.6. 運行時常量池

運行時常量池是方法區的一部分,Class文件中除了有類的版本字段方法接口等描述信息外, 還有一類信息是常量池,用於存儲編譯期間生成的各類字面量符號引用

1.7. 直接內存

直接內存不屬於虛擬機運行時數據區的一部分,也不是Java虛擬機規範中定義的內存區域。 Java NIO容許Java程序直接訪問直接內存,一般直接內存的速度會優於Java堆內存。所以,對於讀寫頻繁、性能要求高的場景,能夠考慮使用直接內存。

(二). 常見內存溢出異常

除了程序計數器外,Java虛擬機的其餘運行時區域都有可能發生OutOfMemoryError的異常,下面分別給出驗證:

2.1. Java堆溢出

Java堆可以存儲對象實例。經過不斷地建立對象,並保證GC Roots到對象有可達路徑來避免垃圾回收機制清除這些對象。 當對象數量到達最大堆的容量限制時就會產生OutOfMemoryError異常。

設置JVM啓動參數:-Xms20M設置堆的最小內存20M-Xmx20M設置堆的最大內存最小內存同樣,這樣能夠防止Java堆在內存不足時自動擴容-XX:+HeapDumpOnOutOfMemoryError參數可讓虛擬機在出現內存溢出異常時Dump內存堆運行時快照。

HeapOOM.java

/** * VM Args: -Xms20M -Xmx20M -XX:+HeapDumpOnOutOfMemoryError */
public class HeapOOM {
    public static class OOMObject {
    }

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

測試運行結果:

打開Java VisualVM導出Heap內存運行時的dump文件。

HeapOOM對象不停地被建立,堆內存使用達到 99%垃圾回收器不斷地嘗試回收但都以失敗了結。

分析:遇到這種狀況,一般要考慮內存泄露內存溢出兩種可能性。

  • 若是是內存泄露:

進一步使用Java VisualVM工具進行分析,查看泄露對象是經過怎樣的路徑GC Roots關聯而致使垃圾回收器沒法回收的。

  • 若是是內存溢出:

經過Java VisualVM工具分析,不存在泄露對象,也就是說堆內存中的對象必須得存活着。就要考慮以下措施:

  1. 從代碼上檢查是否存在某些對象生命週期過長持續狀態時間過長的狀況,嘗試減小程序運行期的內存。
  2. 檢查虛擬機的堆參數(-Xmx-Xms),對比機器的物理內存看是否還能夠調大。

2.2. 虛擬機和本地方法棧溢出

關於虛擬機棧和本地方法棧,分析內存異常類型可能存在如下兩種:

  • 若是現場請求的棧深度大於虛擬機所容許的最大深度,將拋出StackOverflowError異常。
  • 若是虛擬機在擴展棧時沒法申請到足夠的內存空間,可能會拋出OutOfMemoryError異常。

能夠劃分爲兩類問題,當棧空間沒法分配時,到底時棧內存過小,仍是已使用的棧內存過大

StackOverflowError異常

測試方案一:

  • 使用-Xss參數減小棧內存的容量,異常發生時打印的深度。
  • 定義大量的本地局部變量,以達到增大棧幀中的本地變量表的長度。

設置JVM啓動參數:-Xss128k設置棧內存的大小爲128k

JavaVMStackSOF.java

/** * VM Args: -Xss128k */
public class JavaVMStackSOF {
    private int stackLength = 1;

    private void stackLeak() {
        stackLength++;
        stackLeak();
    }

    public static void main(String[] args) {
        JavaVMStackSOF oom = new JavaVMStackSOF();
        try {
            oom.stackLeak();
        } catch (Throwable e) {
            System.out.println("Stack length: " + oom.stackLength);
            throw e;
        }
    }
}
複製代碼

測試結果:

分析:在單個線程下,不管是棧幀太大仍是虛擬機棧容量過小,當沒法分配內存的時候,虛擬機拋出的都是StackOverflowError異常。

測試方案二:

  • 不停地建立線程並保持線程運行狀態。

JavaVMStackOOM.java

/** * VM Args: -Xss2M */
public class JavaVMStackOOM {
    private void running() {
        while (true) {
        }
    }

    public void stackLeakByThread() {
        while (true) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    running();
                }
            }).start();
        }
    }

    public static void main(String[] args) {
        JavaVMStackOOM oom = new JavaVMStackOOM();
        oom.stackLeakByThread();
    }
}
複製代碼

測試結果:

Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread
複製代碼

上述測試代碼運行時存在較大的風險,可能會致使操做系統假死,這裏就不親自測試了,引用做者的測試結果。

2.3. 方法區和運行時常量池溢出

(一). 運行時常量池內存溢出測試

運行時常量字面量都存放於運行時常量池中,常量池又是方法區的一部分,所以兩個區域的測試是同樣的。 這裏採用String.intern()進行測試:

String.intern()是一個native方法,它的做用是:若是字符串常量池中存在一個String對象的字符串,那麼直接返回常量池中的這個String對象; 不然,將此String對象包含的字符串放入常量池中,而且返回這個String對象的引用。

設置JVM啓動參數:經過-XX:PermSize=10M-XX:MaxPermSize=10M限制方法區的大小爲10M,從而間接的限制其中常量池的容量。

RuntimeConstantPoolOOM.java

/** * VM Args: -XX:PermSize=10M -XX:MaxPermSize=10M */
public class RuntimeConstantPoolOOM {

    public static void main(String[] args) {
        // 使用List保持着常量池的引用,避免Full GC回收常量池
        List<String> list = new ArrayList<>();
        // 10MB的PermSize在Integer範圍內足夠產生OOM了
        int i = 0;
        while (true) {
            list.add(String.valueOf(i++).intern());
        }
    }
}
複製代碼

測試結果分析:

JDK1.6版本運行結果:

Exception in thread "main" java.lang.OutOfMemoryError: PermGen space
    at java.lang.String.intern(Native Method)
複製代碼

JDK1.6版本運行結果顯示常量池會溢出並拋出永久帶OutOfMemoryError異常。 而JDK1.7及以上的版本則不會獲得相同的結果,它會一直循環下去。

(二). 方法區內存溢出測試

方法區存放Class相關的信息,好比類名訪問修飾符常量池字段描述方法描述等。 對於方法區的內存溢出的測試,基本思路是在運行時產生大量類字節碼區填充方法區

這裏引入Spring框架的CGLib動態代理的字節碼技術,經過循環不斷生成新的代理類,達到方法區內存溢出的效果。

JavaMethodAreaOOM.java

/** * VM Args: -XX:PermSize=10M -XX:MaxPermSize=10M */
public class JavaMethodAreaOOM {

    public static void main(String[] args) {
        while (true) {
            Enhancer enhancer = new Enhancer();
            enhancer.setSuperclass(OOMObject.class);
            enhancer.setUseCache(false);
            enhancer.setCallback(new MethodInterceptor() {
                @Override
                public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
                    return proxy.invokeSuper(obj, args);
                }
            });

            enhancer.create();
        }
    }

    private static class OOMObject {
        public OOMObject() {
        }
    }
}
複製代碼

JDK1.6版本運行結果:

Exception in thread "main" java.lang.OutOfMemoryError: PermGen space
    at java.lang.ClassLoader.defineClass1(Native Method)
    at java.lang.ClassLoader.defineClassCond(ClassLoader.java:632)
    at java.lang.ClassLoader.defineClass(ClassLoader.java:616)
複製代碼

測試結果分析:

JDK1.6版本運行結果顯示常量池會溢出並拋出永久帶OutOfMemoryError異常。 而JDK1.7及以上的版本則不會獲得相同的結果,它會一直循環下去。

2.4. 直接內存溢出

本機直接內存的容量可經過-XX:MaxDirectMemorySize指定,若是不指定,則默認與Java最大值(-Xmx指定)同樣。

測試場景:

直接經過反射獲取Unsafe實例,經過反射向操做系統申請分配內存:

設置JVM啓動參數:-Xmx20M指定Java堆的最大內存,-XX:MaxDirectMemorySize=10M指定直接內存的大小。

DirectMemoryOOM.java

/** * VM Args: -Xmx20M -XX:MaxDirectMemorySize=10M */
public class DirectMemoryOOM {

    private static final int _1MB = 1024 * 1024;

    public static void main(String[] args) throws Exception {
        Field unsafeField = Unsafe.class.getDeclaredFields()[0];
        unsafeField.setAccessible(true);
        Unsafe unsafe = (Unsafe) unsafeField.get(null);
        while (true) {
            unsafe.allocateMemory(_1MB);
        }
    }
}
複製代碼

測試結果:

測試結果分析:

DirectMemory致使的內存溢出,一個明顯的特徵是Heap Dump文件中不會看到明顯的異常信息。 若是OOM發生後Dump文件很小,而且程序中直接或者間接地使用了NIO,那麼就能夠考慮一下這方面的問題。


歡迎關注技術公衆號: 零壹技術棧

零壹技術棧

本賬號將持續分享後端技術乾貨,包括虛擬機基礎,多線程編程,高性能框架,異步、緩存和消息中間件,分佈式和微服務,架構學習和進階等學習資料和文章。

相關文章
相關標籤/搜索