深入理解 JVM 內(nèi)存結(jié)構(gòu)

1. 概述

JVM 把內(nèi)存進(jìn)行了劃分闪湾,不同的內(nèi)存區(qū)域有不同的功能。有的內(nèi)存區(qū)域是線程私有的绩卤,比如 Java 虛擬機(jī)棧途样、本地方法棧和程序計數(shù)器,每一條線程都有自己獨(dú)立的空間濒憋。有的內(nèi)存區(qū)域是線程共享的何暇,比如方法區(qū)和堆。

所以不同內(nèi)存區(qū)域的功能凛驮、作用域和生命周期是不同的裆站。本文做一個詳細(xì)的分析。

根據(jù) JVM 虛擬機(jī)規(guī)范黔夭,內(nèi)存結(jié)構(gòu)如下:

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

JVM 虛擬機(jī)規(guī)范屬于概念模型宏胯,具體的實(shí)現(xiàn)各個廠商的會有所差異。比如方法區(qū)的設(shè)計本姥,hotspot 在 1.7 之前使用永久代肩袍,1.7 后使用元空間。

本文主要分析 HotSpot 虛擬機(jī)的實(shí)現(xiàn)婚惫。

2. 程序計數(shù)器

JVM 支持多線程了牛,采用時間片輪轉(zhuǎn)的方式實(shí)現(xiàn)多線程并發(fā)。一個內(nèi)核每一刻只能有一個線程執(zhí)行辰妙,多線程下需要線程上下文切換鹰祸。為了確保切換過程中,不同的線程指令和數(shù)據(jù)不會發(fā)生混亂密浑,需要單獨(dú)開辟內(nèi)存空間給每個線程蛙婴,進(jìn)行線程隔離。這些區(qū)域包含了程序計數(shù)器尔破、虛擬機(jī)棧街图、本地方法棧。這些都是線程私有內(nèi)存懒构,生命周期和線程一致餐济。

如果執(zhí)行的不是本地方法,程序計數(shù)器記錄當(dāng)前線程執(zhí)行的指令地址胆剧,字節(jié)碼解釋器通過改變該計數(shù)器的值絮姆,來決定選取下一個要執(zhí)行的指令醉冤。如果執(zhí)行的是本地方法,值為空(undefined)篙悯。

程序計數(shù)器的內(nèi)存空間非常小蚁阳,是 JVM 規(guī)定的唯一不會發(fā)生內(nèi)存溢出(Out Of Memory)的區(qū)域。

3. Java 虛擬機(jī)棧

Java 虛擬機(jī)棧由棧幀組成鸽照,Java 虛擬機(jī)棧和其他常規(guī)語言的棧類似螺捐,存儲本地變量或部分計算結(jié)果,處理方法的調(diào)用和返回矮燎。虛擬機(jī)棧內(nèi)容不能進(jìn)行直接操作定血,只能用來進(jìn)行棧幀的入棧和出棧。方法的調(diào)用到執(zhí)行完成對應(yīng)的就是棧幀的入棧和出棧過程诞外。

Java 虛擬機(jī)棧的生命周期和線程對應(yīng)澜沟,在線程創(chuàng)建的同時創(chuàng)建,和程序計數(shù)器一樣都是線程私有內(nèi)存區(qū)域浅乔。

JVM-虛擬機(jī)棧

Java 虛擬機(jī)規(guī)范對虛擬機(jī)棧大小有這樣的描述:

  • 可以使用固定大小或者動態(tài)擴(kuò)展和收縮倔喂。如果是固定大小,空間大小在棧創(chuàng)建的時候就會確定下來靖苇。
  • 可以配置 Java 虛擬機(jī)棧的初始大小席噩。
  • 如果棧空間可以動態(tài)擴(kuò)展或者收縮贤壁,可以配置棧的最大值和最小值悼枢。

HotSpot 虛擬機(jī)棧的配置:

  • -Xss,設(shè)置虛擬機(jī)棧大小脾拆,JDK1.5 之后默認(rèn)為 1M馒索。棧深度受到這個堆棧大小的約束。在固定物理內(nèi)存下減小 Java 虛擬機(jī)棧大小可以產(chǎn)生更多線程名船,但是一個進(jìn)程的線程數(shù)量有約束绰上,不能無限增加。

Java 虛擬機(jī)椙眨可能會發(fā)生的異常有:

  • 如果線程請求需要的棧深度大于 JVM 限定的蜈块,會發(fā)生 StackOverflowError 異常。
  • 如果 JVM 大小可以動態(tài)擴(kuò)展迷扇,在擴(kuò)展的時候內(nèi)存不足百揭,或者在創(chuàng)建新線程時內(nèi)存不夠創(chuàng)建虛擬機(jī)棧,均會發(fā)生 OutOfMemoryError 異常蜓席。

3.1. 棧深度

方法的從調(diào)用到執(zhí)行完成器一,對應(yīng)了虛擬機(jī)棧的入棧到出棧的過程。

在編譯期就可以確認(rèn)局部變量表的大小和操作數(shù)棧的深度厨内,并且寫入到方法表的 code 屬性中祈秕,運(yùn)行期間不會發(fā)生改變渺贤。所以在編譯器每個棧幀的需要大小就可以確定了。棧深度由運(yùn)行期決定踢步。

具體的棧深度受虛擬機(jī)棧大小和棧幀大小的影響癣亚,要看使用了多少棧幀丑掺,棧幀大小多少获印。每個棧幀的大小不一定一樣,取決于各棧幀對應(yīng)方法的局部變量表和操作數(shù)棧大小等街州。

假設(shè)我們的虛擬機(jī)棧大小固定兼丰,棧幀數(shù)量達(dá)到最大值,也就是達(dá)到最大深度唆缴,深度大小和棧幀大小的示意圖如下:

JVM-虛擬機(jī)棧深度

上面的示意圖可以看出鳍征,在 Java 虛擬機(jī)棧大小固定的情況下,如果每個棧幀都很大面徽,最大可用深度就會變小艳丛。

上面只是一個示意圖,實(shí)際上虛擬機(jī)棧深度沒這么小趟紊。默認(rèn)情況下 Java 虛擬機(jī)棧有 1M氮双,平時開發(fā)時的棧幀也不會很大。

當(dāng)線程請求的棧深度大于虛擬機(jī)的所允許的棧深度會發(fā)生 StackOverflowError 異常霎匈。畢竟如果一個線程不斷地往虛擬機(jī)棧中加入棧幀戴差,會消耗掉大量的內(nèi)存,影響到其他線程的執(zhí)行铛嘱。

比如寫了一個遞歸方法暖释,沒有設(shè)置退出條件,當(dāng)要超過該線程的虛擬機(jī)棧達(dá)到最大深度會發(fā)生異常墨吓。

3.2. 棧幀

棧幀用來存儲方法執(zhí)行需要用到的數(shù)據(jù)球匕。同時還可以執(zhí)行動態(tài)鏈接,返回值給方法帖烘,分發(fā)異常亮曹。所以一個棧幀一般會劃分成以下幾個區(qū)域:局部變量表、操作數(shù)棧蚓让、動態(tài)鏈接乾忱、方法出口。

棧幀的生命周期和方法對應(yīng)历极,在方法調(diào)用的時候就會創(chuàng)建新的棧幀窄瘟,當(dāng)方法執(zhí)行結(jié)束時棧幀銷毀棧幀。即使是因為未捕獲異常退出方法趟卸,棧幀也會被銷毀蹄葱。棧幀的內(nèi)存由 JVM 虛擬機(jī)棧分配氏义。每個棧幀有自己獨(dú)立的局部變量表、操作數(shù)棧图云、指向運(yùn)行時常量池的引用惯悠。

棧幀的內(nèi)容可擴(kuò)展,比如加入調(diào)試信息竣况。

在編譯期就可以根據(jù)棧幀對應(yīng)的方法代碼克婶,確定局部變量表和操作數(shù)棧的大小。棧幀的具體大小依賴于 JVM 虛擬機(jī)的實(shí)現(xiàn)丹泉。編譯期決定了大小情萤,方法被調(diào)用時分配內(nèi)存。

線程在同一時刻只會處理一個棧幀摹恨,被稱為當(dāng)前幀筋岛,位于 Java 虛擬棧的棧頂。該幀對應(yīng)的方法被稱為當(dāng)前方法晒哄,定義該方法的類被稱為當(dāng)前類睁宰。方法的執(zhí)行會操作當(dāng)前幀的局部變量表和操作數(shù)棧。

調(diào)用新方法時寝凌,當(dāng)前幀暫停柒傻,新的棧幀加入到虛擬機(jī)棧的棧頂并成為新的當(dāng)前幀,開始處理新方法硫兰。當(dāng)方法結(jié)束調(diào)用诅愚,當(dāng)前幀出棧,返回處理結(jié)果劫映,回到上一個棧幀违孝,上一個棧幀成為當(dāng)前幀,繼續(xù)操作局部變量表和操作數(shù)棧泳赋。

棧幀屬于當(dāng)前線程私有雌桑,不會被其他線程引用到。

3.2.1. 局部變量表

每一個棧幀都會有一個局部變量表祖今,大小在編譯期就決定校坑,用來記錄方法執(zhí)行需要用到的請求參數(shù)、局部變量千诬,如果不是靜態(tài)方法的話耍目,還會存儲 this 指針來表示當(dāng)前對象實(shí)例。

局部變量的存儲基本單位為 變量槽(Variable Slot)徐绑。單個 Slot 可以存儲 boolean邪驮,byte,char傲茄,short毅访,int沮榜,float,reference 或者 returnAddress喻粹。兩個 Slot 可以存儲 long 和 double蟆融。虛擬機(jī)規(guī)范沒有對 Slot 的物理內(nèi)存大小做出明確規(guī)定,可以隨著處理器守呜、操作系統(tǒng)和虛擬機(jī)的不同而變化型酥。但因為 int、float 等都可以用 32 位的物理內(nèi)存存放弛饭,所以一個 Slot 的物理內(nèi)存必須大于 32 位冕末。

局部變量表采用 索引 進(jìn)行尋址萍歉。第一個局部變量的索引為 0侣颂。在實(shí)例方法中,始終使用局部變量 0 用來表示當(dāng)前對象實(shí)例枪孩,在 Java 中就是 this 指針憔晒。所以實(shí)例方法的局部變量的索引總是從 1 開始。

long 和 double 比較特殊蔑舞,需要使用兩個連續(xù)的 Slot 存儲拒担。這樣會占用兩個索引,取值小的那個攻询。比如一個 double 存入局部變量表从撼,它的索引值是 n,其實(shí)占用了 n 和 n+1 兩個索引钧栖,而 n+1索引是無法加載的低零。下一個局部變量的索引為 n+2。虛擬機(jī)規(guī)范并沒有要求 n 一定是偶數(shù)拯杠,所以在在局部變量表中 long 和 double 并不一定是要 64 位對齊的掏婶。不同 JVM 的實(shí)現(xiàn),可以選擇合適的方式實(shí)現(xiàn)兩個局部變量存儲 long 和 double潭陪。

這里做個實(shí)驗雄妥,創(chuàng)建一個空方法,請求參數(shù)包含所有基礎(chǔ)數(shù)據(jù)類型和一個 String 引用類型依溯,方法內(nèi)有一個 String 局部變量老厌。

public void show(boolean a, byte b, char c, short d, int e, long f, float h, double i, String j) {
    String str = "str";
}

使用 javap -v 查看 show 方法在 class 文件中的局部變量表。

  public void show(boolean, byte, char, short, int, long, float, double, java.lang.String);
    descriptor: (ZBCSIJFDLjava/lang/String;)V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=13, args_size=10
         0: ldc           #2                  // String str
         2: astore        12
         4: return
      LineNumberTable:
        line 14: 0
        line 15: 4
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Loblee/demo/jvm/stack/SimpleObject;
            0       5     1     a   Z
            0       5     2     b   B
            0       5     3     c   C
            0       5     4     d   S
            0       5     5     e   I
            0       5     6     f   J
            0       5     8     h   F
            0       5     9     i   D
            0       5    11     j   Ljava/lang/String;
            4       1    12   str   Ljava/lang/String;

這個方法的為局部變量表 LocalVariableTable 黎炉,類加載后會作為方法的元數(shù)據(jù)存儲到方法區(qū)枝秤,然后方法被調(diào)用的時候載入到新創(chuàng)建的棧幀中。

可以看到編譯期已經(jīng)確認(rèn)了表中每個局部變量的索引和大小拜隧。局部變量表的大小已經(jīng)寫入到 Code 屬性: locals=13 宿百。

這 13 個基本單位是如何計算出來的趁仙?我們上面的案例,所有方法參數(shù)一共需要的基本單位數(shù) 1 + 1 + 1 + 1 + 1 + 2 + 1 + 2 + 1 = 11 垦页,一個局部變量 str 占用 1 個 Slot雀费,有 12 個基本單位了。還有一個 Slot 呢痊焊?

這個是實(shí)例方法盏袄,加入了 this 指針用來表示當(dāng)前對象實(shí)例的引用,在 Slot 0 中:

LocalVariableTable:
Start  Length  Slot  Name   Signature
    0       5     0  this   Loblee/demo/jvm/stack/SimpleObject;

this 指針占用 1 個 Slot薄啥,所以局部變量表總體大小為 13 個 Slot辕羽。

因為 this 指針是通過參數(shù)默認(rèn)傳遞給方法的,應(yīng)該歸到方法參數(shù)中垄惧,所以實(shí)際該方法有 10 個參數(shù)刁愿,也寫入到了 code 屬性:args_size=10

從反編譯的局部變量表還可以看到索引的設(shè)計到逊,show 中參數(shù) f 為 long 類型铣口,索引到 Slot 6,因為占用兩個 Slot觉壶,下一個變量 h 索引到 Slot 8脑题。

JVM 對局部變量表進(jìn)行了優(yōu)化,變量槽 Slot 是可以復(fù)用的铜靶。

如果是靜態(tài)方法的話就不存在 this 引用了叔遂。比如我們創(chuàng)建一個靜態(tài)方法 staticShow

public static void staticShow(boolean a, byte b, char c) {
    String str = "str";
}

使用 javap -v 查看局部變量表如下:

LocalVariableTable:
Start  Length  Slot  Name   Signature
    0       8     0     a   Z
    0       8     1     b   B
    0       8     2     c   C
    3       5     3  str1   Ljava/lang/String;
    7       1     4  str2   Ljava/lang/String;

3.2.2. 操作數(shù)棧

每一個棧幀都有一個后進(jìn)先出(LIFO)的操作數(shù)棧。操作數(shù)棧應(yīng)用于字節(jié)碼執(zhí)行引擎中争剿,JVM 描述字節(jié)碼執(zhí)行引擎是基于 “椧鸭瑁” 的,指的就是操作數(shù)棧秒梅。

操作數(shù)棧的每個條目可以保存 JVM 任何類型的值旗芬,long 和 double 占據(jù)深度的兩個單位,其他類型占據(jù)一個單位捆蜀。操作數(shù)棧的最大深度由編譯期通過方法要執(zhí)行的字節(jié)碼計算出來疮丛,并記錄在 Code 屬性中。

棧幀剛創(chuàng)建時辆它,操作數(shù)棧為空誊薄。JVM 提供了一系列字節(jié)碼指令,將數(shù)據(jù)從局部變量表加載到操作數(shù)棧中锰茉。還有一些指令呢蔫,從操作數(shù)棧中讀取操作數(shù),進(jìn)行處理,然后把結(jié)果入棧片吊。操作數(shù)棧還可以用來準(zhǔn)備參數(shù)傳遞給方法绽昏,或者接收方法返回結(jié)果。比如俏脊,指令 iadd 用來對兩個 int 值進(jìn)行相加全谤。之前的指令已經(jīng)將兩個 int 值壓入到操作數(shù)棧中了,iadd 將兩個 int 值出棧爷贫,相加后將和入棧认然。

操作數(shù)棧中的數(shù)據(jù),必須用合適的類型的字節(jié)碼指令進(jìn)行操作漫萄。比如入棧兩個 int 值卷员,不能當(dāng)做 long 處理。入棧 float 不能使用 iadd 指令進(jìn)行相加腾务。有少量的 JVM 指令不關(guān)心值的類型毕骡,這些指令無法修改值。在類加載流程中窑睁,類文件的校驗階段挺峡,會強(qiáng)制實(shí)施。

設(shè)計了一個 calculate 方法來做一些加減法計算:

public int calculate(int a, int b) {
    int c = a + b;
    int d = a - b;
    int e = c + d;
    return e;
}

反編譯得到:

  public int calculate(int, int);
    descriptor: (II)I
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=6, args_size=3
         0: iload_1
         1: iload_2
         2: iadd
         3: istore_3
         4: iload_1
         5: iload_2
         6: isub
         7: istore        4
         9: iload_3
        10: iload         4
        12: iadd
        13: istore        5
        15: iload         5
        17: ireturn

可以看到操作數(shù)棧深度最大為 2担钮,本地變量表大小 6 個 Slot(索引 0 - 5)。這些字節(jié)碼的解讀如下:

         0: iload_1             加載 Slot 1(從局部變量表加載尤仍,1 表示索引)箫津。實(shí)際為從局部變量表加載 a。
         1: iload_2             加載 Slot 2宰啦。實(shí)際為從局部變量表加載 a苏遥。
         2: iadd                執(zhí)行加法。實(shí)際為 a + b赡模。
         3: istore_3            存儲計算結(jié)果到 Slot 3田炭。實(shí)際為存儲 c 到局部變量表。
         4: iload_1             加載 Slot 1漓柑。實(shí)際為從局部變量表加載 a教硫。
         5: iload_2             加載 Slot 2。實(shí)際為從局部變量表加載 b辆布。
         6: isub                執(zhí)行減法瞬矩。實(shí)際為 a - b。
         7: istore        4     存儲計算結(jié)果到 Slot 4锋玲。實(shí)際為存儲 d 到局部變量表景用。
         9: iload_3             加載 Slot 3。實(shí)際為從局部變量表加載 c惭蹂。
        10: iload         4     加載 Slot 4伞插。實(shí)際為從局部變量表加載 d割粮。
        12: iadd                執(zhí)行加法。實(shí)際為 c + d媚污。
        13: istore        5     存儲計算結(jié)果到 Slot 5穆刻。實(shí)際為存儲 e 到局部變量表。
        15: iload         5     加載 Slot 5 的數(shù)據(jù)杠步。實(shí)際為從局部變量表加載 e氢伟。
        17: ireturn             返回計算結(jié)果

我們傳入 a = 1, b = 2 進(jìn)行計算 calculate(1, 2),第一個加法操作操作數(shù)棧的變化如下:

JVM-操作數(shù)棧和局部變量表變化

這里的代碼是可以優(yōu)化的幽歼,因為局部變量 e 沒有做其他計算朵锣,可以直接返回。如果直接返回結(jié)果會有什么效果甸私?代碼如下:

public int calculate(int a, int b) {
    int c = a + b;
    int d = a - b;
    return c + d;
}

查看字節(jié)碼如下:

  public int calculate(int, int);
    descriptor: (II)I
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=5, args_size=3
         0: iload_1
         1: iload_2
         2: iadd
         3: istore_3
         4: iload_1
         5: iload_2
         6: isub
         7: istore        4
         9: iload_3
        10: iload         4
        12: iadd
        13: ireturn

局部變量表少了一個 Slot诚些,也就是原本 e 的存儲空間。要執(zhí)行的字節(jié)碼指令也少了 3 條皇型。所以平時開發(fā)過程中要注意優(yōu)化诬烹,可以提高性能。

3.2.3. 動態(tài)鏈接

每一個幀都包含了一個指向運(yùn)行時常量池的引用弃鸦,用來實(shí)現(xiàn)字節(jié)碼中的 動態(tài)鏈接(Dynamic Linking)绞吁。類文件中包含了一些字段和方法的符號引用。動態(tài)鏈接會將這些符號引用轉(zhuǎn)換成直接引用唬格,比如在內(nèi)存中的具體偏移地址家破。

如果對應(yīng)的類還沒有被加載,會觸發(fā)該類的加載流程购岗。

符號引用記錄在類常量池中汰聋,是一個由字面量組成的字符串,和具體地址無關(guān)喊积。比如所有對象的類構(gòu)造方法的符號引用為 java/lang/Object."<init>":()V 烹困。編譯并不知道運(yùn)行時的地址,所以用符號引用代替乾吻。

動態(tài)鏈接又稱動態(tài)綁定髓梅。除了該方式,還有種發(fā)生在類文件加載過程中溶弟,這個這個階段就把符號引用轉(zhuǎn)換為直接引用女淑,這樣的方式為饑餓方式或者靜態(tài)綁定。

靜態(tài)綁定和動態(tài)綁定都可以歸為是類加載機(jī)制中的 解析(Resolution) 的一部分辜御。

JVM-類加載機(jī)制

可以看出類加載機(jī)制中的環(huán)節(jié)是有可能交叉進(jìn)行的鸭你。比如解析可能發(fā)生在準(zhǔn)備階段后,靜態(tài)綁定。也可能延遲到初始化后袱巨,在棧幀創(chuàng)建后進(jìn)行動態(tài)綁定阁谆。

綁定只發(fā)生一次,綁定后不再更改愉老。

3.2.4. 方法正常結(jié)束

方法調(diào)用結(jié)束场绿,沒有發(fā)生異常。這里指直接返回結(jié)果或者是顯式調(diào)用 throw 拋出異常嫉入。

被調(diào)用方法的結(jié)果需要傳遞給調(diào)用者方法焰盗。被調(diào)用的方法會執(zhí)行和方法返回相關(guān)的指令,這些指令和返回值的類型對應(yīng)咒林。

當(dāng)前棧會被復(fù)原為調(diào)用者方法的執(zhí)行狀態(tài)熬拒,包括局部變量表和操作數(shù)棧的數(shù)據(jù),程序計數(shù)器會跳過剛剛調(diào)用方法的指令指向下一條垫竞。被調(diào)用方法的返回值被加入到操作數(shù)棧中澎粟,程序繼續(xù)運(yùn)行。

3.2.5. 方法異常結(jié)束

方法內(nèi)部發(fā)生了異常,而且沒有被捕獲,方法會被終止毁靶,并且沒有返回值給調(diào)用者。

4. 堆

堆由 JVM 所有的線程共享拆祈,一般情況下是 JVM 內(nèi)存區(qū)域中最大的一塊。按照 JVM 虛擬機(jī)規(guī)范,堆是一個用來存儲類對象實(shí)例或者數(shù)組的運(yùn)行時數(shù)據(jù)區(qū)。

在 HopSpot 上宫补,類對象實(shí)例不一定就是放在堆中,應(yīng)用了 JIT(Just-In-Time) 技術(shù)曾我,進(jìn)行逃逸分析(Escape Analysis)和標(biāo)量替換(Scalar Replacement)。符合條件的對象實(shí)例會在棧上分配健民。

JVM 啟動的時候堆就會創(chuàng)建抒巢。堆內(nèi)對象實(shí)例不會顯式釋放,由自動內(nèi)存管理系統(tǒng)秉犹,也就是垃圾收集器進(jìn)行回收蛉谜,是垃圾收集器主要管理區(qū)域。JVM 規(guī)范沒有說明垃圾收集器應(yīng)該是怎樣的崇堵,具體由實(shí)現(xiàn)由 JVM 廠商來提供型诚。

比如 HotSpot 虛擬機(jī)中,垃圾回收器采用分代回收算法鸳劳,會將堆進(jìn)行進(jìn)一步細(xì)分狰贯,分為新生代和老生代。新生代還可細(xì)分為 Eden 、From Survivor 和 To Survivor涵紊。這實(shí)際上是為了能夠更好地服務(wù)于垃圾回收傍妒。HotSpot 在 JDK 1.7 中堆還有一個永久代,其實(shí)是 JVM 規(guī)范中方法區(qū)的實(shí)現(xiàn)摸柄,在 JDK1.8 移除颤练。

HotSpot 的 JDK 1.7 堆圖示:

JVM-Heap-1.7

HopSpot 的 JDK 1.8 堆圖示,永久代(PermGen)被移除驱负,使用元空間(Metaspace)存儲類信息嗦玖。

JVM-Heap-1.8

新生代和老年代的內(nèi)存分配流程:

  • 優(yōu)先 Eden 分配,Eden 空間不足會觸發(fā) Minor GC跃脊。
  • Minor GC 后宇挫,Eden + S0 還存活的對象移動到 S1 中,清空 S0匾乓。
  • S1 放不下捞稿,存活次數(shù)達(dá)到要求的對象移動到老年代。
  • 大對象直接分配到老年代拼缝。
  • 老年代內(nèi)存不足會發(fā)生 Major GC
  • 進(jìn)行垃圾回收后娱局,Eden 仍然沒有足夠的空間,拋出 OutOfMemory 異常咧七。

Java 虛擬機(jī)規(guī)范對堆大小有這樣的描述:

  • 可以是固定大小衰齐,也可以動態(tài)的擴(kuò)展和收縮。
  • 堆的內(nèi)存不一定要連續(xù)继阻。(邏輯上連續(xù))
  • 可以配置本地方法棧初始大小耻涛,如果可動態(tài)擴(kuò)展和收縮,可配置最大值和最小值瘟檩。

主流虛擬機(jī)都是采用可動態(tài)擴(kuò)展和收縮的方式實(shí)現(xiàn)的抹缕。堆內(nèi)存物理上可以不連續(xù),但是邏輯上需要連續(xù)墨辛。

HotPot 虛擬機(jī)的堆內(nèi)存配置:

  • -Xms卓研,初始大小,默認(rèn)物理內(nèi)存的 1/64睹簇。

  • -Xmx奏赘,最大內(nèi)存,默認(rèn)物理內(nèi)存的 1/4太惠。

  • -Xmn磨淌,新生代大小,因為持久代的大小一般默認(rèn)為 64M凿渊,在整個堆固定的情況下梁只,增大新生代會相應(yīng)地減少老年代的大小缚柳。官方推薦

  • -XX:NewSize,新生代最小空間大小敛纲。

  • -XX:MaxNewSize喂击,新生代最大空間大小。

  • -XX:NewRatio淤翔,新生代和老年代的比例翰绊,新生代和老年代的默認(rèn)比例為 1:2。

  • -XX:SurvivorRatio旁壮,Eden 和 Survivor 的比例监嗜,默認(rèn)為 Eden:S0:S1 = 8:1:1,即 survivor = 1/10 新生代大小抡谐。

HotSpot 采用的就是動態(tài)擴(kuò)展和收縮的方式裁奇,根據(jù)堆的空閑情況,當(dāng)空閑大于 70%麦撵,會減少至 -Xms刽肠;空閑小于 40%,會增大到 -Xmx免胃。所以服務(wù)器如果配置 -Xms = -Xmx音五,可以避免堆自動擴(kuò)展。

堆會發(fā)生的異常:

  • 如果程序請求的堆內(nèi)存大于 JVM 內(nèi)存管理系統(tǒng)能提供的最大值羔沙,會拋出 OutOfMemoryError 異常躺涝。

5. 方法區(qū)

方法區(qū)由 JVM 所有線程共享。方法區(qū)類似一個用來存儲編譯后的代碼的區(qū)域扼雏。主要用來存儲加載的類信息坚嗜,運(yùn)行時常量池,類和方法的數(shù)據(jù)诗充,即時編譯后的代碼等苍蔬。

JVM 啟動的時候方法區(qū)就會創(chuàng)建。

根據(jù) JVM 虛擬機(jī)規(guī)范蝴蜓,方法區(qū)邏輯上是堆的一部分银室,實(shí)現(xiàn)上可以選擇不進(jìn)行垃圾回收,并且沒有要求方法區(qū)的位置等励翼。所以在方法區(qū)的具體實(shí)現(xiàn)各個虛擬機(jī)又不同的方式。雖然 JVM 虛擬機(jī)規(guī)范把方法區(qū)邏輯上劃給了堆辜荠,為了和實(shí)際堆進(jìn)行了區(qū)分汽抚,方法區(qū)還叫做 “非堆”。

Java 虛擬機(jī)規(guī)范對方法區(qū)大小的描述:

  • 可以是固定大小伯病,也可以動態(tài)的擴(kuò)展和收縮造烁。
  • 方法區(qū)的內(nèi)存不一定要連續(xù)否过。
  • 用戶或者開發(fā)者能夠配置方法區(qū)初始大小,如果方法區(qū)可以動態(tài)擴(kuò)展或收縮惭蟋,需要提供方法區(qū)的最大值和最小值苗桂。

HotSpot 在 JDK1.7 中方法區(qū)內(nèi)存大小配置:

  • -XX:PermSize,最小可分配空間告组,初始分配空間煤伟。

  • -XX:MaxPermSize,最大可分配空間木缝,默認(rèn)大小為 64M(64 位 JVM 默認(rèn)為 85M)

在 JDK1.8 使用了元空間后便锨,方法區(qū)的大小配置:

  • -XX:MetaspaceSize,初始空間大小我碟。
  • -XX:MaxMetaspaceSize放案,最大空間大小,默認(rèn)是沒有限制的矫俺。

方法區(qū)可能發(fā)生的異常:

  • 如果方法區(qū)請求的內(nèi)存無法被滿足吱殉,拋出 OutOfMemoryError 異常。

5.1. 去永久代過程

HotSpot 虛擬機(jī)在 JDK1.7 采用永久代厘托,在堆中分配內(nèi)存友雳。在 JDK1.8 后使用元空間,使用本地內(nèi)存催烘。

從 JDK1.7 開始 “去永久代”沥阱,JDK 1.7 將靜態(tài)變量、字符串常量池移動到堆內(nèi)存中伊群,JDK1.8 去掉永久代考杉,將類信息、即時編譯后的代碼等移動到了元空間舰始。

JVM-方法區(qū)到元空間

之所以要進(jìn)行去永久代崇棠,主要還是該方案存在很多問題,留下很多 bug丸卷。主要有:

  • 字符串存在永久代枕稀,容易發(fā)生內(nèi)存溢出。
  • 類信息比較難確定大小谜嫉,永久代的大小難以指定萎坷,太小永久代容易 OOM,太大老年代容易 OOM沐兰。
  • 永久代 GC 回收復(fù)雜哆档,效率低。

6. 運(yùn)行時常量池

運(yùn)行時常量池是 class 文件的常量池在運(yùn)行時的表示住闯。主要有字面量和符號引用瓜浸。

要理解運(yùn)行時常量池澳淑,我們得先了解 class 的常量池。

創(chuàng)建類 ObjectA 和 Object B插佛,其中 ObjectA 如下:

public class ObjectA {

    private ObjectB b;

    public void setB(ObjectB b) {
        this.b = b;
    }
    
    public ObjectB getB() {
        return b;
    }
}

編譯后使用 javap -v 查看 class 文件中的常量池如下杠巡。

JVM-Constant Pool

運(yùn)行時,在進(jìn)行類加載時雇寇,類常量池會被載入到 JVM 方法區(qū)氢拥。

JVM 虛擬機(jī)規(guī)范沒有約束運(yùn)行時常量池只能放編譯期的常量,虛擬機(jī)的實(shí)現(xiàn)可以自行支持谢床。比如 HotSpot 虛擬機(jī)兄一, Java 調(diào)用 String.intern() 方法,可以在運(yùn)行期把常量加入池中识腿。

在 HotSpot JDK 1.7 之后出革,對常量池進(jìn)行了優(yōu)化:字符串常量池被放在了 JVM 堆中,運(yùn)行時常量池的字面量也存在 JVM 堆中渡讼,而符號引用被移動到了本地內(nèi)存骂束。

以下的異常可能會發(fā)生:

  • 當(dāng)創(chuàng)建一個 class 或者 interface 時成箫,如果運(yùn)行時常量池構(gòu)造需要的內(nèi)存超過 JVM 所能提供的展箱,拋出 OutOfMemoryError 異常。

7. 本地方法棧

JVM 的實(shí)現(xiàn)可能需要使用 "C 棧" 去支持本地方法調(diào)用蹬昌。有可能使用 C 之類的語言混驰,實(shí)現(xiàn) JVM 指令的解釋器,也會使用到本地方法棧皂贩。本地方法棧和 Java 虛擬機(jī)棧類似栖榨,只是這里提供的是本地方法服務(wù)。虛擬機(jī)規(guī)范沒有明確指出本地方法棧使用什么語言明刷、數(shù)據(jù)結(jié)構(gòu)等婴栽,不同廠商的虛擬機(jī)又不同的實(shí)現(xiàn)。比如 HotSpot 虛擬機(jī)把本地方法棧和 Java 虛擬機(jī)棧合并了辈末。

本地方法棧的生命周期線程對應(yīng)愚争,線程創(chuàng)建的時候創(chuàng)建。如果 JVM 不需要調(diào)用本地方法挤聘,可以不需要本地方法棧轰枝。

JVM 規(guī)范對本地方法棧大小的描述

  • 可以使用固定大小,或者動態(tài)擴(kuò)展和收縮组去。如果是固定大小狸膏,當(dāng)棧被創(chuàng)建的時候能夠獨(dú)立選擇。
  • 可以配置本地方法棧初始大小添怔,如果可動態(tài)擴(kuò)展和收縮湾戳,可配置最大值和最小值。

以下異彻懔希可能發(fā)生:

  • 如果線程請求的棧深度大于系統(tǒng)規(guī)定的砾脑,報 StackOverflowError
  • 如果本地方法棸樱可以動態(tài)擴(kuò)展韧衣,沒有足夠的內(nèi)存擴(kuò)展」荷#或者創(chuàng)建新的線程沒有足夠的內(nèi)存創(chuàng)建本地方法棧畅铭,拋出 OutOfMemoryError異常。

8. 參考資料

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末勃蜘,一起剝皮案震驚了整個濱河市硕噩,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌缭贡,老刑警劉巖炉擅,帶你破解...
    沈念sama閱讀 206,126評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異阳惹,居然都是意外死亡谍失,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評論 2 382
  • 文/潘曉璐 我一進(jìn)店門莹汤,熙熙樓的掌柜王于貴愁眉苦臉地迎上來快鱼,“玉大人,你說我怎么就攤上這事纲岭∧ㄖ瘢” “怎么了?”我有些...
    開封第一講書人閱讀 152,445評論 0 341
  • 文/不壞的土叔 我叫張陵荒勇,是天一觀的道長柒莉。 經(jīng)常有香客問我,道長沽翔,這世上最難降的妖魔是什么兢孝? 我笑而不...
    開封第一講書人閱讀 55,185評論 1 278
  • 正文 為了忘掉前任,我火速辦了婚禮仅偎,結(jié)果婚禮上跨蟹,老公的妹妹穿的比我還像新娘。我一直安慰自己橘沥,他們只是感情好窗轩,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,178評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著座咆,像睡著了一般痢艺。 火紅的嫁衣襯著肌膚如雪仓洼。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 48,970評論 1 284
  • 那天堤舒,我揣著相機(jī)與錄音色建,去河邊找鬼。 笑死舌缤,一個胖子當(dāng)著我的面吹牛箕戳,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播国撵,決...
    沈念sama閱讀 38,276評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼陵吸,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了介牙?” 一聲冷哼從身側(cè)響起壮虫,我...
    開封第一講書人閱讀 36,927評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎耻瑟,沒想到半個月后旨指,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,400評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡喳整,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,883評論 2 323
  • 正文 我和宋清朗相戀三年谆构,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片框都。...
    茶點(diǎn)故事閱讀 37,997評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡搬素,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出魏保,到底是詐尸還是另有隱情熬尺,我是刑警寧澤,帶...
    沈念sama閱讀 33,646評論 4 322
  • 正文 年R本政府宣布谓罗,位于F島的核電站粱哼,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏檩咱。R本人自食惡果不足惜揭措,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,213評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望刻蚯。 院中可真熱鬧绊含,春花似錦、人聲如沸炊汹。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至充甚,卻和暖如春以政,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背津坑。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評論 1 260
  • 我被黑心中介騙來泰國打工妙蔗, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人疆瑰。 一個月前我還...
    沈念sama閱讀 45,423評論 2 352
  • 正文 我出身青樓,卻偏偏與公主長得像昙啄,于是被迫代替她去往敵國和親穆役。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,722評論 2 345

推薦閱讀更多精彩內(nèi)容

  • 第二部分 自動內(nèi)存管理機(jī)制 第二章 java內(nèi)存異常與內(nèi)存溢出異常 運(yùn)行數(shù)據(jù)區(qū)域 程序計數(shù)器:當(dāng)前線程所執(zhí)行的字節(jié)...
    小明oh閱讀 1,130評論 0 2
  • 內(nèi)存溢出和內(nèi)存泄漏的區(qū)別 內(nèi)存溢出:out of memory梳凛,是指程序在申請內(nèi)存時耿币,沒有足夠的內(nèi)存空間供其使用,...
    Aimerwhy閱讀 730評論 0 1
  • 參考原地址 JVM內(nèi)存模型 Java虛擬機(jī)(Java Virtual Machine=JVM)的內(nèi)存空間分為五個部...
    流年劃破容顏_cc55閱讀 332評論 0 0
  • 《深入理解Java虛擬機(jī)》筆記_第一遍 先取看完這本書(JVM)后必須掌握的部分韧拒。 第一部分 走近 Java 從傳...
    xiaogmail閱讀 5,062評論 1 34
  • 無題 人生就是一場聚會淹接, 接著是一場分離, 然后又是下一次的聚會叛溢, 下一次的分離塑悼, 反反復(fù)復(fù)。 每一次的聚會楷掉, 都...
    朝花夕拾木本易閱讀 1,243評論 13 14