Java記憶體區域總結(堆、棧、方法區等)

ens發表於2018-12-16

1. JVM 執行時資料區

Java記憶體區域總結(堆、棧、方法區等)

1. 程式計數器

  • 程式計數器(Program Counter Register)是一塊較小的記憶體空間,它可以看做是當前執行緒所執行的位元組碼的行號指示器。位元組碼直譯器工作時就是通過改變這個計數器的值來選取下一條需要執行的位元組碼指令。

  • 位元組碼指令、分支、迴圈、跳轉、異常處理、執行緒恢復等基礎功能都要依賴這個計數器來完成。

  • 每條執行緒都有一個獨立的程式計數器,各條執行緒之間計數器互不影響,獨立儲存。如上圖所示,我們稱這類記憶體區域為 : 執行緒私有記憶體。

  • 如果執行緒正在執行的是一個 Java 方法,這個計數器記錄的是正在執行的虛擬機器位元組碼指令的地址;如果正在執行的是 Native 方法,這個計數器值則為空(Undefined)。

  • 此記憶體區域是唯一一個在 Java 虛擬機器中沒有規範任何 OutOfMemoryError 情況的區域。

2. Java 虛擬機器棧

  • Java 虛擬機器棧也是執行緒私有的,它的生命週期與執行緒相同(隨執行緒而生,隨執行緒而滅)

  • 如果執行緒請求的棧深度大於虛擬機器所允許的深度,將丟擲StackOverflowError異常;

  • 如果虛擬機器棧可以動態擴充套件,如果擴充套件時無法申請到足夠的記憶體,就會丟擲OutOfMemoryError異常;(當前大部分 JVM 都可以動態擴充套件,只不過 JVM 規範也允許固定長度的虛擬機器棧)

  • Java 虛擬機器棧描述的是 Java 方法執行的記憶體模型:每個方法執行的同時會建立一個棧幀。 對於我們來說,主要關注的 stack 棧記憶體,就是虛擬機器棧中區域性變數表部分。

    區域性變數表

  • 定義
    區域性變數表(Local Variable Table)是一組變數值儲存空間,用於存放方法引數方法內部定義的區域性變數

  • 編譯器確定容量
    在Java程式編譯為class檔案時,就在方法的Code屬性的 max_locals 資料項中確定了 該方法所需要分配的區域性變數表的最大容量。

  • 最小單位為變數槽(Slot)
    一個Slot 可以存放一個32位以內的資料型別,包括基本資料型別 (boolean、byte、char、short、int、float、long、double)「String 是引用型別」,物件引用 (reference 型別) 和 returnAddress 型別(它指向了一條位元組碼指令的地址)。

3. 本地方法棧

  • 與JVM棧區別
    本 地方法棧(Native Method Stack)與虛擬機器棧所發揮的作用是非常相似的,它們之間的區別不過是虛擬機器棧為虛擬機器執行 Java 方法(也就是位元組碼)服務,而本地方法棧為虛擬機器使用到的 Native 方法服務。

  • 自由實現
    Java 虛擬機器規範對本地方法棧使用的語言、使用方法與資料結構並沒有強制規定,因此可以由虛擬機器自由實現。例如:HotSpot 虛擬機器直接將本地方法棧和虛擬機器棧合二為一。

  • 異常
    同虛擬機器棧相同,Java 虛擬機器規範對這個區域也規定了兩種異常情況StackOverflowError 和 OutOfMemoryError異常。

4. 堆

  • 對於大多數應用來說,Java 堆 (Java Heap) 是 JVM所管理的記憶體中最大的一塊。

  • Java 堆是被所有執行緒共享的一塊記憶體區域,在虛擬機器啟動時建立。

  • 此記憶體區域的唯一目的就是存放物件例項,幾乎所有的物件例項都在這裡分配記憶體。

  • 陣列引用變數是存放在記憶體中,陣列元素是存放在記憶體中。

  • Java 堆是垃圾收集器管理的主要區域,因此很多時候也被稱作為 "GC 堆"。

  • 從記憶體回收的角度看,Java 堆中還可以細分為: 新生代老年代

  • 程式新建立的物件都是從新生代分配記憶體,新生代由 Eden Space 和兩塊相同大小的 Survivor Space(通常又稱 S0 和 S1 或 From 和 To) 構成。
    詳見JVM常見引數設定

  • 從記憶體分配角度,執行緒共享的 Java 堆可能劃分出多個執行緒私有的分配緩衝區(TLAB)。

  • Java 堆可以處於物理不連續的記憶體空間中,只要邏輯是連續的即可,就像我們的磁碟空間一樣。

  • 在實現時,即可以實現成固定大小的,也可以是可擴充套件的,不過當前主流的虛擬機器都是按照可擴充套件來實現的 (通過 -Xmx 和 -Xms 控制)。

  • 如果堆上沒有記憶體完成例項分配,並且堆也無法再擴充套件時,將會丟擲 OutOfMemoryError異常。

5. 方法區

  • 方法區 (Method Area) 與 Java 堆一樣, 是各個執行緒共享的記憶體區域。

  • 它用於儲存已經被虛擬機器載入的類資訊常量靜態變數即時編譯器編譯後的程式碼等資料

  • 執行時常量池 (Runtime Constant Pool) 是方法區的一部分。

  • 雖然 JVM規範把方法區描述為堆的一個邏輯部分, 但是它卻又一個別名叫做 Non-Heap(非堆), 目的應該是與 Java 堆區分開來.

  • 方法區 和 永久代(Permanent Generation), 本質上兩者並不相等。

    僅僅是因為 HotSpot 虛擬機器的設計團隊選擇把 GC 分代收集擴充套件至方法區, 或者說使用永久代來實現方法區而已。

    這樣 HotSpot 的垃圾收集器可以像管理 Java 堆一樣管理這部分內容, 能夠省去專門為方法區編寫記憶體管理程式碼的工作。

    因此, 對於 HotSpot 虛擬機器, 根據官方釋出的路線圖資訊, 現在也有放棄永久代並逐步採用 Native Memory 來實現方法區的規劃了, 在目前已經發布的 JDK1.7 的 HotSpot 中, 已經把原本放在永久代的字串常量池移出

  • JVM規範對方法區的限制非常寬鬆

    和堆一樣, 允許固定大小, 也允許可擴充套件的大小, 還可以選擇不實現垃圾回收。 相對而言, 垃圾收集行為在這個區域是比較少出現的, 但是並非資料進入了方法區就如同進入永久代的名字一樣” 永久” 存在了。

    這區域的記憶體回收目標主要是針對常量池的回收和對型別的解除安裝, 一般來說, 這個區域的回收” 成績” 比較難以令人滿意, 尤其是對型別的解除安裝, 條件相當苛刻, 但是這部分割槽域的回收確實是存在必要的。

    在 Sun 公司的 BUG 列表裡, 曾出現過的若干個嚴重的 BUG 就是由於低版本的 HotSpot 虛擬機器對此區域未完全回收而導致記憶體洩漏

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

    記憶體洩露和記憶體溢位

    記憶體洩露: 指程式中動態分配記憶體給一些臨時物件,但是物件不會被 GC 所回收,它始終佔用記憶體。即被分配的物件可達但已無用,可用記憶體越來越少。

    記憶體溢位: 指程式執行過程中無法申請到足夠的記憶體而導致的一種錯誤。記憶體溢位通常發生於老年代或永久代垃圾回收後,仍然無記憶體空間容納新的 Java 物件的情況。

    記憶體洩露是記憶體溢位的一種誘因,不是唯一因素。

    執行時常量池

  • Class 檔案中除了有類的版本、欄位、方法、介面等描述資訊外,還有一項資訊是常量池 (Constant Pool Table),用於存放編譯期生成的字面量和符號引用,這部分內容(也可以稱為 .Class 檔案中的靜態常量池)將在類載入後進入方法區的執行時常量池中存放。

    字面量
    比較接近 Java 語言層面的常量概念,如文字字串、宣告為 final 的常量值等。(final 修飾的成員變數和類變數【類變數:靜態成員變數】)

    符號引用
    符號引用就是字串,這個字串包含足夠的資訊,以供實際使用時可以找到相應的位置。
    你比如說某個方法的符號引用,如:“java/io/PrintStream.println:(Ljava/lang/String;)V”。裡面有類的資訊,方法名,方法引數等資訊。
    當第一次執行時,要根據字串的內容,到該類的方法表中搜尋這個方法。
    執行一次之後,符號引用會被替換為直接引用,下次就不用搜尋了。
    直接引用就是偏移量,通過偏移量虛擬機器可以直接在該類的記憶體區域中找到方法位元組碼的起始位置。

  • 除了儲存 Class 檔案中描述的符號引用外,還會把編譯出來的直接引用也儲存在執行時常量池中。

  • Java 語言並不要求常量一定只有編譯期才能產生,也就是並非置入 Class 檔案中常量池的內容才能進入方法區執行時常量池,執行期間也可能將新的常量放入池中,這種特性被開發人員利用得比較多的便是 String 類的 intern() 方法。

  • 執行時常量池受方法區記憶體的限制,當常量池無法再申請到記憶體時也會丟擲OutofMemoryError異常。

6. 變數總結

Java記憶體區域總結(堆、棧、方法區等)


參考來源:
周志明 《深入理解Java虛擬機器》
Java 記憶體區域——堆,棧,方法區等
知乎網友Intopass分享

相關文章