1. 程式人生 > >JVM記憶體模型詳解

JVM記憶體模型詳解

引言

JVM記憶體模型可以分為兩個部分,如下圖所示,堆和方法區是所有執行緒共有的,而虛擬機器棧,本地方法棧和程式計數器則是執行緒私有的。下面我們就來一一分析一下這些不同區域的作用。

jvm memory modeljvm memory model

堆記憶體

堆記憶體是所有執行緒共有的,可以分為兩個部分:年輕代和老年代。下圖中的Perm代表的是永久代,但是注意永久代並不屬於堆記憶體中的一部分,同時jdk1.8之後永久代也將被移除。

hotspot heap memoryhotspot heap memory

GC(垃圾回收器)對年輕代中的物件進行回收被稱為Minor GC,用通俗一點的話說年輕代就是用來存放的年輕的物件,年輕物件是什麼意思呢?年輕物件可以簡單的理解為沒有經歷過多次垃圾回收的物件,如果一個物件經歷過了一定次數的Minor GC

,JVM一般就會將這個物件放入到年老代,而JVM對年老代的物件的回收則稱為Major GC

如上圖所示,年輕代中還可以細分為三個部分,我們需要重點關注這幾點:

  1. 大部分物件剛建立的時候,JVM會將其分佈到Eden區域。
  2. Eden區域中的物件達到一定的數目的時候,就會進行Minor GC,經歷這次垃圾回收後所有存活的物件都會進入兩個Suvivor Place中的一個。
  3. 同一時刻兩個Suvivor Place,即s0和s1中總有一個總是空的。
  4. 年輕代中的物件經歷過了多次的垃圾回收就會轉移到年老代中,可以通過MaxTenuringThrehold引數來控制。

當申請不到空間時會丟擲 OutOfMemoryError。下面我們簡單的模擬一個堆記憶體溢位的情況:

import java.util.ArrayList;
import java.util.List;

/* java -Xms20m -Xmx20m HeapOOM */
public class HeapOOM {
    static class OOMObject {
    }

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

        while (true) {
            list.add(new
OOMObject()); } } }

下面是執行結果:

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
    at java.util.Arrays.copyOf(Arrays.java:3210)
    at java.util.Arrays.copyOf(Arrays.java:3181)
    at java.util.ArrayList.grow(ArrayList.java:261)
    at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:235)
    at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:227)
    at java.util.ArrayList.add(ArrayList.java:458)
    at HeapOOM.main(HeapOOM.java:13)

堆記憶體是我們平時在生產環境中進行效能調優中的一個非常重要的部分,對於這裡我在另外一篇文章JVM垃圾回收演算法及回收器詳解有詳細介紹,這裡我們還是拓展補充幾個常見的效能調優引數:

  • PretenureSizeThreshold: 直接晉升到老年代的物件大小,設定這個引數後,大於這個引數的物件將直接在老年代分配。
  • MaxTenuringThrehold: 晉升到老年代的物件年齡。每個物件在堅持過一次Minor GC之後,年齡就會加1,當超過這個引數值時就進入老年代。
  • UseAdaptiveSizePolicy: 動態調整Java堆中各個區域的大小以及進入老年代的年齡。
  • SurvivorRattio: 新生代Eden區域與Survivor區域的容量比值,預設為8,代表Eden: Suvivor= 8: 1。
  • NewRatio: 設定新生代(包括Eden和兩個Survivor區)與老年代的比值(除去持久代),設定為3,則新生代與老年代所佔比值為1:3,新生代佔整個堆疊的1/4。
  • Xmx: 設定JVM堆最大記憶體。
  • Xms: 設定JVM堆初始記憶體。
  • Xmn: 設定年輕代大小。

方法區

方法區與Java堆一樣,是各個執行緒共享的區域,它用於儲存已被虛擬機器載入的類資訊,常量,靜態變數,即時編譯(JIT)後的程式碼等資料。

對於JDK1.8之前的HotSpot虛擬機器而言,很多人經常將方法區稱為我們上圖中所描述的永久代,實際上兩者並不等價,因為這僅僅是HotSpot的設計團隊選擇利用永久代來實現方法區而言。同時對於其他虛擬機器比如IBM J9中是不存在永久代的概念的。

其實,移除永久代的工作從JDK1.7就開始了。JDK1.7中,儲存在永久代的部分資料就已經轉移到了Java Heap或者是 Native Heap。但永久代仍存在於JDK1.7中,並沒完全移除,譬如符號引用(Symbols)轉移到了native heap;字面量(interned strings)轉移到了java heap;類的靜態變數(class statics)轉移到了java heap。而在JDK1.8之後永久代概念也已經不再存在取而代之的是元空間metaspace。

常量池其實是方法區中的一部分,因為這裡比較重要,所以我們拿出來單獨看一下。注意我們這裡所說的執行時的常量池並僅僅是指Class檔案中的常量池,因為JVM可能會進行即時編譯進行優化,在執行時將部分常量載入到常量池中。

程式計數器

JVM中的程式計數器和計算機組成原理中提到的程式計數器PC概念類似,是執行緒私有的,用來記錄當前執行的位元組碼位置。還是稍微解釋一下吧,CPU的佔有時間是以分片的形式分配給給每個不同執行緒的,從作業系統的角度來講,在不同執行緒之間切換的時候就是依賴程式計數器來記錄上一次執行緒所執行到具體的程式碼的行數,在JVM中就是位元組碼。

Java虛擬機器棧

與程式計數器一樣,Java虛擬機器棧也是執行緒私有的,用通俗的話將它就是我們常常聽說到堆疊中的那個“棧記憶體”。虛擬機器棧描述的是Java方法執行的記憶體模型:每個方法在執行的同時都會建立一個棧幀(Stack Frame)用於儲存區域性變量表(區域性變量表需要的記憶體在編譯期間就確定了所以在方法執行期間不會改變大小),運算元棧,動態連結,方法出口等資訊。每一個方法從呼叫至出棧的過程,就對應著棧幀在虛擬機器中從入棧到出棧的過程。p.s: 關於棧幀這裡我們以後講虛擬機器位元組碼執行引擎的時候再來仔細分析。

本地方法棧

本地方法棧和Java虛擬機器棧類似,只不過是為JVM執行Native方法服務,這裡就不解釋了。

References

INFOQ