Java的內(nèi)存 - 內(nèi)存模型

關(guān)于內(nèi)存模型,很多人混淆了JVM規(guī)范和虛擬機(jī)的實現(xiàn)。這篇文章就是分析Java內(nèi)存模型的規(guī)范和HotSpot虛擬機(jī)的實現(xiàn)。

關(guān)于內(nèi)存回收的內(nèi)容,放在 《Java的內(nèi)存 - 內(nèi)存回收》中。

1. JVM內(nèi)存規(guī)范

JVM 內(nèi)存規(guī)范中定義內(nèi)存包含五個區(qū)域:程序計數(shù)器、Java棧、Native棧、堆、方法區(qū)。
除了程序計數(shù)器,其他四個內(nèi)存區(qū)域在內(nèi)存不足時,都會出現(xiàn) OutOfMemoryError。

1.1 程序計數(shù)器

作用:保存每個線程當(dāng)前執(zhí)行到的字節(jié)碼的位置。
每個線程都有一個獨立的程序計數(shù)器。只有執(zhí)行 Java 方法時才保存字節(jié)碼位置,如果是 Native 方法則值為 Undefined。

1.2 Java 棧

作用:描述當(dāng)前線程 Java 方法的執(zhí)行。
每個線程都有一個獨立的Java棧。棧區(qū)由棧幀組成,每個方法都是一個棧幀。

棧幀的組成:

  1. 操作數(shù)棧:保存各指令的操作數(shù);
  2. 指向運(yùn)行時常量池的引用(動態(tài)鏈接):保存當(dāng)前函數(shù)在方法區(qū)中的地址;
  3. 方法返回地址;
  4. 其他附加信息;
  5. 局部變量表:存的是局部變量的引用 和 原子類型的值。在編譯時就確定了每個方法需要分配多大的內(nèi)存,運(yùn)行期間不會改變;關(guān)于局部變量表的更多信息,見下一小節(jié)。

局部變量表:

  1. 局部變量表用來保存一個棧幀中的所有局部變量。
  2. 單位是 變量槽(Variable Slot),每個變量槽都能容納 boolean、byte、short、char、int、float、返回地址、引用 這8種類型的數(shù)據(jù)。double、long 占用兩個 Slot 空間。
  3. 如果當(dāng)前方法不是靜態(tài)方法,那么局部變量表的第 0 位就是 this 指針。接下來是參數(shù)列表,之后是方法內(nèi)定義的局部變量。
  4. Slot 是可以重用的,復(fù)用節(jié)省了棧的空間,Slot 的重用可以影響內(nèi)存回收。例如:
void func() {
    {
        byte[] image = new byte[1280 * 720 * 3];
    } 
    // 出了作用域后,image 可以被復(fù)用。
    int cover = 1; // 如果沒有這句,image 占用的 Slot 沒有被復(fù)用,image 不會被釋放。
    System.gc();
}

在虛擬機(jī)的實現(xiàn)中,棧幀的上一個棧幀的操作數(shù)表和下一個棧幀的局部變量表可能會有重疊。

局部變量表的容量和操作數(shù)棧的大小在編譯時就確定了
例如這個方法:

public static int sum(int a, int b) {
    int temp = a + b;
    return temp;
}

通過 javap -v 反編譯后,得到:

public static int sum(int, int);
    descriptor: (II)I
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=3, args_size=2
         0: iload_0
         1: iload_1
         2: iadd
         3: istore_2
         4: iload_2
         5: ireturn
      LineNumberTable:
        line 4: 0
        line 5: 4

其中 stack=2 表示操作數(shù)棧的深度是 2, locals=3 表示局部變量表的長度是3.

1.3 Native棧

作用:描述當(dāng)前線程 native 方法的執(zhí)行。和 Java 棧功能類似。部分虛擬機(jī)將本地方法棧和Java棧合并為一個(例如 HotSpot 虛擬機(jī))。

1.4 堆

作用: 堆區(qū)用于存儲對象實例本身和數(shù)組。
所有線程共享一個堆區(qū)。常聽到的年輕代、老年代的說法,不屬于JVM內(nèi)存規(guī)范。它是在JVM使用分代垃圾回收器時對堆內(nèi)存的分代描述。例如 HotSpot 的堆內(nèi)存劃分:

HotSpot虛擬機(jī)堆區(qū)內(nèi)存劃分.png

1.5 方法區(qū)

作用:存儲已被虛擬機(jī)加載的 類名、方法信息、常量、靜態(tài)變量、即時編譯器編譯后的代碼、運(yùn)行時常量池 等數(shù)據(jù)。
方法區(qū)是被所有的線程共享的內(nèi)存區(qū)域,是一個邏輯分區(qū),在邏輯上保持獨立,虛擬機(jī)在實現(xiàn)時可能會把方法區(qū)放在堆內(nèi)存中(例如HotSpot虛擬機(jī))。

運(yùn)行時常量池
不僅用于存放 class 文件常量池中的數(shù)據(jù),還存放運(yùn)行期間生成的常量,例如 String#intern() 方法生成的常量。


2. HotSpot 虛擬機(jī)

HotSpot VM 是 Java 虛擬機(jī)的絕對主流,除此之外還有 J9(IBM)、JRockit(Oracle) 等。Android 平臺還有 Dalvik 和 Art(這兩個虛擬機(jī)在內(nèi)存區(qū)域劃分上沒有太多資料)。更多其他虛擬機(jī)參考維基百科:Java 虛擬機(jī)。
HotSpot 使用分代垃圾回收,常見的年輕代、老年代、永久代的劃分方式就是 HotSpot 的實現(xiàn),但這并不是 JVM 規(guī)范的一部分,J9、JRockit等虛擬機(jī)都沒有 永久代 這個區(qū)域。
HotSpot 把內(nèi)存分為堆區(qū)和非堆區(qū)兩部分。

2.1 堆區(qū)

堆區(qū)分為 年輕代(Young Generation)和 老年代(Old Generartion)。
大部分對象都在年輕代創(chuàng)建,少部分占內(nèi)存很大的對象會直接在老年代中創(chuàng)建。

2.1.1 年輕代

組成:
由一個較大的 伊甸園區(qū)(Eden Space)和 兩個較小的 幸存者區(qū)(Survivor Space)組成。

伊甸園區(qū):
保存新創(chuàng)建的對象。

幸存者區(qū):
保存著 至少經(jīng)歷過一個GC而幸存下來的 對象。
又細(xì)分為 幸存者1區(qū)(S1) 和 幸存者2區(qū)(S2) 兩塊相同的區(qū)域,這樣分為兩部分是因為使用了復(fù)制垃圾回收算法。

2.1.2 老年代

老年代保存的是久經(jīng)GC但還沒有被回收的對象。通常是從幸存者區(qū)移動來的對象。

2.2 非堆區(qū)

2.2.1 代碼緩存區(qū)

JIT編譯器生成的代碼放在這個區(qū)域。

2.2.2 永久代

永久代對應(yīng)內(nèi)存規(guī)范中的方法區(qū),即保存類信息等。
方法區(qū)是一個 邏輯上獨立 的分區(qū),HotSpot虛擬機(jī)在實現(xiàn)時把這個分區(qū)放到了堆內(nèi)存中, 永久代會進(jìn)行垃圾回收(Full GC 時 如果超過了 MaxMetaspaceSize)。
永久代在 Java 7 時,部分功能被轉(zhuǎn)移到其他內(nèi)存區(qū)。例如符號引用(Symbols)轉(zhuǎn)移到了 Native 堆、字面量(Interned Strings)轉(zhuǎn)移到了 Java 堆;類的靜態(tài)變量(Class Statics)轉(zhuǎn)移到了 Java 堆。
永久代在 Java 8 被元空間取代了。

以下兩段引來自O(shè)racle官網(wǎng) 《HotSpot 虛擬機(jī)內(nèi)存管理白皮書》
1. 永久代保存的是類信息:The permanent generation holds objects that
the JVM finds convenient to have the garbage collector manage, such as objects describing classes and methods, as well as the classes and methods themselves.
2. 永久代會進(jìn)行垃圾回收: With the serial collector, the old and permanent generations are collected via a mark-sweep-compact
collection algorithm.

元空間:
元空間是用來取代永久代的一個內(nèi)存區(qū)域,它使用的不是虛擬機(jī)內(nèi)存,而是本地內(nèi)存。默認(rèn)情況元空間的大小僅受本地內(nèi)存的限制。

4. Java 棧和 Native 棧

參考前文的 Java棧 和 Native棧。


下一篇 《Java的內(nèi)存 - 內(nèi)存回收》。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容