溫馨提示×

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

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

深入理解Java虛擬機(jī)-Java內(nèi)存區(qū)域透徹分析

發(fā)布時(shí)間:2020-06-28 07:11:47 來(lái)源:網(wǎng)絡(luò) 閱讀:133 作者:歐陽(yáng)思海 欄目:編程語(yǔ)言

Java虛擬機(jī)深入理解系列全部文章更新中...

  • 深入理解Java虛擬機(jī)-Java內(nèi)存區(qū)域透徹分析
  • 深入理解Java虛擬機(jī)-常用vm參數(shù)分析
  • 深入理解Java虛擬機(jī)-JVM內(nèi)存分配與回收策略原理,從此告別JVM內(nèi)存分配文盲
  • 深入理解Java虛擬機(jī)-如何利用JDK自帶的命令行工具監(jiān)控上百萬(wàn)的高并發(fā)的虛擬機(jī)性能
  • 深入理解Java虛擬機(jī)-如何利用VisualVM對(duì)高并發(fā)項(xiàng)目進(jìn)行性能分析
  • 深入理解Java虛擬機(jī)-你了解GC算法原理嗎

這篇文章主要介紹Java內(nèi)存區(qū)域,也是作為Java虛擬機(jī)的一些最基本的知識(shí),理解了這些知識(shí)之后,才能更好的進(jìn)行Jvm調(diào)優(yōu)或者更加深入的學(xué)習(xí),本來(lái)這些知識(shí)是晦澀難懂的,所以希望能夠講解的透徹且形象。

0 運(yùn)行時(shí)數(shù)據(jù)區(qū)域

JVM載執(zhí)行Java程序的過(guò)程中會(huì)把它所管理的內(nèi)存劃分為若干個(gè)不同的數(shù)據(jù)區(qū)域。

Java 虛擬機(jī)所管理的內(nèi)存一共分為Method Area(方法區(qū))、VM Stack(虛擬機(jī)棧)、Native Method Stack(本地方法棧)、Heap(堆)、Program Counter Register(程序計(jì)數(shù)器)五個(gè)區(qū)域。

這些區(qū)域都有各自的用途,以及創(chuàng)建和銷毀的時(shí)間,有的區(qū)域隨著虛擬機(jī)進(jìn)程的啟動(dòng)而存在,有些區(qū)域則是依賴用戶線程的啟動(dòng)和結(jié)束而建立和銷毀。具體如下圖所示:

深入理解Java虛擬機(jī)-Java內(nèi)存區(qū)域透徹分析cdn.xitu.io/2019/12/10/16eeeba1315b179a?w=593&h=479&f=png&s=18655">

上圖介紹的是JDK1.8 JVM運(yùn)行時(shí)內(nèi)存數(shù)據(jù)區(qū)域劃分。1.8同1.7比,最大的差別就是:元數(shù)據(jù)區(qū)取代了永久代。元空間的本質(zhì)和永久代類似,都是對(duì)JVM規(guī)范中方法區(qū)的實(shí)現(xiàn)。不過(guò)元空間與永久代之間最大的區(qū)別在于:元數(shù)據(jù)空間并不在虛擬機(jī)中,而是使用本地內(nèi)存

1 程序計(jì)數(shù)器(Program Counter Register)

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

程序計(jì)數(shù)器是一塊 “線程私有” 的內(nèi)存,每條線程都有一個(gè)獨(dú)立的程序計(jì)數(shù)器,能夠?qū)⑶袚Q后的線程恢復(fù)到正確的執(zhí)行位置。

  • 執(zhí)行的是一個(gè)Java方法

計(jì)數(shù)器記錄的是正在執(zhí)行的虛擬機(jī)字節(jié)碼指令的地址。

  • 執(zhí)行的是Native方法

計(jì)數(shù)器為空(Undefined),因?yàn)閚ative方法是java通過(guò)JNI直接調(diào)用本地C/C++庫(kù),可以近似的認(rèn)為native方法相當(dāng)于C/C++暴露給java的一個(gè)接口,java通過(guò)調(diào)用這個(gè)接口從而調(diào)用到C/C++方法。由于該方法是通過(guò)C/C++而不是java進(jìn)行實(shí)現(xiàn)。那么自然無(wú)法產(chǎn)生相應(yīng)的字節(jié)碼,并且C/C++執(zhí)行時(shí)的內(nèi)存分配是由自己語(yǔ)言決定的,而不是由JVM決定的。

  • 程序計(jì)數(shù)器也是唯一一個(gè)在Java虛擬機(jī)規(guī)范中沒(méi)有規(guī)定任何OutOfMemoryError情況的內(nèi)存區(qū)域。

其實(shí),我感覺(jué)這塊區(qū)域,作為我們開發(fā)人員來(lái)說(shuō)是不能過(guò)多的干預(yù)的,我們只需要了解有這個(gè)區(qū)域的存在就可以,并且也沒(méi)有虛擬機(jī)相應(yīng)的參數(shù)可以進(jìn)行設(shè)置及控制。

2 Java虛擬機(jī)棧(Java Virtual Machine Stacks)

深入理解Java虛擬機(jī)-Java內(nèi)存區(qū)域透徹分析

Java虛擬機(jī)棧(Java Virtual Machine Stacks)描述的是Java方法執(zhí)行的內(nèi)存模型:每個(gè)方法在執(zhí)行的同時(shí)都會(huì)創(chuàng)建一個(gè)棧幀(Stack Frame),從上圖中可以看出,棧幀中存儲(chǔ)著局部變量表、操作數(shù)棧、動(dòng)態(tài)鏈接、方法出口等信息。每一個(gè)方法從調(diào)用直至執(zhí)行完成的過(guò)程,會(huì)對(duì)應(yīng)一個(gè)棧幀在虛擬機(jī)棧中入棧到出棧的過(guò)程。

與程序計(jì)數(shù)器一樣,Java虛擬機(jī)棧也是線程私有的。

局部變量表中存放了編譯期可知的各種:

  • 基本數(shù)據(jù)類型(boolen、byte、char、short、int、 float、 long、double)
  • 對(duì)象引用(reference類型,它不等于對(duì)象本身,可能是一個(gè)指向?qū)ο笃鹗嫉刂返闹羔?,也可能是指向一個(gè)代表對(duì)象的句柄或其他與此對(duì)象相關(guān)的位置)
  • returnAddress類型(指向了一條字節(jié)碼指令的地址)

其中64位長(zhǎng)度的long和double類型的數(shù)據(jù)會(huì)占用2個(gè)局部變量空間(Slot),其余數(shù)據(jù)類型只占用1個(gè)。局部變量表所需的內(nèi)存空間在編譯期間完成分配,當(dāng)進(jìn)入一個(gè)方法時(shí),這個(gè)方法需要在幀中分配多大的局部變量空間是完全確定的,在方法運(yùn)行期間不會(huì)改變局部變量表的大小。

Java虛擬機(jī)規(guī)范中對(duì)這個(gè)區(qū)域規(guī)定了兩種異常狀況:

  • StackOverflowError:線程請(qǐng)求的棧深度大于虛擬機(jī)所允許的深度,將會(huì)拋出此異常。
  • OutOfMemoryError:當(dāng)可動(dòng)態(tài)擴(kuò)展的虛擬機(jī)棧在擴(kuò)展時(shí)無(wú)法申請(qǐng)到足夠的內(nèi)存,就會(huì)拋出該異常。

一直覺(jué)得上面的概念性的知識(shí)還是比較抽象的,下面我們通過(guò)JVM參數(shù)的方式來(lái)控制棧的內(nèi)存容量,模擬StackOverflowError異常現(xiàn)象。

3 本地方法棧(Native Method Stack)

本地方法棧(Native Method Stack) 與Java虛擬機(jī)棧作用很相似,它們的區(qū)別在于虛擬機(jī)棧為虛擬機(jī)執(zhí)行Java方法(即字節(jié)碼)服務(wù),而本地方法棧則為虛擬機(jī)使用到的Native方法服務(wù)。

在虛擬機(jī)規(guī)范中對(duì)本地方法棧中使用的語(yǔ)言、方式和數(shù)據(jù)結(jié)構(gòu)并無(wú)強(qiáng)制規(guī)定,因此具體的虛擬機(jī)可實(shí)現(xiàn)它。甚至有的虛擬機(jī)(Sun HotSpot虛擬機(jī))直接把本地方法棧和虛擬機(jī)棧合二為一。與虛擬機(jī)一樣,本地方法棧會(huì)拋出StackOverflowErrorOutOfMemoryError異常。

  • 使用-Xss參數(shù)減少棧內(nèi)存容量(更多的JVM參數(shù)可以參考這篇文章:深入理解Java虛擬機(jī)-常用vm參數(shù)分析)

這個(gè)例子中,我們將棧內(nèi)存的容量設(shè)置為256K(默認(rèn)1M),并且再定義一個(gè)變量查看棧遞歸的深度。

/**
 * @ClassName Test_02
 * @Description 設(shè)置Jvm參數(shù):-Xss256k
 * @Author 歐陽(yáng)思海
 * @Date 2019/9/30 11:05
 * @Version 1.0
 **/
public class Test_02 {

    private int len = 1;

    public void stackTest() {
        len++;
        System.out.println("stack len:" + len);
        stackTest();
    }

    public static void main(String[] args) {
        Test_02 test = new Test_02();
        try {
            test.stackTest();
        } catch (Throwable e) {
            e.printStackTrace();
        }
    }
}

運(yùn)行時(shí)設(shè)置JVM參數(shù)

深入理解Java虛擬機(jī)-Java內(nèi)存區(qū)域透徹分析

輸出結(jié)果:

深入理解Java虛擬機(jī)-Java內(nèi)存區(qū)域透徹分析

4 Java堆(Heap)

對(duì)于大多數(shù)應(yīng)用而言,Java堆(Heap)是Java虛擬機(jī)所管理的內(nèi)存中最大的一塊,它被所有線程共享的,在虛擬機(jī)啟動(dòng)時(shí)創(chuàng)建。此內(nèi)存區(qū)域唯一的目的存放對(duì)象實(shí)例,幾乎所有的對(duì)象實(shí)例都在這里分配內(nèi)存,且每次分配的空間是不定長(zhǎng)的。在Heap 中分配一定的內(nèi)存來(lái)保存對(duì)象實(shí)例,實(shí)際上只是保存對(duì)象實(shí)例的屬性值,屬性的類型對(duì)象本身的類型標(biāo)記等,并不保存對(duì)象的方法(方法是指令,保存在Stack中),在Heap 中分配一定的內(nèi)存保存對(duì)象實(shí)例和對(duì)象的序列化比較類似。

Java堆是垃圾收集器管理的主要區(qū)域,因此也被稱為 “GC堆(Garbage Collected Heap)” 。從內(nèi)存回收的角度看內(nèi)存空間可如下劃分:

深入理解Java虛擬機(jī)-Java內(nèi)存區(qū)域透徹分析

  • 新生代(Young): 新生成的對(duì)象優(yōu)先存放在新生代中,新生代對(duì)象朝生夕死,存活率很低。在新生代中,常規(guī)應(yīng)用進(jìn)行一次垃圾收集一般可以回收70% ~ 95% 的空間,回收效率很高。

如果把新生代再分的細(xì)致一點(diǎn),新生代又可細(xì)分為Eden空間From Survivor空間、To Survivor空間,默認(rèn)比例為8:1:1。

  • 老年代(Tenured/Old):在新生代中經(jīng)歷了多次(具體看虛擬機(jī)配置的閥值)GC后仍然存活下來(lái)的對(duì)象會(huì)進(jìn)入老年代中。老年代中的對(duì)象生命周期較長(zhǎng),存活率比較高,在老年代中進(jìn)行GC的頻率相對(duì)而言較低,而且回收的速度也比較慢。
  • 永久代(Perm):永久代存儲(chǔ)類信息、常量、靜態(tài)變量、即時(shí)編譯器編譯后的代碼等數(shù)據(jù),對(duì)這一區(qū)域而言,Java虛擬機(jī)規(guī)范指出可以不進(jìn)行垃圾收集,一般而言不會(huì)進(jìn)行垃圾回收。

其中新生代和老年代組成了Java堆的全部?jī)?nèi)存區(qū)域,而永久代不屬于堆空間,它在JDK 1.8以前被Sun HotSpot虛擬機(jī)用作方法區(qū)的實(shí)現(xiàn)

另外,再?gòu)?qiáng)調(diào)一下堆空間內(nèi)存分配的大體情況,這對(duì)于后面一些Jvm優(yōu)化的技巧還是有幫助的。

  • 老年代 : 三分之二的堆空間
  • 年輕代 : 三分之一的堆空間
    eden區(qū): 8/10 的年輕代空間
    survivor0 : 1/10 的年輕代空間
    survivor1 : 1/10 的年輕代空間

最后,我們?cè)偻ㄟ^(guò)一個(gè)簡(jiǎn)單的例子更加形象化的展示一下堆溢出的情況。

  • JVM參數(shù)設(shè)置:-Xms10m -Xmx10m

這里將堆的最小值和最大值都設(shè)置為10m,如果不了解這些參數(shù)的含義,可以參考這篇文章:深入理解Java虛擬機(jī)-常用vm參數(shù)分析

/**
 * VM Args:-Xms10m -Xmx10m -XX:+HeapDumpOnOutOfMemoryError
 * @author zzm
 */
public class HeapTest {

    static class HeapObject {
    }

    public static void main(String[] args) {
        List<HeapObject> list = new ArrayList<HeapObject>();

        //不斷的向堆中添加對(duì)象
        while (true) {
            list.add(new HeapObject());
        }
    }
}

輸出結(jié)果:
深入理解Java虛擬機(jī)-Java內(nèi)存區(qū)域透徹分析

圖中出現(xiàn)了java.lang.OutOfMemoryError,并且提示了Java heap space,這就說(shuō)明是Java堆內(nèi)存溢出的情況。

堆的Dump文件分析

我的使用的是VisualVM工具進(jìn)行分析,關(guān)于如何使用這個(gè)工具查看這篇文章(深入理解Java虛擬機(jī)-如何利用VisualVM對(duì)高并發(fā)項(xiàng)目進(jìn)行性能分析 )。在運(yùn)行程序之后,會(huì)同時(shí)打開VisualVM工具,查看堆內(nèi)存的變化情況。

深入理解Java虛擬機(jī)-Java內(nèi)存區(qū)域透徹分析

在上圖中,可以看到,堆的最大值是30m,但是使用的堆的容量也快接近30m了,所以很容易發(fā)生堆內(nèi)存溢出的情況。

接著查看dump文件。

深入理解Java虛擬機(jī)-Java內(nèi)存區(qū)域透徹分析

如上圖,堆中的大部分的對(duì)象都是HeapObject,所以,就是因?yàn)檫@個(gè)對(duì)象的一直產(chǎn)生,所以導(dǎo)致堆內(nèi)存不夠分配,所以出現(xiàn)內(nèi)存溢出。

我們?cè)倏碐C情況。

深入理解Java虛擬機(jī)-Java內(nèi)存區(qū)域透徹分析

如上圖,Eden新生代總共48次minor gc,耗時(shí)1.168s,基本滿足要求,但是survivor卻沒(méi)有,這不正常,同時(shí)Old Gen老年代總共27次full gc,耗時(shí)4.266s,耗時(shí)長(zhǎng),gc多,這正是因?yàn)榇罅康拇髮?duì)象進(jìn)入到老年代導(dǎo)致的,所以,導(dǎo)致full gc頻繁。

5 方法區(qū)(Method Area)

方法區(qū)(Method Area) 與Java堆一樣,是各個(gè)線程共享的內(nèi)存區(qū)域。它用于存儲(chǔ)一杯虛擬機(jī)加載類信息、常量、靜態(tài)變量、及時(shí)編譯器編譯后的代碼等數(shù)據(jù)。正因?yàn)榉椒▍^(qū)所存儲(chǔ)的數(shù)據(jù)與堆有一種類比關(guān)系,所以它還被稱為 Non-Heap。

<big>運(yùn)行時(shí)常量池(Runtime Constant Pool)</big>

運(yùn)行時(shí)常量池(Runtime Constant Pool)是方法區(qū)的一部分。Class文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項(xiàng)信息是常量池(Constant Pool Table),用于存放編譯期生成的各種字面量和符號(hào)引用,這部分內(nèi)容將在類加載后進(jìn)入方法區(qū)的運(yùn)行時(shí)常量池存放。

Java虛擬機(jī)對(duì)Class文件每一部分(自然包括常量池)的格式有嚴(yán)格規(guī)定,每一個(gè)字節(jié)用于存儲(chǔ)那種數(shù)據(jù)都必須符合規(guī)范上的要求才會(huì)被虛擬機(jī)認(rèn)可、裝載和執(zhí)行。但對(duì)于運(yùn)行時(shí)常量池,Java虛擬機(jī)規(guī)范沒(méi)有做任何有關(guān)細(xì)節(jié)的要求,不同的提供商實(shí)現(xiàn)的虛擬機(jī)可以按照自己的需求來(lái)實(shí)現(xiàn)此內(nèi)存區(qū)域。不過(guò)一般而言,除了保存Class文件中的描述符號(hào)引用外,還會(huì)把翻譯出的直接引用也存儲(chǔ)在運(yùn)行時(shí)常量池中。

運(yùn)行時(shí)常量池相對(duì)于Class文件常量池的另外一個(gè)重要特征是具備動(dòng)態(tài)性,Java語(yǔ)言并不要求常量一定只有編譯器才能產(chǎn)生,也就是并非置入Class文件中的常量池的內(nèi)容才能進(jìn)入方法區(qū)運(yùn)行時(shí)常量池,運(yùn)行期間也可能將新的常量放入池中。

運(yùn)行時(shí)常量池舉例

上面的動(dòng)態(tài)性在開發(fā)中用的比較多的便是String類的intern() 方法。所以,我們以intern() 方法舉例,講解一下運(yùn)行時(shí)常量池

String.intern()是一個(gè)native方法,作用是:如果字符串常量池中已經(jīng)包含有一個(gè)等于此String對(duì)象的字符串,則直接返回池中的字符串;否則,加入到池中,并返回。

/**
 * @ClassName MethodTest
 * @Description vm參數(shù)設(shè)置:-Xms512m -Xmx512m -Xmn128m -XX:PermSize=10M -XX:MaxPermSize=10M -XX:NewRatio=4 -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=15 -XX:-HeapDumpOnOutOfMemoryError -XX:+UseParNewGC -XX:+UseConcMarkSweepGC
 * @Author 歐陽(yáng)思海
 * @Date 2019/11/25 20:06
 * @Version 1.0
 **/

public class MethodTest {

    public static void main(String[] args) {
        List<String> list = new ArrayList<String>();
        long i = 0;
        while (i < 1000000000) {
            System.out.println(i);
            list.add(String.valueOf(i++).intern());
        }
    }
}

vm參數(shù)介紹:

-Xms512m -Xmx512m -Xmn128m -XX:PermSize=10M -XX:MaxPermSize=10M -XX:NewRatio=4 -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=15 -XX:-HeapDumpOnOutOfMemoryError -XX:+UseParNewGC -XX:+UseConcMarkSweepGC
開始堆內(nèi)存和最大堆內(nèi)存都是512m,永久代大小10m,新生代和老年代1:4,E:S1:S2=8:1:1,最大經(jīng)過(guò)15次survivor進(jìn)入老年代,使用的,垃圾收集器是新生代ParNew,老年代CMS。

通過(guò)這樣的設(shè)置之后,查看運(yùn)行結(jié)果:
深入理解Java虛擬機(jī)-Java內(nèi)存區(qū)域透徹分析

首先堆內(nèi)存耗完,然后看看GC情況,設(shè)置這些參數(shù)之后,GC情況應(yīng)該會(huì)不錯(cuò),拭目以待。

深入理解Java虛擬機(jī)-Java內(nèi)存區(qū)域透徹分析

上圖是GC情況,我們可以看到新生代 21 次minor gc,用了1.179秒,平均不到50ms一次,性能不錯(cuò),老年代 117 次full gc,用了45.308s,平均一次不到1s,性能也不錯(cuò),說(shuō)明jvm運(yùn)行是不錯(cuò)的。

注意: 在JDK1.6及以前的版本中運(yùn)行以上代碼,因?yàn)槲覀兺ㄟ^(guò)-XX:PermSize=10M -XX:MaxPermSize=10M設(shè)置了方法區(qū)的大小,所以也就是設(shè)置了常量池的容量,所以運(yùn)行之后,會(huì)報(bào)錯(cuò):java.lang.OutOfMemoryError:PermGen space,這說(shuō)明常量池溢出;在JDK1.7及以后的版本中,將會(huì)一直運(yùn)行下去,不會(huì)報(bào)錯(cuò),在前面也說(shuō)到,JDK1.7及以后,去掉了永久代。

6 直接內(nèi)存

直接內(nèi)存(Direct Memory)并不是虛擬機(jī)運(yùn)行時(shí)數(shù)據(jù)區(qū)的一部分,也不是Java虛擬機(jī)規(guī)范中定義的內(nèi)存區(qū)域。但這部分內(nèi)存也被頻繁運(yùn)用,而卻可能導(dǎo)致OutOfMemoryError異常出現(xiàn)。

這個(gè)我們實(shí)際中主要接觸到的就是NIO,在NIO中,我們?yōu)榱四軌蚣涌霫O操作,采用了一種直接內(nèi)存的方式,使得相比于傳統(tǒng)的IO快了很多。

在NIO引入了一種基于通道(Channel)與緩沖區(qū)(Buffer)的I/O方式,它可以使用Native函數(shù)庫(kù)直接分配堆外內(nèi)存,然后通過(guò)一個(gè)存儲(chǔ)在Java堆中的DirectByteBuffer對(duì)象作為這塊內(nèi)存的引用進(jìn)行操作。這樣能避免在Java堆和Native堆中來(lái)回復(fù)制數(shù)據(jù),在一些場(chǎng)景里顯著提高性能。

在配置虛擬機(jī)參數(shù)時(shí),會(huì)根據(jù)實(shí)際內(nèi)存設(shè)置-Xmx等參數(shù)信息,但經(jīng)常忽略直接內(nèi)存,使得各個(gè)內(nèi)存區(qū)域總和大于物理內(nèi)存限制(包括物理的和操作系統(tǒng)的限制),從而導(dǎo)致動(dòng)態(tài)擴(kuò)展時(shí)出現(xiàn)OutOfMemoryError異常。

1、原創(chuàng)不易,老鐵,文章需要你的點(diǎn)贊讓更多的人看到,希望能夠幫助到大家!

2、文章有不當(dāng)之處,歡迎指正,如果喜歡微信閱讀,你也可以關(guān)注我的微信公眾號(hào)好好學(xué)java,公眾號(hào)已有 6W 粉絲,回復(fù):1024,獲取公眾號(hào)的大禮包,公眾號(hào)長(zhǎng)期發(fā)布 Java 優(yōu)質(zhì)系列文章,關(guān)注我們一定會(huì)讓你收獲很多!

向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)容。

AI