图文并茂,傻瓜都能看懂的 JVM 内存布局
本 JVM 系列屬于本人學(xué)習(xí)過程當(dāng)中總結(jié)的一些知識點,目的是想讓讀者更快地掌握 JVM 相關(guān)的知識要點,難免會有所側(cè)重,若想要更加系統(tǒng)更加詳細(xì)的學(xué)習(xí) JVM 知識,還是需要去閱讀專業(yè)的書籍和文檔。
本文主題內(nèi)容:
-
JVM 內(nèi)存區(qū)域概覽
-
堆區(qū)的空間分配是怎么樣?堆溢出的演示
-
創(chuàng)建一個新對象內(nèi)存是怎么分配的?
-
方法區(qū) 到 Metaspace 元空間
-
棧幀是什么?棧幀里有什么?怎么理解?
-
本地方法棧
-
程序計數(shù)器
-
Code Cache 是什么?
注:請區(qū)分 JVM 內(nèi)存結(jié)構(gòu)(內(nèi)存布局)和 JMM(Java 內(nèi)存模型)這兩個不同的概念!
概念
內(nèi)存是非常重要的系統(tǒng)資源,是硬盤和 CPU 的中間倉庫及橋梁,承載著操作系統(tǒng)和應(yīng)用程序的實時運行。JVM 內(nèi)存布局規(guī)定了 Java 在運行過程中內(nèi)存申請、分配、管理的策略,保證了 JVM 的高效穩(wěn)定運行。
上圖描述了當(dāng)前比較經(jīng)典的 JVM 內(nèi)存布局。(堆區(qū)畫小了 2333,按理來說應(yīng)該是最大的區(qū)域)
如果按照線程是否共享來分類的話,如下圖所示:
PS:線程是否共享這點,實際上理解了每塊區(qū)域的實際用處之后,就很自然而然的就記住了。不需要死記硬背。
下面讓我們來了解下各個區(qū)域。
Heap (堆區(qū))
1.?堆區(qū)的介紹
我們先來說堆。堆是 OOM 故障最主要的發(fā)生區(qū)域。它是內(nèi)存區(qū)域中最大的一塊區(qū)域,被所有線程共享,存儲著幾乎所有的實例對象、數(shù)組。所有的對象實例以及數(shù)組都要在堆上分配,但是隨著 JIT 編譯器的發(fā)展與逃逸分析技術(shù)逐漸成熟,棧上分配、標(biāo)量替換優(yōu)化技術(shù)將會導(dǎo)致一些微妙的變化發(fā)生,所有的對象都分配在堆上也漸漸變得不是那么“絕對”了。
延伸知識點:JIT 編譯優(yōu)化中的一部分內(nèi)容 -?逃逸分析。
推薦閱讀:面試問我 Java 逃逸分析,瞬間被秒殺了。
Java 堆是垃圾收集器管理的主要區(qū)域,因此很多時候也被稱做“GC 堆”。從內(nèi)存回收的角度來看,由于現(xiàn)在收集器基本都采用分代收集算法,所以 Java 堆中還可以細(xì)分為:新生代和老年代。再細(xì)致一點的有 Eden 空間、From Survivor 空間、To Survivor 空間等。從內(nèi)存分配的角度來看,線程共享的 Java 堆中可能劃分出多個線程私有的分配緩沖區(qū)(Thread Local Allocation Buffer,TLAB)。不過無論如何劃分,都與存放內(nèi)容無關(guān),無論哪個區(qū)域,存儲的都仍然是對象實例,進(jìn)一步劃分的目的是為了更好地回收內(nèi)存,或者更快地分配內(nèi)存。
2.?堆區(qū)的調(diào)整
根據(jù) Java 虛擬機(jī)規(guī)范的規(guī)定,Java 堆可以處于物理上不連續(xù)的內(nèi)存空間中,只要邏輯上是連續(xù)的即可,就像我們的磁盤空間一樣。在實現(xiàn)時,既可以實現(xiàn)成固定大小的,也可以在運行時動態(tài)地調(diào)整。
如何調(diào)整呢?
通過設(shè)置如下參數(shù),可以設(shè)定堆區(qū)的初始值和最大值,比如?-Xms256M -Xmx 1024M,其中?-X?這個字母代表它是 JVM 運行時參數(shù),ms?是?memory start?的簡稱,中文意思就是內(nèi)存初始值,mx?是?memory max?的簡稱,意思就是最大內(nèi)存。
值得注意的是,在通常情況下,服務(wù)器在運行過程中,堆空間不斷地擴(kuò)容與回縮,會形成不必要的系統(tǒng)壓力所以在線上生產(chǎn)環(huán)境中 JVM 的?Xms?和?Xmx?會設(shè)置成同樣大小,避免在 GC 后調(diào)整堆大小時帶來的額外壓力。
3.?堆的默認(rèn)空間分配
另外,再強(qiáng)調(diào)一下堆空間內(nèi)存分配的大體情況。
這里可能就會有人來問了,你從哪里知道的呢?如果我想配置這個比例,要怎么修改呢?
我先來告訴你怎么看虛擬機(jī)的默認(rèn)配置。命令行上執(zhí)行如下命令,就可以查看當(dāng)前 JDK 版本所有默認(rèn)的 JVM 參數(shù)。
java -XX:+PrintFlagsFinal -version
輸出
對應(yīng)的輸出應(yīng)該有幾百行,我們這里去看和堆內(nèi)存分配相關(guān)的兩個參數(shù)
>java -XX:+PrintFlagsFinal -version
[Global flags]
...
uintx InitialSurvivorRatio = 8
uintx NewRatio = 2
...
java version "1.8.0_131"
Java(TM) SE Runtime Environment (build 1.8.0_131-b11)
Java HotSpot(TM) 64-Bit Server VM (build 25.131-b11, mixed mode)
參數(shù)解釋
因為新生代是由?Eden + S0 + S1?組成的,所以按照上述默認(rèn)比例,如果?eden?區(qū)內(nèi)存大小是 40M,那么兩個?survivor?區(qū)就是 5M,整個?young?區(qū)就是 50M,然后可以算出?Old?區(qū)內(nèi)存大小是 100M,堆區(qū)總大小就是 150M。
4. 堆溢出演示
/** * VM Args:-Xms10m -Xmx10m -XX:+HeapDumpOnOutOfMemoryError * @author Richard_Yi */ public class HeapOOMTest {public static final int _1MB = 1024 * 1024;public static void main(String[] args) {List<byte[]> byteList = new ArrayList<>(10);for (int i = 0; i < 10; i++) {byte[] bytes = new byte[2 * _1MB];byteList.add(bytes);}} }輸出
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid32372.hprof ...
Heap dump file created [7774077 bytes in 0.009 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at jvm.HeapOOMTest.main(HeapOOMTest.java:18)
-XX:+HeapDumpOnOutOfMemoryError 可以讓 JVM 在遇到 OOM 異常時,輸出堆內(nèi)信息,特別是對相隔數(shù)月才出現(xiàn)的 OOM 異常尤為重要。
創(chuàng)建一個新對象內(nèi)存分配流程
看完上面對堆的介紹,我們趁熱打鐵再學(xué)習(xí)一下 JVM 創(chuàng)建一個新對象的內(nèi)存分配流程。
絕大部分對象在?Eden?區(qū)生成,當(dāng)?Eden?區(qū)裝填滿的時候,會觸發(fā)?Young Garbage Collection,即?YGC。垃圾回收的時候,在?Eden?區(qū)實現(xiàn)清除策略,沒有被引用的對象則直接回收。依然存活的對象會被移送到?Survivor?區(qū)。Survivor?區(qū)分為 so 和 s1 兩塊內(nèi)存空間。每次?YGC?的時候,它們將存活的對象復(fù)制到未使用的那塊空間,然后將當(dāng)前正在使用的空間完全清除,交換兩塊空間的使用狀態(tài)。如果 YGC 要移送的對象大于?Survivor?區(qū)容量的上限,則直接移交給老年代。一個對象也不可能永遠(yuǎn)呆在新生代,就像人到了 18 歲就會成年一樣,在 JVM 中?-XX:MaxTenuringThreshold?參數(shù)就是來配置一個對象從新生代晉升到老年代的閾值。默認(rèn)值是 15,可以在?Survivor?區(qū)交換 14 次之后,晉升至老年代。
上述涉及到一部分垃圾回收的名詞,不熟悉的讀者可以查閱資料或者看下本系列的垃圾回收章節(jié)。46張PPT弄懂JVM、GC算法和性能調(diào)優(yōu),分享給大家。
Metaspace 元空間
在?HotSpot JVM?中,永久代( ≈ 方法區(qū))中用于存放類和方法的元數(shù)據(jù)以及常量池,比如?Class?和?Method。每當(dāng)一個類初次被加載的時候,它的元數(shù)據(jù)都會放到永久代中。
永久代是有大小限制的,因此如果加載的類太多,很有可能導(dǎo)致永久代內(nèi)存溢出,即萬惡的?java.lang.OutOfMemoryError: PermGen,為此我們不得不對虛擬機(jī)做調(diào)優(yōu)。
那么,Java 8 中?PermGen?為什么被移出?HotSpot JVM?了?(詳見:JEP 122: Remove the Permanent Generation):
1. 由于?PermGen?內(nèi)存經(jīng)常會溢出,引發(fā)惱人的?java.lang.OutOfMemoryError: PermGen,因此 JVM 的開發(fā)者希望這一塊內(nèi)存可以更靈活地被管理,不要再經(jīng)常出現(xiàn)這樣的?OOM
2. 移除?PermGen?可以促進(jìn)?HotSpot JVM?與?JRockit VM?的融合,因為?JRockit?沒有永久代。
根據(jù)上面的各種原因,PermGen?最終被移除,方法區(qū)移至?Metaspace,字符串常量池移至堆區(qū)。
準(zhǔn)確來說,Perm 區(qū)中的字符串常量池被移到了堆內(nèi)存中是在 Java7 之后,Java 8 時,PermGen 被元空間代替,其他內(nèi)容比如類元信息、字段、靜態(tài)屬性、方法、常量等都移動到元空間區(qū)。比如?java/lang/Object?類元信息、靜態(tài)屬性 System.out、整形常量 100000 等。
元空間的本質(zhì)和永久代類似,都是對 JVM 規(guī)范中方法區(qū)的實現(xiàn)。不過元空間與永久代之間最大的區(qū)別在于:元空間并不在虛擬機(jī)中,而是使用本地內(nèi)存。因此,默認(rèn)情況下,元空間的大小僅受本地內(nèi)存限制。(和后面提到的直接內(nèi)存一樣,都是使用本地內(nèi)存)
In JDK 8, classes metadata is now stored in the?native heap?and this space is called?Metaspace.
對應(yīng)的 JVM 調(diào)參:
46張PPT弄懂JVM、GC算法和性能調(diào)優(yōu),分享給大家。
延伸閱讀:關(guān)于 Metaspace 比較好的兩篇文章
Metaspace in Java 8
http://lovestblog.cn/blog/2016/10/29/metaspace/
Java 虛擬機(jī)棧
對于每一個線程,JVM 都會在線程被創(chuàng)建的時候,創(chuàng)建一個單獨的棧。也就是說虛擬機(jī)棧的生命周期和線程是一致,并且是線程私有的。除了 Native 方法以外,Java 方法都是通過 Java 虛擬機(jī)棧來實現(xiàn)調(diào)用和執(zhí)行過程的(需要程序技術(shù)器、堆、元空間內(nèi)數(shù)據(jù)的配合)。所以 Java 虛擬機(jī)棧是虛擬機(jī)執(zhí)行引擎的核心之一。而 Java 虛擬機(jī)棧中出棧入棧的元素就稱為「棧幀」。
棧幀(Stack Frame)是用于支持虛擬機(jī)進(jìn)行方法調(diào)用和方法執(zhí)行的數(shù)據(jù)結(jié)構(gòu)。棧幀存儲了方法的局部變量表、操作數(shù)棧、動態(tài)連接和方法返回地址等信息。每一個方法從調(diào)用至執(zhí)行完成的過程,都對應(yīng)著一個棧幀在虛擬機(jī)棧里從入棧到出棧的過程。
棧對應(yīng)線程,棧幀對應(yīng)方法
在活動線程中, 只有位于棧頂?shù)膸攀怯行У?#xff0c; 稱為當(dāng)前棧幀。正在執(zhí)行的方法稱為當(dāng)前方法。在執(zhí)行引擎運行時, 所有指令都只能針對當(dāng)前棧幀進(jìn)行操作。而?StackOverflowError?表示請求的棧溢出, 導(dǎo)致內(nèi)存耗盡, 通常出現(xiàn)在遞歸方法中。
虛擬機(jī)棧通過 pop 和 push 的方式,對每個方法對應(yīng)的活動棧幀進(jìn)行運算處理,方法正常執(zhí)行結(jié)束,肯定會跳轉(zhuǎn)到另一個棧幀上。在執(zhí)行的過程中,如果出現(xiàn)了異常,會進(jìn)行異常回溯,返回地址通過異常處理表確定。
可以看出棧幀在整個 JVM 體系中的地位頗高。下面也具體介紹一下棧幀中的存儲信息。
1. 局部變量表
局部變量表就是存放方法參數(shù)和方法內(nèi)部定義的局部變量的區(qū)域。
局部變量表所需的內(nèi)存空間在編譯期間完成分配,當(dāng)進(jìn)入一個方法時,這個方法需要在幀中分配多大的局部變量空間是完全確定的,在方法運行期間不會改變局部變量表的大小。
這里直接上代碼,更好理解。
public int test(int a, int b) {Object obj = new Object();return a + b; }如果局部變量是 Java 的 8 種基本基本數(shù)據(jù)類型,則存在局部變量表中,如果是引用類型。如 new 出來的 String,局部變量表中存的是引用,而實例在堆中。
2. 操作棧
操作數(shù)棧(Operand Stack)看名字可以知道是一個棧結(jié)構(gòu)。Java 虛擬機(jī)的解釋執(zhí)行引擎稱為“基于棧的執(zhí)行引擎”,其中所指的“?!本褪遣僮鲾?shù)棧。當(dāng) JVM 為方法創(chuàng)建棧幀的時候,在棧幀中為方法創(chuàng)建一個操作數(shù)棧,保證方法內(nèi)指令可以完成工作。
還是用實操理解一下。
/** * @author Richard_yyf */ public class OperandStackTest {public int sum(int a, int b) {return a + b;} }編譯生成 .class 文件之后,再反匯編查看匯編指令
> javac OperandStackTest.java
> javap -v OperandStackTest.class > 1.txt
3. 動態(tài)連接
每個棧幀中包含一個在常量池中對當(dāng)前方法的引用, 目的是支持方法調(diào)用過程的動態(tài)連接。
4. 方法返回地址
方法執(zhí)行時有兩種退出情況:
-
正常退出,即正常執(zhí)行到任何方法的返回字節(jié)碼指令,如?RETURN、IRETURN、ARETURN?等
-
異常退出
無論何種退出情況,都將返回至方法當(dāng)前被調(diào)用的位置。方法退出的過程相當(dāng)于彈出當(dāng)前棧幀,退出可能有三種方式:
-
返回值壓入上層調(diào)用棧幀
-
異常信息拋給能夠處理的棧幀
-
PC 計數(shù)器指向方法調(diào)用后的下一條指令
本地方法棧
本地方法棧(Native Method Stack)與虛擬機(jī)棧所發(fā)揮的作用是非常相似的,它們之間的區(qū)別不過是虛擬機(jī)棧為虛擬機(jī)執(zhí)行 Java 方法(也就是字節(jié)碼)服務(wù),而本地方法棧則為虛擬機(jī)使用到的 Native 方法服務(wù)。在虛擬機(jī)規(guī)范中對本地方法棧中方法使用的語言、使用方式與數(shù)據(jù)結(jié)構(gòu)并沒有強(qiáng)制規(guī)定,因此具體的虛擬機(jī)可以自由實現(xiàn)它。甚至有的虛擬機(jī)(譬如 Sun HotSpot 虛擬機(jī))直接就把本地方法棧和虛擬機(jī)棧合二為一。與虛擬機(jī)棧一樣,本地方法棧區(qū)域也會拋出?StackOverflowError?和?OutOfMemoryError?異常。
程序計數(shù)器
程序計數(shù)器(Program Counter Register)是一塊較小的內(nèi)存空間。是線程私有的。它可以看作是當(dāng)前線程所執(zhí)行的字節(jié)碼的行號指示器。什么意思呢?
白話版本:因為代碼是在線程中運行的,線程有可能被掛起。即 CPU 一會執(zhí)行線程 A,線程 A 還沒有執(zhí)行完被掛起了,接著執(zhí)行線程 B,最后又來執(zhí)行線程 A 了,CPU 得知道執(zhí)行線程A的哪一部分指令,線程計數(shù)器會告訴 CPU。
由于 Java 虛擬機(jī)的多線程是通過線程輪流切換并分配處理器執(zhí)行時間的方式來實現(xiàn)的,CPU 只有把數(shù)據(jù)裝載到寄存器才能夠運行。寄存器存儲指令相關(guān)的現(xiàn)場信息,由于 CPU 時間片輪限制,眾多線程在并發(fā)執(zhí)行過程中,任何一個確定的時刻,一個處理器或者多核處理器中的一個內(nèi)核,只會執(zhí)行某個線程中的一條指令。
因此,為了線程切換后能恢復(fù)到正確的執(zhí)行位置,每條線程都需要有一個獨立的程序計數(shù)器,各條線程之間計數(shù)器互不影響,獨立存儲。每個線程在創(chuàng)建后,都會產(chǎn)生自己的程序計數(shù)器和棧幀,程序計數(shù)器用來存放執(zhí)行指令的偏移量和行號指示器等,線程執(zhí)行或恢復(fù)都要依賴程序計數(shù)器。此區(qū)域也不會發(fā)生內(nèi)存溢出異常。
直接內(nèi)存
直接內(nèi)存(Direct Memory)并不是虛擬機(jī)運行時數(shù)據(jù)區(qū)的一部分,也不是 Java 虛擬機(jī)規(guī)范中定義的內(nèi)存區(qū)域。但是這部分內(nèi)存也被頻繁地使用,而且也可能導(dǎo)致 OutOfMemoryError 異常出現(xiàn),所以我們放到這里一起講解。
在 JDK 1.4 中新加入了 NIO(New Input/Output)類,引入了一種基于通道(Channel)與緩沖區(qū)(Buffer)的 I/O 方式,它可以使用 Native 函數(shù)庫直接分配堆外內(nèi)存,然后通過一個存儲在 Java 堆中的?DirectByteBuffer?對象作為這塊內(nèi)存的引用進(jìn)行操作。這樣能在一些場景中顯著提高性能,因為避免了在 Java 堆和 Native 堆中來回復(fù)制數(shù)據(jù)。
顯然,本機(jī)直接內(nèi)存的分配不會受到 Java 堆大小的限制,但是,既然是內(nèi)存,肯定還是會受到本機(jī)總內(nèi)存(包括 RAM 以及 SWAP 區(qū)或者分頁文件)大小以及處理器尋址空間的限制。如果內(nèi)存區(qū)域總和大于物理內(nèi)存的限制,也會出現(xiàn) OOM。
Code Cache
簡而言之, JVM 代碼緩存是 JVM 將其字節(jié)碼存儲為本機(jī)代碼的區(qū)域?。我們將可執(zhí)行本機(jī)代碼的每個塊稱為?nmethod。該?nmethod?可能是一個完整的或內(nèi)聯(lián) Java 方法。
實時(JIT)編譯器是代碼緩存區(qū)域的最大消費者。這就是為什么一些開發(fā)人員將此內(nèi)存稱為 JIT 代碼緩存的原因。
這部分代碼所占用的內(nèi)存空間成為?CodeCache?區(qū)域。一般情況下我們是不會關(guān)心這部分區(qū)域的且大部分開發(fā)人員對這塊區(qū)域也不熟悉。如果這塊區(qū)域 OOM 了,在日志里面就會看到:
java.lang.OutOfMemoryError code cache。
診斷選項
?
參考:
《深入理解Java虛擬機(jī)》 - 周志明
《碼出高效》
Metaspace in Java 8
JVM機(jī)器指令集圖解:
Introduction to JVM Code Cache
《新程序員》:云原生和全面數(shù)字化實踐50位技術(shù)專家共同創(chuàng)作,文字、視頻、音頻交互閱讀總結(jié)
以上是生活随笔為你收集整理的图文并茂,傻瓜都能看懂的 JVM 内存布局的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 阿里P9架构师分享:通俗易懂Redis原
- 下一篇: 大白话 + 13 张图解 Kafka