華爲技術專家萬字講清JVM內存模型(建議收藏!)

內存是很是重要的系統資源,是硬盤和CPU的中間倉庫及橋樑,承載着操做系統和應用程序的實時運行。 JVM內存佈局規定了Java在運行過程當中內存申請、分配、管理的策略,保證了JVM的高效穩定運行 不一樣的JVM對於內存的劃分方式和管理機制存在着部分差別 結合JVM虛擬機規範,來探討經典的JVM內存佈局java

  • JVM運行時數據區

  • 線程獨佔:每一個線程都會有它獨立的空間,隨線程生命週期而建立和銷燬
  • 線程共享:全部線程能訪問這塊內存數據,隨虛擬機或者GC而建立和銷燬

JVM內存模型-2

1 Program Counter Register (程序計數寄存器)

Register 的命名源於CPU的寄存器,CPU只有把數據裝載到寄存器纔可以運行 寄存器存儲指令相關的現場信息,因爲CPU時間片輪限制,衆多線程在併發執行過程當中,任何一個肯定的時刻,一個處理器或者多核處理器中的一個內核,只會執行某個線程中的一條指令。這樣必然致使常常中斷或恢復,如何保證分毫無差呢? 每一個線程在建立後,都會產生本身的程序計數器和棧幀,程序計數器用來存放執行指令的偏移量和行號指示器等,線程執行或恢復都要依賴程序計數器。程序計數器在各個線程之間互不影響,此區域也不會發生內存溢出異常。git

1.1. 定義

程序計數器是一塊較小的內存空間,可看做當前線程正在執行的字節碼的行號指示器 若是當前線程正在執行的是程序員

  • Java方法

計數器記錄的就是當前線程正在執行的字節碼指令的地址github

  • 本地方法

那麼程序計數器值爲undefined面試

1.2. 做用

程序計數器有兩個做用數組

  • 字節碼解釋器經過改變程序計數器來依次讀取指令,從而實現代碼的流程控制,如:順序執行、選擇、循環、異常處理。
  • 在多線程的狀況下,程序計數器用於記錄當前線程執行的位置,從而當線程被切換回來的時候可以知道該線程上次運行到哪兒了。

1.3. 特色

一塊較小的內存空間 線程私有。每條線程都有一個獨立的程序計數器。 是惟一一個不會出現OOM的內存區域。 生命週期隨着線程的建立而建立,隨着線程的結束而死亡。bash

2. Java虛擬機棧(JVM Stack)

2.1. 定義

相對於基於寄存器的運行環境,JVM是基於棧結構的運行環境 。棧結構移植性更好,可控性更強。服務器

JVM中的虛擬機棧是描述Java方法執行的內存區域,屬線程私有。markdown

棧中的元素用於支持虛擬機進行方法調用,每一個方法從開始調用到執行完成的過程,就是棧幀從入棧到出棧的過程。多線程

2.2 結構

棧幀是方法運行的基本結構。

  • 在活動線程中,只有位於棧頂的幀纔是有效的,稱爲當前棧幀
  • 正在執行的方法稱爲當前方法

在執行引擎運行時,全部指令都只能針對當前棧幀操做, StackOverflowError表示請求的棧溢出,致使內存耗盡,一般出如今遞歸方法。 JVM可以橫掃千軍,虛擬機棧就是它的心腹大將,當前方法的棧幀,都是正在戰鬥的戰場,其中的操做棧是參與戰鬥的士兵 操做棧的壓棧與出棧

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

棧幀在整個JVM體系中的地位頗高,包括:局部變量表、操做棧、動態鏈接、方法返回地址等。

局部變量表

存放方法參數和局部變量。 相對於類屬性變量的準備階段和初始化階段,局部變量沒有準備階段,必須顯式初始化。 若是是非靜態方法,則在index[0]位置上存儲的是方法所屬對象的實例引用,隨後存儲的是參數和局部變量。 字節碼指令中的STORE指令就是將操做棧中計算完成的局部變量寫回局部變量表的存儲空間內。

操做數棧

一個初始狀態爲空的桶式結構棧。因爲 Java 沒有寄存器,全部參數傳遞使用操做數棧。在方法執行過程當中,會有各類指令往棧中寫入和提取信息。JVM的執行引擎是基於棧的執行引擎,其中的棧指的就是操做棧。 字節碼指令集的定義都是基於棧類型的,棧的深度在方法元信息的stack屬性中。

操做棧與局部變量表交互

  • 詳細的字節碼操做順序以下:

第1處說明:局部變量表就像箇中藥櫃,裏面有不少抽屜,依次編號爲0, 1, 2,3,.,. n 字節碼指令istore_ 1就是打開1號抽屜,把棧頂中的數13存進去 棧是一個很深的豎桶,任什麼時候候只能對桶口元素進行操做,因此數據只能在棧頂進行存取

某些指令能夠直接在抽屜裏進行,好比inc指令,直接對抽屜裏的數值進行+1操做 程序員面試過程當中,常見的i++和++i的區別,能夠從字節碼上對比出來 i++和++i的區別

  • iload_ 1 從局部變量表的第1號抽屜裏取出一個數,壓入棧頂,下一步直接在抽屜裏實現+1的操做,而這個操做對棧頂元素的值沒有影響

因此istore_ 2只是把棧頂元素賦值給a

  • 表格右列,先在第1號抽屜裏執行+1操做,而後經過iload_ 1 把第1號抽屜裏的數壓入棧頂,因此istore_ 2存入的是+1以後的值

i++並不是原子操做。即便經過volatile關鍵字進行修飾,多個線程同時寫的話,也會產生數據互相覆蓋的問題。

動態鏈接

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

方法返回地址

方法執行時有兩種退出狀況:

  • 正常退出

正常執行到任何方法的返回字節碼指令,如RETURN、IRETURN、ARETURN等。

  • 異常退出

不管何種,都將返回至方法當前被調用的位置。方法退出的過程至關於彈出當前棧幀。

退出可能有三種方式:

  • 返回值壓入,上層調用棧幀
  • 異常信息拋給可以處理的棧幀
  • PC計數器指向方法調用後的下一條指令

Java虛擬機棧是描述Java方法運行過程的內存模型。Java虛擬機棧會爲每個即將運行的Java方法建立「棧幀」。用於存儲該方法在運行過程當中所須要的一些信息。

  • 局部變量表

存放基本數據類型變量、引用類型的變量、returnAddress類型的變量

  • 操做數棧
  • 動態連接
  • 當前方法的常量池指針
  • 當前方法的返回地址
  • 方法出口等信息

每個方法從被調用到執行完成的過程,都對應着一個個棧幀在JVM棧中的入棧和出棧過程

注意:人們常說,Java的內存空間分爲「棧」和「堆」,棧中存放局部變量,堆中存放對象。 這句話不徹底正確!這裏的「堆」能夠這麼理解,但這裏的「棧」就是如今講的虛擬機棧,或者說Java虛擬機棧中的局部變量表部分. 真正的Java虛擬機棧是由一個個棧幀組成,而每一個棧幀中都擁有:局部變量表、操做數棧、動態連接、方法出口信息.

特色

局部變量表的建立是在方法被執行的時候,隨棧幀建立而建立。 表的大小在編譯期就肯定,在建立的時候只需分配事先規定好的大小便可。在方法運行過程當中,表的大小不會改變。Java虛擬機棧會出現兩種異常:

  • StackOverFlowError

若Java虛擬機棧的內存大小不容許動態擴展,那麼當線程請求的棧深度大於虛擬機容許的最大深度時(但內存空間可能還有不少),就拋出此異常 棧內存默認最大是1M,超出則拋出StackOverflowError

  • OutOfMemoryError

若Java虛擬機棧的內存大小容許動態擴展,且當線程請求棧時內存用完了,沒法再動態擴展了,此時拋出OutOfMemoryError異常

Java虛擬機棧也是線程私有的,每一個線程都有各自的Java虛擬機棧,並且隨着線程的建立而建立,隨線程的死亡而死亡。

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

和虛擬機棧功能相似,虛擬機棧是爲虛擬機執行JAVA方法而準備的 虛擬機規範沒有規定具體的實現,由不一樣的虛擬機廠商去實現。 HotSpot虛擬機中虛擬機棧和本地方法棧的實現式同樣的。一樣,超出大小之後 也會拋出StackOverflowError.

本地方法棧和Java虛擬機棧實現的功能與拋出異常幾乎相同 只不過虛擬機棧是爲虛擬機執行Java方法(也就是字節碼)服務,本地方法區則爲虛擬機使用到的Native方法服務.

在JVM內存佈局中,也是線程對象私有的,可是虛擬機棧「主內」,而本地方法棧「主外」 這個「內外」是針對JVM來講的,本地方法棧爲Native方法服務 線程開始調用本地方法時,會進入一個再也不受JVM約束的世界 本地方法能夠經過JNI(Java Native Interface)來訪問虛擬機運行時的數據區,甚至能夠調用寄存器,具備和JVM相同的能力和權限 當大量本地方法出現時,勢必會削弱JVM對系統的控制力,由於它的出錯信息都比較黑盒. 對於內存不足的狀況,本地方法棧仍是會拋出native heap OutOfMemory

最著名的本地方法應該是System.currentTimeMillis(),JNI 使Java深度使用OS的特性功能,複用非Java代碼 可是在項目過程當中,若是大量使用其餘語言來實現JNI,就會喪失跨平臺特性,威脅到程序運行的穩定性 假如須要與本地代碼交互,就能夠用中間標準框架進行解耦,這樣即便本地方法崩潰也不至於影響到JVM的穩定 固然,若是要求極高的執行效率、偏底層的跨進程操做等,能夠考慮設計爲JNI調用方式

4 Java堆(Java Heap)

JVM啓動時建立,存放對象的實例。垃圾回收器主要就是管理堆內存。 Heap是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次以後,晉升至老年代 對象分配與簡要GC流程圖

Survivor區沒法放下,或者超大對象的閾值超過上限,則嘗試在老年代中進行分配; 若是老年代也沒法放下,則會觸發Full Garbage Collection(Full GC); 若是依然沒法放下,則拋OOM.

堆出現OOM的機率是全部內存耗盡異常中最高的 出錯時的堆內信息對解決問題很是有幫助,因此給JVM設置運行參數-

XX:+HeapDumpOnOutOfMemoryError
複製代碼

讓JVM遇到OOM異常時能輸出堆內信息

在不一樣的JVM實現及不一樣的回收機制中,堆內存的劃分方式是不同的

存放全部的類實例及數組對象 除了實例數據,還保存了對象的其餘信息,如Mark Word(存儲對象哈希碼,GC標誌,GC年齡,同步鎖等信息),Klass Pointy(指向存儲類型元數據的指針)及一些字節對齊補白的填充數據(若實例數據恰好知足8字節對齊,則可不存在補白)

特色

Java虛擬機所須要管理的內存中最大的一塊.

堆內存物理上不必定要連續,只須要邏輯上連續便可,就像磁盤空間同樣. 堆是垃圾回收的主要區域,因此也被稱爲GC堆.

堆的大小既能夠固定也能夠擴展,但主流的虛擬機堆的大小是可擴展的(經過-Xmx和-Xms控制),所以當線程請求分配內存,但堆已滿,且內存已滿沒法再擴展時,就拋出OutOfMemoryError.

線程共享 整個Java虛擬機只有一個堆,全部的線程都訪問同一個堆. 它是被全部線程共享的一塊內存區域,在虛擬機啓動時建立. 而程序計數器、Java虛擬機棧、本地方法棧都是一個線程對應一個

5 方法區

5.1 定義

Java虛擬機規範中定義方法區是堆的一個邏輯區劃部分,具體實現根據不一樣虛擬機來實現,如: HotSpot在Java7中方法區放在永久代,Java8放在元數據空間,而且經過GC機制對這個區域進行管理 別名Non-Heap(非堆),以與Java堆區分. 方法區中存放已經被虛擬機加載的類信息、常量、靜態變量、即時編譯器(JIT)編譯後的代碼等數據.

5.2 特色

  • 線程共享

方法區是堆的一個邏輯部分,所以和堆同樣,都是線程共享的.整個虛擬機中只有一個方法區.

  • 永久代

方法區中的信息通常須要長期存在,並且它又是堆的邏輯分區,所以用堆的劃分方法,咱們把方法區稱爲永久代.

  • 內存回收效率低

Java虛擬機規範對方法區的要求比較寬鬆,能夠不實現垃圾收集. 方法區中的信息通常須要長期存在,回收一遍內存以後可能只有少許信息無效. 對方法區的內存回收的主要目標是:對常量池的回收和對類型的卸載

和堆同樣,容許固定大小,也容許可擴展的大小,還容許不實現垃圾回收。

當方法區內存空間沒法知足內存分配需求時,將拋出OutOfMemoryError異常.

5.3 運行時常量池(Runtime Constant Pool)

5.3.1 定義

運行時常量池是方法區的一部分. 方法區中存放三種數據:類信息、常量、靜態變量、即時編譯器編譯後的代碼.其中常量存儲在運行時常量池中.

咱們知道,.java文件被編譯以後生成的.class文件中除了包含:類的版本、字段、方法、接口等信息外,還有一項就是常量池 常量池中存放編譯時期產生的各類字面量和符號引用,.class文件中的常量池中的全部的內容在類被加載後存放到方法區的運行時常量池中。

//age是一個變量,能夠被賦值;21就是一個字面值常量,不能被賦值;
PS:int age = 21; 
//pai就是一個符號常量,一旦被賦值以後就不能被修改。
int final pai = 3.14;
複製代碼

Class文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項信息是常量池( Constant pool table),用於存放編譯期生成的各類字面量和符號引用,這部份內容將在類加載後進入運行時常量池中存放。運行時常量池相對於class文件常量池的另一個特性是具有動態性,java語言並不要求常量必定只有編譯器才產生,也就是並不是預置入class文件中常量池的內容才能進入方法區運行時常量池,運行期間也可能將新的常量放入池中。

在近三個JDK版本(六、七、8)中, 運行時常量池的所處區域一直在不斷的變化, 在JDK6時它是方法區的一部分 7又把他放到了堆內存中 8以後出現了元空間,它又回到了方法區。 其實,這也說明了官方對「永久代」的優化從7就已經開始了

5.3.2 特性

class文件中的常量池具備動態性. Java並不要求常量只能在編譯時候產生,Java容許在運行期間將新的常量放入方法區的運行時常量池中. String類中的intern()方法就是採用了運行時常量池的動態性.當調用 intern 方法時,若是池已經包含一個等於此 String 對象的字符串,則返回池中的字符串.不然,將此 String 對象添加到池中,並返回此 String 對象的引用.

5.3.3 可能拋出的異常

運行時常量池是方法區的一部分,因此會受到方法區內存的限制,所以當常量池沒法再申請到內存時就會拋出OutOfMemoryError異常.

咱們通常在一個類中經過public static final來聲明一個常量。這個類被編譯後便生成Class文件,這個類的全部信息都存儲在這個class文件中。

當這個類被Java虛擬機加載後,class文件中的常量就存放在方法區的運行時常量池中。並且在運行期間,能夠向常量池中添加新的常量。如:String類的intern()方法就能在運行期間向常量池中添加字符串常量。

當運行時常量池中的某些常量沒有被對象引用,同時也沒有被變量引用,那麼就須要垃圾收集器回收。

6 直接內存(Direct Memory)

直接內存不是虛擬機運行時數據區的一部分,也不是JVM規範中定義的內存區域,但在JVM的實際運行過程當中會頻繁地使用這塊區域.並且也會拋OOM

在JDK 1.4中加入了NIO(New Input/Output)類,引入了一種基於管道和緩衝區的IO方式,它可使用Native函數庫直接分配堆外內存,而後經過一個存儲在堆裏的DirectByteBuffer對象做爲這塊內存的引用來操做堆外內存中的數據. 這樣能在一些場景中顯著提高性能,由於避免了在Java堆和Native堆中來回複製數據.

綜上看來

程序計數器、Java虛擬機棧、本地方法棧是線程私有的,即每一個線程都擁有各自的程序計數器、Java虛擬機棧、本地方法區。而且他們的生命週期和所屬的線程同樣。 而堆、方法區是線程共享的,在Java虛擬機中只有一個堆、一個方法棧。並在JVM啓動的時候就建立,JVM中止才銷燬。

7 Metaspace (元空間)

在JDK8,元空間的前身Perm區已經被淘汰,在JDK7及以前的版本中,只有Hotspot纔有Perm區(永久代),它在啓動時固定大小,很難進行調優,而且Full GC時會移動類元信息。

在某些場景下,若是動態加載類過多,容易產生Perm區的OOM。好比某個實際Web工程中,由於功能點比較多,在運行過程當中,要不斷動態加載不少的類,常常出現致命錯誤:

Exception in thread ‘dubbo client x.x connector' java.lang.OutOfMemoryError: PermGenspac 複製代碼

爲解決該問題,須要設定運行參數

-XX:MaxPermSize= l280m
複製代碼

若是部署到新機器上,每每會由於JVM參數沒有修改致使故障再現。不熟悉此應用的人排查問題時每每苦不堪言,除此以外,永久代在GC過程當中還存在諸多問題

因此,JDK8使用元空間替換永久代.區別於永久代,元空間在本地內存中分配. 也就是說,只要本地內存足夠,它不會出現像永久代中java.lang.OutOfMemoryError: PermGen space

一樣的,對永久代的設置參數 PermSizeMaxPermSize也會失效 在JDK8及以上版本中,設定MaxPermSize參數,JVM在啓動時並不會報錯,可是會提示:

Java HotSpot 64Bit Server VM warning:ignoring option MaxPermSize=2560m; support was removed in 8.0
複製代碼

默認狀況下,「元空間」的大小能夠動態調整,或者使用新參數MaxMetaspaceSize 來限制本地內存分配給類元數據的大小.

在JDK8裏,Perm 區全部內容中

  • 字符串常量移至堆內存
  • 其餘內容包括類元信息、字段、靜態屬性、方法、常量等都移動至元空間

好比上圖中的Object類元信息、靜態屬性System.out、整型常量000000等 圖中顯示在常量池中的String,其實際對象是被保存在堆內存中的。

元空間特點

  • 充分利用了Java語言規範:類及相關的元數據的生命週期與類加載器的一致
  • 每一個類加載器都有它的內存區域-元空間
  • 只進行線性分配
  • 不會單獨回收某個類(除了重定義類 RedefineClasses 或類加載失敗)
  • 沒有GC掃描或壓縮
  • 元空間裏的對象不會被轉移
  • 若是GC發現某個類加載器再也不存活,會對整個元空間進行集體回收

GC

  • Full GC時,指向元數據指針都不用再掃描,減小了Full GC的時間
  • 不少複雜的元數據掃描的代碼(尤爲是CMS裏面的那些)都刪除了
  • 元空間只有少許的指針指向Java堆

這包括:類的元數據中指向java.lang.Class實例的指針;數組類的元數據中,指向java.lang.Class集合的指針。

  • 沒有元數據壓縮的開銷
  • 減小了GC Root的掃描(再也不掃描虛擬機裏面的已加載類的目錄和其它的內部哈希表)
  • G1回收器中,併發標記階段完成後就能夠進行類的卸載

元空間內存分配模型

  • 絕大多數的類元數據的空間都在本地內存中分配
  • 用來描述類元數據的對象也被移除
  • 爲元數據分配了多個映射的虛擬內存空間
  • 爲每一個類加載器分配一個內存塊列表
    • 塊的大小取決於類加載器的類型
    • Java反射的字節碼存取器(sun.reflect.DelegatingClassLoader )佔用內存更小
  • 空閒塊內存返還給塊內存列表
  • 當元空間爲空,虛擬內存空間會被回收
  • 減小了內存碎片

最後,從線程共享的角度來看

  • 堆和元空間是全部線程共享的
  • 虛擬機棧、本地方法棧、程序計數器是線程內部私有的

從這個角度看一下Java內存結構 Java 的線程與內存

8 從GC角度看Java堆

堆和方法區都是線程共享的區域,主要用來存放對象的相關信息。咱們知道,一個接口中的多個實現類須要的內存可能不同,一個方法中的多個分支須要的內存也可能不同,咱們只有在程序運行期間才能知道會建立哪些對象,所以, 這部分的內存和回收都是動態的,垃圾收集器所關注的就是這部份內存(本節後續所說的「內存」分配與回收也僅指這部份內存)。而在JDK1.7和1.8對這部份內存的分配也有所不一樣,下面咱們來詳細看一下

Java8中堆內存分配以下圖:

9 JVM關閉

  • 正常關閉:當最後一個非守護線程結束或調用了System.exit或經過其餘特定於平臺的方式,好比ctrl+c。
  • 強制關閉:調用Runtime.halt方法,或在操做系統中直接kill(發送single信號)掉JVM進程。
  • 異常關閉:運行中遇到RuntimeException 異常等

在某些狀況下,咱們須要在JVM關閉時作一些掃尾的工做,好比刪除臨時文件、中止日誌服務。爲此JVM提供了關閉鉤子(shutdown hocks)來作這些事件。

Runtime類封裝java應用運行時的環境,每一個java應用程序都有一個Runtime類實例,使用程序能與其運行環境相連。

關閉鉤子本質上是一個線程(也稱爲hock線程),能夠經過Runtime的addshutdownhock (Thread hock)向主jvm註冊一個關閉鉤子。hock線程在jvm正常關閉時執行,強制關閉不執行。

對於在jvm中註冊的多個關閉鉤子,他們會併發執行,jvm並不能保證他們的執行順序。

參考

  • 《碼出高效》
相關文章
相關標籤/搜索