要點提煉| 理解JVM之記憶體管理機制

釐米姑娘發表於2018-06-29

本系列專題的第二個板塊“理解JVM”是對周志明老師的《深入理解Java虛擬機器》著作的學習和擴充套件,也是在春招過程中發現自己Java基礎的不足,特意精選了幾個重要知識點進行總結。現在先從非常重要的記憶體管理開始吧~

本篇將瞭解JVM記憶體是如何劃分的,以及每個區域的具體內容。

  • 概述
  • JVM記憶體區域劃分
  • 作業系統記憶體與JVM記憶體
  • HotSpot虛擬機器記憶體物件探祕

1.概述

Java與C++之間有一堵由記憶體動態分配垃圾回收機制所圍成的高牆,牆外面的人想進去,牆裡面的人出不來。

必要性:雖然JVM有自動記憶體管理機制,不需要人為地給每一個new操作寫配對的delete/free程式碼,不容易出現記憶體洩漏和記憶體溢位問題。然而一旦出現記憶體洩漏和溢位方面的問題,如果不清楚JVM記憶體的記憶體管理機制,那麼將很難定位與解決問題。


2.JVM記憶體區域劃分

JVM執行Java程式的過程:Java原始碼檔案(.java)會被Java編譯器編譯為位元組碼檔案(.class),然後由JVM中的類載入器載入各個類的位元組碼檔案,載入完畢之後,交由JVM執行引擎執行。

在上述過程中,JVM會用一段空間來儲存執行程式期間需要用到的資料和相關資訊,這段空間就是執行時資料區(Runtime Data Area),也就是常說的JVM記憶體。JVM會將它所管理的記憶體劃分為若干個不同的資料區域,劃分結果如圖:

要點提煉| 理解JVM之記憶體管理機制

可見,執行時資料區被分為執行緒私有資料區執行緒共享資料區兩大類:

  • 執行緒私有資料區包含:程式計數器、虛擬機器棧、本地方法棧
  • 執行緒共享資料區包含:Java堆、方法區(內部包含常量池)

接下來分別介紹:

a.程式計數器(Program Counter Register)

  • 當前執行緒所執行的位元組碼的行號指示器。
    • 如果執行緒正在執行的是一個Java方法,那麼計數器記錄的是正在執行的虛擬機器位元組碼指令的地址
    • 如果執行緒正在執行的是一個Native方法,那麼計數器的值則為

位元組碼直譯器工作時,就是通過改變這個計數器的值來選取下一條需要執行的位元組碼指令,分支、迴圈、跳轉、異常處理、執行緒恢復等基礎功能都需要依賴這個計數器來完成。

  • 為了執行緒切換後能恢復到正確的執行位置,每條執行緒都需要有一個獨立的程式計數器,各條執行緒之間計數器互不影響,獨立儲存,因此它是執行緒私有的記憶體。
  • 在Java虛擬機器規範中,是唯一一個沒有規定任何OutOfMemoryError情況的區域。

b.Java虛擬機器棧(Java Virtual Machine Stacks)

  • Java方法執行的記憶體模型。
    • 每個方法在執行的同時都會建立一個棧幀,用於儲存區域性變數表、運算元棧、動態連結、方法出口等資訊。
    • 每一個方法從呼叫直至執行完成的過程,就對應著一個棧幀在虛擬機器棧中入棧到出棧的過程。

區域性變數表存放了編譯期可知的各種基本資料型別、物件引用型別和returnAddress型別,它所需的記憶體空間在編譯期間完成分配。

  • 是執行緒私有的記憶體,與執行緒生命週期相同。
  • 一般把Java記憶體區分為堆記憶體(Heap)和棧記憶體(Stack),其中『棧』指的是虛擬機器棧,『堆』指的是Java堆。
  • 在Java虛擬機器規範中,對這個區域規定了兩種異常狀況:
    • 如果執行緒請求的棧深度大於虛擬機器所允許的深度,將丟擲StackOverflowError異常;
    • 如果虛擬機器棧可動態擴充套件且擴充套件時無法申請到足夠的記憶體,將丟擲OutOfMemoryError異常。

c.本地方法棧(Native Method Stack)

  • 是虛擬機器使用到的Native方法服務。
  • 在虛擬機器規範中,對這個區域無強制規定,由具體的虛擬機器自由實現。與虛擬機器棧一樣,本地方法棧區域也會丟擲StackOverflowError和OutOfMemoryError異常。

d.Java堆(Java Heap)

  • 用於存放幾乎所有的物件例項和陣列。
  • 被所有執行緒共享的一塊記憶體區域,在虛擬機器啟動時建立。

在Java堆中,可能劃分出多個執行緒私有的分配緩衝區(Thread Local Allocation Buffer,TLAB),但無論哪個區域,儲存的都仍然是物件例項,進一步劃分的目的是為了更好地回收記憶體,或者更快地分配記憶體。

  • 是垃圾收集器管理的主要區域,也被稱做“GC堆”。
  • 是Java虛擬機器所管理的記憶體中最大的一塊。
  • 在Java虛擬機器規範中,如果在堆中沒有記憶體完成例項分配,且堆也無法再擴充套件時,將會丟擲OutOfMemoryError異常。

e.方法區(Method Area)

  • 用於儲存已被虛擬機器載入的類資訊、常量、靜態變數、即時編譯器編譯後的程式碼等資料。
  • 與Java堆一樣,是各個執行緒共享的記憶體區域。
  • 人們更願意把這個區域稱為**“永久代”(Permanent Generation),在釋出的JDK1.7的HotSpot中,已經把原本放在永久代的字串常量池移出。它還有個別名叫做Non-Heap(非堆)**。
  • 在Java虛擬機器規範中,當方法區無法滿足記憶體分配需求時,將丟擲OutOfMemoryError異常。

f.執行時常量池(Runtime Constant Pool)

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

  • 相對於Class檔案常量池的重要特徵是具備動態性,體現在並非只有預置入Class檔案中常量池的內容才能進入方法區執行時常量池,執行期間也可能將新的常量放入池中。
  • 是方法區的一部分,會受到方法區記憶體的限制。
  • 在Java虛擬機器規範中,當常量池無法再申請到記憶體時會丟擲OutOfMemoryError異常。

推薦閱讀JVM記憶體溢位詳解(棧溢位,堆溢位,持久代溢位、無法建立本地執行緒)


3.作業系統記憶體與JVM記憶體

要點提煉| 理解JVM之記憶體管理機制

從上圖可見作業系統記憶體和JVM記憶體的聯絡:

作業系統分為棧和堆:

  • 由作業系統管理,並由作業系統自動回收。
    • JVM本地方法棧使用的是作業系統的棧。
  • 由使用者分配使用。
    • 除JVM本地方法棧以外的JVM記憶體使用的作業系統的堆,以防JVM分配的記憶體被作業系統回收。

圖片來源JVM記憶體管理—執行時記憶體區域


4.HotSpot虛擬機器記憶體物件探祕

在熟悉虛擬機器記憶體劃分及其具體內容之後,為詳細瞭解虛擬機器記憶體中資料的其他細節,以常用的虛擬機器HotSpot和常用的記憶體區域Java堆為例,探討HotSpot虛擬機器在Java堆中物件分配、佈局和訪問的全過程。

a.物件的建立:遇到一個new指令後建立過程分三步

  • 類載入檢查:檢查new指令的引數是否能在常量池中定位到一個類的符號引用且該符號引用代表的類是否已被載入、解析和初始化,若沒有則需先執行相應的類載入,反之下一步。
  • 分配記憶體:由Java堆中的記憶體是否規整決定如何給新生物件分配可用空間。
    • 若規整,採用“指標碰撞”分配方式:
      • 過程:將用過和空閒的記憶體放在兩邊,中間以一個指標作為分界指示器。當分配記憶體時,就把指標向空閒一邊挪動與物件大小相等的距離即可。
      • 應用:Serial、ParNew等帶Compact過程的收集器。
    • 若非規整,採用“空閒列表”分配方式:
      • 過程:維護一個記錄可用記憶體塊的列表。當分配記憶體時,就從列表中找到一塊足夠大的空間劃分給物件例項並更新記錄。
      • 應用:基於Mark-Sweep演算法的CMS收集器。

保證記憶體分配是執行緒安全的解決方案:

  • 對記憶體分配的動作進行同步處理;
  • 每個執行緒在Java堆中預先分配一塊記憶體(本地執行緒分配緩衝TLAB),在本執行緒的TLAB上進行分配,當TLAB用完需要分配新的TLAB時再同步鎖定。
  • 設定物件頭:將物件的所屬類、找到類的後設資料資訊的方式、物件的雜湊碼、物件的GC分代年齡等資訊存放在物件的物件頭中。

經過上述步驟,一個物件就產生了,但此時所有的欄位都還為零,還需要執行<init>方法進行初始化,才能成為真正可用的物件。

b.物件的記憶體佈局:分為三塊區域

  • 物件頭(Header):包括兩部分資訊
    • Mark Word:用於儲存物件自身的執行時資料,如雜湊碼、GC分代年齡、鎖狀態標誌、執行緒持有的鎖、偏向執行緒ID、偏向時間戳等。
    • 型別指標:用於確定這個物件的所屬類。
  • 例項資料(Instance Data):儲存真正的有效資訊,是程式程式碼中定義的各種型別的欄位內容。儲存順序會受虛擬機器分配策略引數和欄位在Java原始碼中定義順序這兩個因素影響。
  • 對齊填充(Padding):佔位符,幫助補全未對齊的物件例項資料部分(保證是8位元組的倍數),非必需。

c.物件的訪問定位:主流的兩種訪問方式

  • 通過控制程式碼訪問物件:在Java堆中劃分出一塊記憶體來作為控制程式碼池,reference儲存的是物件的控制程式碼地址,在控制程式碼中包含了物件例項資料與型別資料各自的具體地址資訊。好處:reference中儲存的是穩定的控制程式碼地址,在物件被移動時只會改變控制程式碼中的例項資料指標,而reference本身不需要修改。
    要點提煉| 理解JVM之記憶體管理機制
  • 通過直接指標訪問物件:在Java堆物件的佈局中考慮如何放置訪問型別資料的相關資訊,reference儲存的直接就是物件地址。好處:速度更快,節省了一次指標定位的時間開銷。
    要點提煉| 理解JVM之記憶體管理機制

下篇將介紹和記憶體管理緊密相關的垃圾回收機制。

相關文章