1. 程式人生 > 實用技巧 >JVM記憶體區域

JVM記憶體區域

包含:

  • 程式計數器
  • 虛擬機器棧
  • 本地方法棧
  • 方法區(包含執行時常量池)
  • 直接記憶體

執行緒私有:程式計數器,虛擬機器棧,本地方法棧
執行緒共享:堆,方法區

程式計數器

程式計數器是一塊較小的記憶體空間,可以看作是當前執行緒執行的位元組碼行號指示器,JVM 通過改變這個計數器的值,來選取下一條需要執行的位元組碼指令,分支、迴圈、跳轉、異常處理、執行緒恢復等基礎功能。
程式計數器空間是私有的,原因在於 Java 虛擬機器是通過執行緒輪流切換並分配處理器時間來實現的多執行緒,為了執行緒切換後能恢復到正確的執行位置,每個執行緒都需要一個獨立的程式計數器。
如果執行緒正在執行 Java 方法,則程式計數器記錄的是虛擬機器位元組碼指令地址

如果執行緒正在執行 Native 方法,則程式計數器值為空(Undefined)
程式計數器是唯一一個在 Java 虛擬機器規範中不會發生 OutOfMemoryError 的區域。

簡單說就是控制執行緒執行、切換的計數指示器。

虛擬機器棧

每個方法在執行時都會建立一個棧幀(Stack Frame),用於儲存區域性變量表、運算元棧、動態連結、方法出口等資訊。方法執行從開始到結束,對應的是棧幀在虛擬機器棧中入棧及出棧的過程。以下著重介紹區域性變量表。

區域性變量表存放以下型別的變數,其中 64 位長度的 long 和 double 型別資料會佔用 2 個區域性變數空間(Slot),其餘資料型別只佔據 1 個。

編譯期已知的各種基本資料型別:boolean、byte、char、short、int、float、long、double
物件引用:reference 型別,可能是一個指向物件起始地址的引用指標,也可能是指向一個代表物件的控制代碼或其他與此物件相關的位置
returnAddress 型別:指向位元組碼指令地址
區域性變量表所需的內促農建在編譯期間完成分配,在方法執行期間不會更改區域性變量表的大小。

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

StackOverflowError: 執行緒請求的棧深度大於虛擬機器所允許的深度
OutOfMemoryError: 虛擬機器棧進行動態擴充套件時無法申請到足夠的記憶體

下面用一段簡單的程式碼說明操作棧與區域性變量表的互動


詳細的位元組碼操作順序如下:

簡單說就是方法的區域性變數和操做入棧和出棧的棧空間。

本地方法棧

虛擬機器棧是為虛擬機器執行Java方法(也就是位元組碼)服務,本地方法區則為虛擬機器使用到的Native方法服務.

虛擬機器棧“主內”,而本地方法棧“主外”
這個“內外”是針對JVM來說的,本地方法棧為Native方法服務。執行緒開始呼叫本地方法時,會進入一個不再受JVM約束的世界,本地方法可以通過JNI(Java Native Interface)來訪問虛擬機器執行時的資料區,甚至可以呼叫暫存器,具有和JVM相同的能力和許可權,當大量本地方法出現時,勢必會削弱JVM對系統的控制力,因為它的出錯資訊都比較黑盒.
對於記憶體不足的情況,本地方法棧還是會丟擲native heap OutOfMemory、以及 StackOverflowError 和 OutOfMemoryError

最著名的本地方法應該是System.currentTimeMillis()

Java 堆(Heap)

虛擬機器所管理的記憶體中最大的一塊區域,被所有執行緒共享。堆存在的唯一意義是存放物件例項,在 Java 虛擬機器規範中的表述是“所有的物件和陣列都要在堆上分配”。但是隨著 JIT 編譯器的發展與逃逸分析技術逐漸成熟,棧上分配、標量替換優化技術將會導致這一規則不再那麼絕對。GC 就是對堆上的物件進行回收。堆區域空間不足會導致 OOM。
堆的記憶體空間既可以固定大小,也可執行時動態地調整,通過如下引數設定初始值和最大值,比如

-Xms256M. -Xmx1024M
  • -X表示它是JVM執行引數
  • ms是memorystart的簡稱 最小堆容量
  • mx是memory max的簡稱 最大堆容量

但是在通常情況下,伺服器在執行過程中,堆空間不斷地擴容與回縮,勢必形成不必要的系統壓力,所以在線上生產環境中,JVM的Xms和Xmx設定成一樣大小,避免在GC後調整堆大小時帶來的額外壓力。

堆分成兩大塊:新生代和老年代
物件產生之初在新生代,步入暮年時進入老年代,但是老年代也接納在新生代無法容納的超大物件

新生代= 1個Eden區+ 2個Survivor區
絕大部分物件在Eden區生成,當Eden區裝填滿的時候,會觸發Young GC。垃圾回收的時候,在Eden區實現清除策略,沒有被引用的物件則直接回收。依然存活的物件會被移送到Survivor區,這個區真是名副其實的存在。Survivor 區分為S0和S1兩塊記憶體空間,送到哪塊空間呢?每次Young GC的時候,將存活的物件複製到未使用的那塊空間,然後將當前正在使用的空間完全清除,交換兩塊空間的使用狀態。如果YGC要移送的物件大於Survivor區容量上限,則直接移交給老年代。假如一些沒有進取心的物件以為可以一直在新生代的Survivor區交換來交換去,那就錯了。每個物件都有一個計數器,每次YGC都會加1。

-XX:MaxTenuringThreshold 

引數能配置計數器的值到達某個閾值的時候,物件從新生代晉升至老年代。如果該引數配置為1,那麼從新生代的Eden區直接移至老年代。預設值是15,可以在Survivor 區交換14次之後,晉升至老年代。若Survivor區無法放下,或者超大物件的閾值超過上限,則嘗試在老年代中進行分配;如果老年代也無法放下,則會觸發Full Garbage Collection(Full GC);
如果依然無法放下,則拋OOM.

堆出現OOM的概率是所有記憶體耗盡異常中最高的。出錯時的堆內資訊對解決問題非常有幫助,所以給JVM設定執行引數-

XX:+HeapDumpOnOutOfMemoryError

讓JVM遇到OOM異常時能輸出堆內資訊。

方法區(Method Area)

儲存已被虛擬機器載入的類資訊、常量、靜態變數、即時編譯器編譯後的程式碼等資料。別名 Non-Heap。HotSpot 虛擬機器設計團隊選擇把 GC 分代收集擴充套件至方法區,導致也有人稱呼方法區為“永久代”(Permanent Generation),但這並不是一個好的實踐,會導致記憶體溢位問題,而且極少數的方法會因為這個原因而在不同虛擬機器上產生不同表現。

垃圾收集行為在方法區是較少出現的,而且回收率不高,回收目標主要是針對常量池的回收和對型別的解除安裝。

  • 執行緒共享
    方法區是堆的一個邏輯部分,因此和堆一樣,都是執行緒共享的.整個虛擬機器中只有一個方法區.
  • 永久代
    方法區中的資訊一般需要長期存在,而且它又是堆的邏輯分割槽,因此用堆的劃分方法,我們把方法區稱為永久代.
  • 記憶體回收效率低
    Java虛擬機器規範對方法區的要求比較寬鬆,可以不實現垃圾收集.
    方法區中的資訊一般需要長期存在,回收一遍記憶體之後可能只有少量資訊無效.
    對方法區的記憶體回收的主要目標是:對常量池的回收和對型別的解除安裝

和堆一樣,允許固定大小,也允許可擴充套件的大小,還允許不實現垃圾回收。

當方法區記憶體空間無法滿足記憶體分配需求時,將丟擲OutOfMemoryError異常.

對每個載入的型別(類class、介面interface、列舉enum、註解annotation),JVM必須在方法區中儲存以下型別資訊:

  • 型別的完整有效名稱(全名=包名.類名)
  • 型別直接父類的完整有效名稱( java.lang.Object除外,其他型別若沒有宣告父類,預設父類是Object)
  • 型別的修飾符(public、abstract、final的某個子集)
  • 型別直接介面的一個有序列表
  • 型別的常量池( constant pool)
  • 域(Field)資訊
  • 方法(Method)資訊
  • 除了常量外的所有靜態(static)變數

執行時常量池(Runtime Constant Pool)是方法區的一部分,Class 檔案除了有類的版本、欄位、方法、介面等描述資訊外,還有一項資訊是常量池(Constant Pool Table),用於存放編譯期間生成的各種字面量和符號引用。可能會丟擲 OOM 異常。

直接記憶體(Direct Memory)

直接記憶體不是 JVM 執行時資料區的一部分,JDK 1.4 中新加入了 NIO(New Input/Output)類,引入了一種基於通道(Channel)與緩衝區(Buffer)的 I/O 方式,可以使用 Native 函式庫直接分配堆外記憶體,然後通過一個儲存在 Java 堆的 DirectByteBuffer 物件作為這塊記憶體的引用進行操作。從而避免在 Java 堆和 Native 堆中來回複製護具,在一些場景中顯著提高效能。使用不當會有 OOM 異常。

  • 由於在 JDK 1.4 中引入了 NIO 機制,為此實現了一種通過 native 函式直接分配對外記憶體的,而這一切是通過以下兩個概念實現的:
    • 通道(Channel);
    • 緩衝區(Buffer);
  • 通過儲存在 Java 堆裡面的 DirectByteBuffer 物件對這塊記憶體的引用進行操作;
  • 因避免了 Java 堆和 Native 堆(native heap)中來回複製資料,所以在一些場景中顯著提高了效能;
  • 直接記憶體出現 OutOfMemoryError 異常的原因是物理機器的記憶體是受限的,但是我們通常會忘記需要為直接記憶體在物理機中預留相關記憶體空間;

直接記憶體的最大大小可以通過 -XX:MaxDirectMemorySize 來設定,預設是 64M。

 在 Java 中分配記憶體的方式一般是通過 sun.misc.Unsafe類的公共 native 方法實現的(比如 檔案以及網路 IO 類,但是非常不建議開發者使用,使用時一定要確保安全),而類 DirectByteBuffer 類的也是藉助於此向實體記憶體(比如 JVM 運行於 Linux 上,那麼 Linux 的記憶體就被稱為實體記憶體)。

 Unsafe 是位於 sun.misc 包下的一個類,主要提供一些用於執行低級別、不安全操作的方法,如直接訪問系統記憶體資源、自主管理記憶體資源等,這些方法在提升 Java 執行效率、增強 Java 語言底層資源操作能力方面起到了很大的作用。但由於 Unsafe 類使 Java 語言擁有了類似 C 語言指標一樣操作記憶體空間的能力,這無疑也增加了程式發生相關指標問題的風險。在程式中過度、不正確使用 Unsafe 類會使得程式出錯的概率變大,使得 Java 這種安全的語言變得不再“安全”,因此對 Unsafe 的使用一定要慎重。