溫馨提示×

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

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

如何理解JVM 內(nèi)存區(qū)域及內(nèi)存溢出分析

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

本篇文章給大家分享的是有關(guān)如何理解JVM 內(nèi)存區(qū)域及內(nèi)存溢出分析,小編覺(jué)得挺實(shí)用的,因此分享給大家學(xué)習(xí),希望大家閱讀完這篇文章后可以有所收獲,話不多說(shuō),跟著小編一起來(lái)看看吧。

前言

在JVM的管控下,Java程序員不再需要管理內(nèi)存的分配與釋放,這和在C和C++的世界是完全不一樣的。所以,在JVM的幫助下,Java程序員很少會(huì)關(guān)注內(nèi)存泄露和內(nèi)存溢出的問(wèn)題。但是,一旦JVM發(fā)生這些情況的時(shí)候,如果你不清楚JVM內(nèi)存的內(nèi)存管理機(jī)制是很難定位與解決問(wèn)題的。

一、JVM 內(nèi)存區(qū)域

Java虛擬機(jī)在運(yùn)行時(shí),會(huì)把內(nèi)存空間分為若干個(gè)區(qū)域,根據(jù)《Java虛擬機(jī)規(guī)范(Java SE 7 版)》的規(guī)定,Java虛擬機(jī)所管理的內(nèi)存區(qū)域分為如下部分:方法區(qū)、堆內(nèi)存、虛擬機(jī)棧、本地方法棧、程序計(jì)數(shù)器。

如何理解JVM 內(nèi)存區(qū)域及內(nèi)存溢出分析

1、方法區(qū)

方法區(qū)主要用于存儲(chǔ)虛擬機(jī)加載的類信息、常量、靜態(tài)變量,以及編譯器編譯后的代碼等數(shù)據(jù)。在jdk1.7及其之前,方法區(qū)是堆的一個(gè)“邏輯部分”(一片連續(xù)的堆空間),但為了與堆做區(qū)分,方法區(qū)還有個(gè)名字叫“非堆”,也有人用“永久代”(HotSpot對(duì)方法區(qū)的實(shí)現(xiàn)方法)來(lái)表示方法區(qū)。

從jdk1.7已經(jīng)開始準(zhǔn)備“去永久代”的規(guī)劃,jdk1.7的HotSpot中,已經(jīng)把原本放在方法區(qū)中的靜態(tài)變量、字符串常量池等移到堆內(nèi)存中,(常量池除字符串常量池還有class常量池等),這里只是把字符串常量池移到堆內(nèi)存中;在jdk1.8中,方法區(qū)已經(jīng)不存在,原方法區(qū)中存儲(chǔ)的類信息、編譯后的代碼數(shù)據(jù)等已經(jīng)移動(dòng)到了元空間(MetaSpace)中,元空間并沒(méi)有處于堆內(nèi)存上,而是直接占用的本地內(nèi)存(NativeMemory)。根據(jù)網(wǎng)上的資料結(jié)合自己的理解對(duì)jdk1.3~1.6、jdk1.7、jdk1.8中方法區(qū)的變遷畫了張圖如下(如有不合理的地方希望讀者指出):

如何理解JVM 內(nèi)存區(qū)域及內(nèi)存溢出分析

去永久代的原因有:

(1)字符串存在永久代中,容易出現(xiàn)性能問(wèn)題和內(nèi)存溢出。

(2)類及方法的信息等比較難確定其大小,因此對(duì)于永久代的大小指定比較困難,太小容易出現(xiàn)永久代溢出,太大則容易導(dǎo)致老年代溢出。

(3)永久代會(huì)為 GC 帶來(lái)不必要的復(fù)雜度,并且回收效率偏低。

2、堆內(nèi)存

堆內(nèi)存主要用于存放對(duì)象和數(shù)組,它是JVM管理的內(nèi)存中最大的一塊區(qū)域,堆內(nèi)存和方法區(qū)都被所有線程共享,在虛擬機(jī)啟動(dòng)時(shí)創(chuàng)建。在垃圾收集的層面上來(lái)看,由于現(xiàn)在收集器基本上都采用分代收集算法,因此堆還可以分為新生代(YoungGeneration)和老年代(OldGeneration),新生代還可以分為 Eden、From Survivor、To Survivor。

3、程序計(jì)數(shù)器

程序計(jì)數(shù)器是一塊非常小的內(nèi)存空間,可以看做是當(dāng)前線程執(zhí)行字節(jié)碼的行號(hào)指示器,每個(gè)線程都有一個(gè)獨(dú)立的程序計(jì)數(shù)器,因此程序計(jì)數(shù)器是線程私有的一塊空間,此外,程序計(jì)數(shù)器是Java虛擬機(jī)規(guī)定的唯一不會(huì)發(fā)生內(nèi)存溢出的區(qū)域。

4、虛擬機(jī)棧

虛擬機(jī)棧也是每個(gè)線程私有的一塊內(nèi)存空間,它描述的是方法的內(nèi)存模型,直接看下圖所示:

如何理解JVM 內(nèi)存區(qū)域及內(nèi)存溢出分析

虛擬機(jī)會(huì)為每個(gè)線程分配一個(gè)虛擬機(jī)棧,每個(gè)虛擬機(jī)棧中都有若干個(gè)棧幀,每個(gè)棧幀中存儲(chǔ)了局部變量表、操作數(shù)棧、動(dòng)態(tài)鏈接、返回地址等。一個(gè)棧幀就對(duì)應(yīng) Java 代碼中的一個(gè)方法,當(dāng)線程執(zhí)行到一個(gè)方法時(shí),就代表這個(gè)方法對(duì)應(yīng)的棧幀已經(jīng)進(jìn)入虛擬機(jī)棧并且處于棧頂?shù)奈恢?,每一個(gè) Java 方法從被調(diào)用到執(zhí)行結(jié)束,就對(duì)應(yīng)了一個(gè)棧幀從入棧到出棧的過(guò)程。

5、本地方法棧

本地方法棧與虛擬機(jī)棧的區(qū)別是,虛擬機(jī)棧執(zhí)行的是 Java 方法,本地方法棧執(zhí)行的是本地方法(Native Method),其他基本上一致,在 HotSpot 中直接把本地方法棧和虛擬機(jī)棧合二為一,這里暫時(shí)不做過(guò)多敘述。

6、元空間

上面說(shuō)到,jdk1.8 中,已經(jīng)不存在永久代(方法區(qū)),替代它的一塊空間叫做 “ 元空間 ”,和永久代類似,都是 JVM 規(guī)范對(duì)方法區(qū)的實(shí)現(xiàn),但是元空間并不在虛擬機(jī)中,而是使用本地內(nèi)存,元空間的大小僅受本地內(nèi)存限制,但可以通過(guò) -XX:MetaspaceSize 和 -XX:MaxMetaspaceSize 來(lái)指定元空間的大小。

二、JVM 內(nèi)存溢出

1、堆內(nèi)存溢出

堆內(nèi)存中主要存放對(duì)象、數(shù)組等,只要不斷地創(chuàng)建這些對(duì)象,并且保證 GC Roots 到對(duì)象之間有可達(dá)路徑來(lái)避免垃圾收集回收機(jī)制清除這些對(duì)象,當(dāng)這些對(duì)象所占空間超過(guò)最大堆容量時(shí),就會(huì)產(chǎn)生 OutOfMemoryError 的異常。堆內(nèi)存異常示例如下:

/**
* 設(shè)置最大堆最小堆:-Xms20m -Xmx20m
* 運(yùn)行時(shí),不斷在堆中創(chuàng)建OOMObject類的實(shí)例對(duì)象,且while執(zhí)行結(jié)束之前,GC Roots(代碼中的oomObjectList)到對(duì)象(每一個(gè)OOMObject對(duì)象)之間有可達(dá)路徑,垃圾收集器就無(wú)法回收它們,最終導(dǎo)致內(nèi)存溢出。
*/
public class HeapOOM {
    static class OOMObject {
    }
    public static void main(String[] args) {
        List<OOMObject> oomObjectList = new ArrayList<>();
        while (true) {
            oomObjectList.add(new OOMObject());
        }
    }
}


運(yùn)行后會(huì)報(bào)異常,在堆棧信息中可以看到:

java.lang.OutOfMemoryError: Java heap space 的信息,說(shuō)明在堆內(nèi)存空間產(chǎn)生內(nèi)存溢出的異常。

新產(chǎn)生的對(duì)象最初分配在新生代,新生代滿后會(huì)進(jìn)行一次 Minor GC,如果 Minor GC 后空間不足會(huì)把該對(duì)象和新生代滿足條件的對(duì)象放入老年代,老年代空間不足時(shí)會(huì)進(jìn)行 Full GC,之后如果空間還不足以存放新對(duì)象則拋出 OutOfMemoryError 異常。

常見原因:內(nèi)存中加載的數(shù)據(jù)過(guò)多如一次從數(shù)據(jù)庫(kù)中取出過(guò)多數(shù)據(jù);集合對(duì)對(duì)象引用過(guò)多且使用完后沒(méi)有清空;代碼中存在死循環(huán)或循環(huán)產(chǎn)生過(guò)多重復(fù)對(duì)象;堆內(nèi)存分配不合理;網(wǎng)絡(luò)連接問(wèn)題、數(shù)據(jù)庫(kù)問(wèn)題等。

2、虛擬機(jī)棧/本地方法棧溢出

(1)StackOverflowError:當(dāng)線程請(qǐng)求的棧的深度大于虛擬機(jī)所允許的最大深度,則拋出StackOverflowError,簡(jiǎn)單理解就是虛擬機(jī)棧中的棧幀數(shù)量過(guò)多(一個(gè)線程嵌套調(diào)用的方法數(shù)量過(guò)多)時(shí),就會(huì)拋出StackOverflowError異常。

最常見的場(chǎng)景就是方法無(wú)限遞歸調(diào)用,如下:

/**
* 設(shè)置每個(gè)線程的棧大?。?Xss256k
* 運(yùn)行時(shí),不斷調(diào)用doSomething()方法,main線程不斷創(chuàng)建棧幀并入棧,導(dǎo)致棧的深度越來(lái)越大,最終導(dǎo)致棧溢出。
*/
public class StackSOF {
    private int stackLength=1;
    public void doSomething(){
            stackLength++;
            doSomething();
    }
    public static void main(String[] args) {
        StackSOF stackSOF=new StackSOF();
        try {
            stackSOF.doSomething();
        }catch (Throwable e){//注意捕獲的是Throwable
            System.out.println("棧深度:"+stackSOF.stackLength);
            throw e;
        }
    }
}


上述代碼執(zhí)行后拋出:

Exception in thread "Thread-0" java.lang.StackOverflowError 的異常。

(2)OutOfMemoryError:如果虛擬機(jī)在擴(kuò)展棧時(shí)無(wú)法申請(qǐng)到足夠的內(nèi)存空間,則拋出 OutOfMemoryError。

我們可以這樣理解,虛擬機(jī)中可以供棧占用的空間≈可用物理內(nèi)存 - 最大堆內(nèi)存 - 最大方法區(qū)內(nèi)存,比如一臺(tái)機(jī)器內(nèi)存為 4G,系統(tǒng)和其他應(yīng)用占用 2G,虛擬機(jī)可用的物理內(nèi)存為 2G,最大堆內(nèi)存為 1G,最大方法區(qū)內(nèi)存為 512M,那可供棧占有的內(nèi)存大約就是 512M,假如我們?cè)O(shè)置每個(gè)線程棧的大小為 1M,那虛擬機(jī)中最多可以創(chuàng)建 512個(gè)線程,超過(guò) 512個(gè)線程再創(chuàng)建就沒(méi)有空間可以給棧了,就報(bào) OutOfMemoryError 異常了。

如何理解JVM 內(nèi)存區(qū)域及內(nèi)存溢出分析

棧上能夠產(chǎn)生 OutOfMemoryError 的示例如下:

/**
* 設(shè)置每個(gè)線程的棧大小:-Xss2m
* 運(yùn)行時(shí),不斷創(chuàng)建新的線程(且每個(gè)線程持續(xù)執(zhí)行),每個(gè)線程對(duì)一個(gè)一個(gè)棧,最終沒(méi)有多余的空間來(lái)為新的線程分配,導(dǎo)致OutOfMemoryError
*/
public class StackOOM {
    private static int threadNum = 0;
    public void doSomething() {
        try {
            Thread.sleep(100000000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    public static void main(String[] args) {
        final StackOOM stackOOM = new StackOOM();
        try {
            while (true) {
                threadNum++;
                Thread thread = new Thread(new Runnable() {
                    @Override
                    public void run() {
                        stackOOM.doSomething();
                    }
                });
                thread.start();
            }
        } catch (Throwable e) {
            System.out.println("目前活動(dòng)線程數(shù)量:" + threadNum);
            throw e;
        }
    }
}


上述代碼運(yùn)行后會(huì)報(bào)異常

在堆棧信息中可以看到java.lang.OutOfMemoryError: unable to create new native thread的信息,無(wú)法創(chuàng)建新的線程,說(shuō)明是在擴(kuò)展棧的時(shí)候產(chǎn)生的內(nèi)存溢出異常。

總結(jié):在線程較少的時(shí)候,某個(gè)線程請(qǐng)求深度過(guò)大,會(huì)報(bào) StackOverflow 異常,解決這種問(wèn)題可以適當(dāng)加大棧的深度(增加棧空間大小),也就是把 -Xss 的值設(shè)置大一些,但一般情況下是代碼問(wèn)題的可能性較大;在虛擬機(jī)產(chǎn)生線程時(shí),無(wú)法為該線程申請(qǐng)??臻g了。

會(huì)報(bào) OutOfMemoryError 異常,解決這種問(wèn)題可以適當(dāng)減小棧的深度,也就是把 -Xss 的值設(shè)置小一些,每個(gè)線程占用的空間小了,總空間一定就能容納更多的線程,但是操作系統(tǒng)對(duì)一個(gè)進(jìn)程的線程數(shù)有限制,經(jīng)驗(yàn)值在 3000~5000 左右。

在 jdk1.5 之前 -Xss 默認(rèn)是 256k,jdk1.5 之后默認(rèn)是 1M,這個(gè)選項(xiàng)對(duì)系統(tǒng)硬性還是蠻大的,設(shè)置時(shí)要根據(jù)實(shí)際情況,謹(jǐn)慎操作。

3、方法區(qū)溢出

前面說(shuō)到,方法區(qū)主要用于存儲(chǔ)虛擬機(jī)加載的類信息、常量、靜態(tài)變量,以及編譯器編譯后的代碼等數(shù)據(jù),所以方法區(qū)溢出的原因就是沒(méi)有足夠的內(nèi)存來(lái)存放這些數(shù)據(jù)。

由于在 jdk1.6 之前字符串常量池是存在于方法區(qū)中的,所以基于 jdk1.6 之前的虛擬機(jī),可以通過(guò)不斷產(chǎn)生不一致的字符串(同時(shí)要保證和 GC Roots 之間保證有可達(dá)路徑)來(lái)模擬方法區(qū)的 OutOfMemoryError 異常;但方法區(qū)還存儲(chǔ)加載的類信息,所以基于 jdk1.7 的虛擬機(jī),可以通過(guò)動(dòng)態(tài)不斷創(chuàng)建大量的類來(lái)模擬方法區(qū)溢出。

/**
* 設(shè)置方法區(qū)最大、最小空間:-XX:PermSize=10m -XX:MaxPermSize=10m
* 運(yùn)行時(shí),通過(guò)cglib不斷創(chuàng)建JavaMethodAreaOOM的子類,方法區(qū)中類信息越來(lái)越多,最終沒(méi)有可以為新的類分配的內(nèi)存導(dǎo)致內(nèi)存溢出
*/
public class JavaMethodAreaOOM {
    public static void main(final String[] args){
       try {
           while (true){
               Enhancer enhancer=new Enhancer();
               enhancer.setSuperclass(JavaMethodAreaOOM.class);
               enhancer.setUseCache(false);
               enhancer.setCallback(new MethodInterceptor() {
                   @Override
                   public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
                       return methodProxy.invokeSuper(o,objects);
                   }
               });
               enhancer.create();
           }
       }catch (Throwable t){
           t.printStackTrace();
       }
    }
}


上述代碼運(yùn)行后會(huì)報(bào):

 java.lang.OutOfMemoryError: PermGen space 的異常,說(shuō)明是在方法區(qū)出現(xiàn)了內(nèi)存溢出的錯(cuò)誤。

4、本機(jī)直接內(nèi)存溢出

本機(jī)直接內(nèi)存(DirectMemory)并不是虛擬機(jī)運(yùn)行時(shí)數(shù)據(jù)區(qū)的一部分,也不是 Java 虛擬機(jī)規(guī)范中定義的內(nèi)存區(qū)域,但 Java 中用到 NIO 相關(guān)操作時(shí)(比如 ByteBuffer 的 allocteDirect 方法申請(qǐng)的是本機(jī)直接內(nèi)存),也可能會(huì)出現(xiàn)內(nèi)存溢出的異常。

JVM內(nèi)存區(qū)域劃分,便于它能夠更加高效的管理自身的內(nèi)存。當(dāng)程序中出現(xiàn)這種由于JVM造成的內(nèi)存溢出的情況的時(shí)候,需要根據(jù)不同的情況做不同的分析與處理。

以上就是如何理解JVM 內(nèi)存區(qū)域及內(nèi)存溢出分析,小編相信有部分知識(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