JVM系列(二)----JVM內存結構

世界上並沒有完美的程序,但我們並不因此而沮喪,因為寫程序本來就是一個不斷追求完美的過程。

**經典JVM內存佈局(JDK8以上)**

JVM系列(二)----JVM內存結構

一、Heap(堆區)

**Heap存儲著幾乎所有的實例對象,堆由垃圾收集器自動回收,堆區由各個子線程共享使用。** 通常情況下,它佔用的空間是所有內存區域中最大的,同時也是OOM(Out Of Memory)故障最主要的發源地。

堆的內存空間既可以固定大小,也可以在運行時動態地調整。

通過如下參數設置初始值和最大值,比如-Xms256M -Xmx1024M (-X表示它是JVM運行參數,ms是memory start(最小堆容量)的簡稱,mx是memory max(最大堆容量)的簡稱)。由於服務器在不斷運行過程中,堆空間不斷地擴容與回縮,勢必形成不必要的系統壓力,所以在線上生產環境中,JVM的Xms Xmx 設置成一樣大小,避免在GC調整堆大小時帶來的額外壓力。

**堆分為兩大塊:新生代和老年代。** 對象產生之初在新生代,步入暮年進入老年代,但是老年代也接納在新生代無法容納的超大對象。

**新生代 = 1個Eden區 + 2個Survivor區。** 絕大部分對象在Eden區生成,當Eden區填滿的時候,後觸發Young Garbage Collection(YGC)。垃圾回收的時候在Eden區實現清除策略,沒有被引用的對象則直接回收。仍然存活的對象會被移送到Survivor區。Survivor區被分為S0和S1兩塊內存空間,每次YGC的時候,他們將存活的對象複製到未使用那塊空間,然後將當前正在使用的空間完全清除,交換兩塊空間的使用狀態。如果YGC要移送的對象大於Survivor區容量的上限,則直接移交給老年代。

**每個對象都有一個計數器,每次YGC都會加1。-XX:MaxTenuringThreshold參數能配置計數器的值到達某個閾值的時候,對象從新生代晉升到老年代。默認值是15,可以在Survivor區交換14次之後,晉升至老年代。** 如果參數配置為1,那麼從新生代的Eden區直接移至老年代。

如果Survivor區無法放下,或者超大對象的閾值超過上限,則嘗試在老年代進行分配;如果老年代也無法放下,則會觸發Full Garbage Collection,即FGC。如果依然無法放下,則拋出OOM。可設置參數 -XX:+HeapDumpOnOutOfMemoryError,讓JVM遇到OOM時輸出堆內信息。

二、方法區

- 方法區與Java堆一樣,是各個線程共享的內存區域,它用於存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據。

- 元空間的前身為永久代(Perm),在JDK8之後被淘汰,在JDK7及之前的版本中,只有Hotspot才有Perm區,譯為永久代,它在啟動的時候固定大小,很難進行調優,並且FGC時會移動類元信息。

- 區別於永久代,元空間在本地內存中分配。在JDK8裡,Perm區中的所有內容中 字符串常量移至堆內存,其他內容包括類元信息,字段,靜態屬性,方法,常量等移動至元空間內。

三、JVM Stack(虛擬機棧)

- java虛擬機棧是線程私有的,生命週期與線程相同。

- 虛擬機棧描述的是Java方法執行的內存模型:每個方法在執行的同時會創建一個棧幀用於存儲局部變量表,操作數棧,動態連接,方法出口等信息。每個方法從調用到執行完成的過程,都對應著一個棧幀在虛擬機棧入棧到出棧的過程。

- Java虛擬機規範中對這個區域規定兩種異常情況:

1. OutOfMemoryError(在虛擬機棧可以動態擴展的情況下,擴展時無法申請到足夠的內存);

2. StackOverflowError(線程請求的棧深度 > 虛擬機所允許的深度);

虛擬機棧通過壓棧和出棧的方式,對每個方法對應的活動棧幀進行運算處理,方法正常執行結束,肯定會跳轉到另一個棧幀上。在執行過程中,如果出現異常,會進行異常回溯,返回地址通過異常處理表確定。

**1. 局部變量表**

局部變量表是存放方法參數和局部變量的區域。相對於類屬性變量的準備階段和初始化階段,局部變量沒有準備階段,必須顯式化初始化。如果是非靜態方法,則在index[0]位置上存儲的是方法所屬對象的實例引用,隨後存儲的是參數和局部變量。

**2. 操作棧**

操作棧是一個初始狀態為空的桶式結構棧。JVM的執行引擎是基於棧的執行引擎,其中的棧指的就是操作棧。字節碼指令集的定義都是基於棧類型的,棧的深度在方法元信息的stack屬性中。

**3. 動態連接**

每個棧幀中包含一個在常量池中對當前方法的引用,目的就是支持方法調用過程的動態連接。

**4. 方法返回地址**

方法執行有兩種退出情況:第一,正常退出;第二,異常退出。無論哪種退出情況,都將返回至方法當前被調用的位置。方法退出相當於彈出當前棧幀。

**退出的三種方式:**

- 返回值壓入上層調用棧幀

- 異常信息拋給能夠處理的棧幀

- PC計數器指向方法調用後的下一條指令

四、本地方法棧

- 本地方法棧描述的是Native方法執行的內存模型

- 可能拋出的異常:與 Java 虛擬機棧一樣。

本地方法棧在JVM內存佈局中,也是線程對象私有的,但是虛擬機棧“主內”,而本地方法棧“主外”。本地方法棧為Native方法服務。

線程開始調用本地方法時,會進入一個不再受JVM約束的世界。本地方法可以通過JNI(Java Native Interface)來訪問虛擬機運行時的數據區,甚至可以調用寄存器,具有和JVM相同的能力和權限。當大量本地方法出現時,勢必會削弱JVM對系統的控制力,因為他的出錯信息比較黑盒。對於內存不足的情況,本地方法棧還是會拋出native heap OutOfMemory。

五、程序計數器(Program Counter Register,PC)

- 程序計數器是一塊較小的內存空間,可以看做是當前線程所執行的字節碼的行號指示器。

- 字節碼解釋器工作時就是通過改變這個計數器的值來選取下一條需要執行的字節碼指令,分支,循環,跳轉,異常處理,線程恢復等基礎功能都需要依賴這個計數器來完成。

- JVM多線程是通過線程輪流切換並分配處理器執行時間的方式來實現的,所以,為了線程切換後能恢復到正確的執行位置,每條線程都需要一個獨立的程序計數器,各條線程互不影響,獨立存儲,線程私有。

- 線程執行Java方法,計數器記錄的是正在執行的虛擬機字節碼指令的地址。如果執行Native方法,這個計數器值為空(Undefined)。

- 此區域是唯一一個JVM規範中沒有規定任何OOM(OutOfMemoryError)情況的區域。

每個線程創建後,都會產生自己的程序計數器和棧幀,程序計數器都用來存放執行指令的偏移量和行號指示器等,線程執行和恢復都要依賴程序計數器。程序計數器在各個線程之間互不影響,此區域也不會發生內存溢出異常。

從線程共享的角度來看,堆和元空間是所有線程共享的,而虛擬機棧、本地方法棧、程序計數器是線程內部私有的。


分享到:


相關文章: