多圖:一文帶你入門掌握JVM所有知識點

前言

本JVM系列屬於本人學習過程當中總結的一些知識點,目的是想讓讀者更快地掌握JVM相關的知識要點,難免會有所側重,若想要更加系統更加詳細的學習JVM知識,還是需要去閱讀專業的書籍和文檔。

本文主題內容:

  1. JVM 內存區域概覽
  2. 堆區的空間分配是怎麼樣?堆溢出的演示
  3. 創建一個新對象內存是怎麼分配的?
  4. 方法區 到 Metaspace 元空間
  5. 棧幀是什麼?棧幀裡有什麼?怎麼理解?
  6. 本地方法棧
  7. 程序計數器
  8. Code Cache 是什麼?

注:請 區分 JVM內存結構(內存佈局) 和 JMM(Java內存模型)這兩個不同的概念!

概覽

內存是非常重要的系統資源,是硬盤和CPU的中間倉庫及橋樑,承載著操作系統和應用程序的實時運行。JVM 內存佈局規定了 Java 在運行過程中內存申請、分配、管理的策略 ,保證了 JVM 的高效穩定運行。

多圖:一文帶你入門掌握JVM所有知識點

上圖描述了當前比較經典的Java內存佈局。(堆區畫小了2333,按理來說應該是最大的區域)

如果按照線程是否共享來分類的話,如下圖所示:

多圖:一文帶你入門掌握JVM所有知識點

PS:線程是否共享這點,實際上理解了每塊區域的實際用處之後,就很自然而然的就記住了。不需要死記硬背。

下面讓我們來了解下各個區域。

一、Heap (堆區)

1.1 堆區的介紹

我們先來說堆。堆是 OOM故障最主要的發生區域。它是內存區域中最大的一塊區域,被所有線程共享,存儲著幾乎所有的實例對象、數組。所有的對象實例以及數組都要在堆上分配,但是隨著JIT編譯器的發展與逃逸分析技術逐漸成熟,棧上分配、標量替換優化技術將會導致一些微妙的變化發生,所有的對象都分配在堆上也漸漸變得不是那麼“絕對”了

延伸知識點:JIT編譯優化中的一部分內容 -

逃逸分析。 推薦閱讀:深入理解Java中的逃逸分析

Java堆是垃圾收集器管理的主要區域,因此很多時候也被稱做“GC堆”。從內存回收的角度來看,由於現在收集器基本都採用分代收集算法,所以Java堆中還可以細分為:新生代和老年代。再細緻一點的有Eden空間、From Survivor空間、To Survivor空間等。從內存分配的角度來看,線程共享的Java堆中可能劃分出多個線程私有的分配緩衝區(Thread Local Allocation Buffer,TLAB)。不過無論如何劃分,都與存放內容無關,無論哪個區域,存儲的都仍然是對象實例,進一步劃分的目的是為了更好地回收內存,或者更快地分配內存。

1.2 堆區的調整

根據Java虛擬機規範的規定,Java堆可以處於物理上不連續的內存空間中,只要邏輯上是連續的即可,就像我們的磁盤空間一樣。在實現時,既可以實現成固定大小的,也可以在運行時動態地調整。

如何調整呢?

通過設置如下參數,可以設定堆區的初始值和最大值,比如 -Xms256M-Xmx1024M,其中 -X這個字母代表它是JVM運行時參數, ms是 memory start的簡稱,中文意思就是內存初始值, mx 是 memory max的簡稱,意思就是最大內存。

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

1.3 堆的默認空間分配

另外,再強調一下堆空間內存分配的大體情況。

多圖:一文帶你入門掌握JVM所有知識點

這裡可能就會有人來問了,你從哪裡知道的呢?如果我想配置這個比例,要怎麼修改呢?

我先來告訴你怎麼看虛擬機的默認配置。命令行上執行如下命令,就可以查看當前JDK版本所有默認的JVM參數。

<code>1.  `java -XX:+PrintFlagsFinal-version`/<code>

輸出

對應的輸出應該有幾百行,我們這裡去看和堆內存分配相關的兩個參數

<code>`>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)`/<code>

參數解釋

多圖:一文帶你入門掌握JVM所有知識點

因為新生代是由Eden + S0 + S1組成的,所以按照上述默認比例,如果eden區內存大小是40M,那麼兩個survivor區就是5M,整個young區就是50M,然後可以算出Old區內存大小是100M,堆區總大小就是150M。

1.4 堆溢出 演示

<code>/*** 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);}}} /<byte>/<code>

輸出

<code>java.lang.OutOfMemoryError: Java heap spaceDumping heap to java_pid32372.hprof ...Heapdump file created [7774077 bytes in0.009 secs]Exceptionin thread "main" java.lang.OutOfMemoryError: Java heap spaceat jvm.HeapOOMTest.main(HeapOOMTest.java:18)/<code>

-XX:+HeapDumpOnOutOfMemoryError 可以讓JVM在遇到OOM異常時,輸出堆內信息,特別是對相隔數月才出現的OOM異常尤為重要。

創建一個新對象 內存分配流程

看完上面對堆的介紹,我們趁熱打鐵再學習一下JVM創建一個新對象的內存分配流程。

多圖:一文帶你入門掌握JVM所有知識點

絕大部分對象在Eden區生成,當Eden區裝填滿的時候,會觸發 YoungGarbageCollection,即 YGC。垃圾回收的時候,在Eden區實現清除策略,沒有被引用的對象則直接回收。依然存活的對象會被移送到Survivor區。Survivor區分為so和s1兩塊內存空間。每次 YGC的時候,它們將存活的對象複製到未使用的那塊空間,然後將當前正在使用的空間完全清除,交換兩塊空間的使用狀態。如果 YGC要移送的對象大於Survivor區容量的上限,則直接移交給老年代。一個對象也不可能永遠呆在新生代,就像人到了18歲就會成年一樣,在JVM中 -XX:MaxTenuringThreshold參數就是來配置一個對象從新生代晉升到老年代的閾值。默認值是15, 可以在Survivor區交換14次之後,晉升至老年代。

上述涉及到一部分垃圾回收的名詞,不熟悉的讀者可以查閱資料或者看下本系列的垃圾回收章節。

二、Metaspace 元空間

在 HotSpot JVM 中,永久代( ≈ 方法區)中用於存放類和方法的元數據以及常量池,比如 Class和 Method。每當一個類初次被加載的時候,它的元數據都會放到永久代中。

永久代是有大小限制的,因此如果加載的類太多,很有可能導致永久代內存溢出,即萬惡的 java.lang.OutOfMemoryError:PermGen,為此我們不得不對虛擬機做調優。

那麼,Java 8 中 PermGen 為什麼被移出 HotSpot JVM 了?(詳見:JEP 122: Remove the Permanent Generation):

1、由於 PermGen 內存經常會溢出,引發惱人的 java.lang.OutOfMemoryError:PermGen,因此 JVM 的開發者希望這一塊內存可以更靈活地被管理,不要再經常出現這樣的 OOM

2、移除 PermGen 可以促進 HotSpot JVM 與 JRockit VM 的融合,因為 JRockit 沒有永久代。

根據上面的各種原因,PermGen 最終被移除,方法區移至 Metaspace,字符串常量池移至堆區

準確來說,Perm 區中的字符串常量池被移到了堆內存中是在Java7 之後,Java 8 時,PermGen 被元空間代替,其他內容比如類元信息、字段、靜態屬性、方法、常量等都移動到元空間區。比如 java/lang/Object類元信息、靜態屬性 System.out、整形常量 100000等。

元空間的本質和永久代類似,都是對JVM規範中方法區的實現。不過元空間與永久代之間最大的區別在於:

元空間並不在虛擬機中,而是使用本地內存。因此,默認情況下,元空間的大小僅受本地內存限制。(和後面提到的直接內存一樣,都是使用本地內存)

In JDK 8, classes metadata is now stored in the native heap and this space is calledMetaspace.

對應的JVM調參:

多圖:一文帶你入門掌握JVM所有知識點

三、 Java 虛擬機棧

對於每一個線程,JVM 都會在線程被創建的時候,創建一個單獨的棧。也就是說虛擬機棧的生命週期和線程是一致,並且是線程私有的。除了Native方法以外,Java方法都是通過Java 虛擬機棧來實現調用和執行過程的(需要程序技術器、堆、元空間內數據的配合)。所以Java虛擬機棧是虛擬機執行引擎的核心之一。而Java虛擬機棧中出棧入棧的元素就稱為「棧幀」。

棧幀(Stack Frame)是用於支持虛擬機進行方法調用和方法執行的數據結構。棧幀存儲了方法的局部變量表、操作數棧、動態連接和方法返回地址等信息。每一個方法從調用至執行完成的過程,都對應著一個棧幀在虛擬機棧裡從入棧到出棧的過程。

棧對應線程,棧幀對應方法

在活動線程中, 只有位於棧頂的幀才是有效的, 稱為當前棧幀。正在執行的方法稱為當前方法。在執行引擎運行時, 所有指令都只能針對當前棧幀進行操作。而 StackOverflowError 表示請求的

棧溢出, 導致內存耗盡, 通常出現在遞歸方法中。

虛擬機棧通過pop和push的方式,對每個方法對應的活動棧幀進行運算處理,方法正常執行結束,肯定會跳轉到另一個棧幀上。在執行的過程中,如果出現了異常,會進行異常回溯,返回地址通過異常處理表確定。

可以看出棧幀在整個JVM 體系中的地位頗高。下面也具體介紹一下棧幀中的存儲信息。

多圖:一文帶你入門掌握JVM所有知識點

1. 局部變量表

局部變量表就是存放方法參數和方法內部定義的局部變量的區域

局部變量表所需的內存空間在編譯期間完成分配,當進入一個方法時,這個方法需要在幀中分配多大的局部變量空間是完全確定的,在方法運行期間不會改變局部變量表的大小

這裡直接上代碼,更好理解。

<code>publicint test(int a, int b) {Object obj = newObject();return a + b;} /<code>

如果局部變量是Java的8種基本基本數據類型,則存在局部變量表中,如果是引用類型。如new出來的String,局部變量表中存的是引用,而實例在堆中。

多圖:一文帶你入門掌握JVM所有知識點

2. 操作棧

操作數棧(Operand Stack)看名字可以知道是一個棧結構。Java虛擬機的解釋執行引擎稱為“基於棧的執行引擎”,其中所指的“棧”就是操作數棧。當JVM為方法創建棧幀的時候,在棧幀中為方法創建一個操作數棧,保證方法內指令可以完成工作。

還是用實操理解一下。

<code>/*** @author Richard_yyf*/public class OperandStackTest {public int sum(int a, int b) {return a + b;}}/<code>

編譯生成 .class文件之後,再反彙編查看彙編指令

<code>> javac OperandStackTest.java> javap -v OperandStackTest.class> 1.txt /<code>


<code>public int sum(int, int);descriptor: (II)Iflags: ACC_PUBLICCode:stack=2, locals=3, args_size=3 // 最大棧深度為2 局部變量個數為30: iload_1 // 局部變量1 壓棧1: iload_2 // 局部變量2 壓棧2: iadd // 棧頂兩個元素相加,計算結果壓棧3: ireturnLineNumberTable:line 10: 0 /<code>

3. 動態連接

每個棧幀中包含一個在常量池中對當前方法的引用

, 目的是支持方法調用過程的動態連接

4. 方法返回地址

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

1、正常退出,即正常執行到任何方法的返回字節碼指令,如 RETURN、 IRETURN、 ARETURN等 2、異常退出

無論何種退出情況,都將返回至方法當前調用的位置。方法退出的過程相當於彈出當前棧幀,退出可能有三種方式:

1、返回值壓入上層調用棧幀 2、異常信息拋給能夠處理的棧幀 3、PC 計數器指向方法調用後的下一條指令

延伸閱讀:JVM機器指令集圖解

四、本地方法棧

本地方法棧(Native Method Stack)與虛擬機棧所發揮的作用是非常相似的,它們之間的區別不過是虛擬機棧為虛擬機執行Java方法(也就是字節碼)服務,而本地方法棧則為虛擬機使用到的Native方法服務。在虛擬機規範中對本地方法棧中方法使用的語言、使用方式與數據結構並沒有強制規定,因此具體的虛擬機可以自由實現它。甚至有的虛擬機(譬如Sun HotSpot虛擬機)直接就把本地方法棧和虛擬機棧合二為一。與虛擬機棧一樣,本地方法棧區域也會拋出StackOverflowError和OutOfMemoryError異常

五、程序計數器

程序計數器(Program Counter Register)是一塊較小的內存空間。是線程私有的。它可以看作是當前線程所執行的字節碼的行號指示器。什麼意思呢?

白話版本:因為代碼是在線程中運行的,線程有可能被掛起。即CPU一會執行線程A,線程A還沒有執行完被掛起了,接著執行線程B,最後又來執行線程A了,CPU得知道執行線程A的哪一部分指令,線程計數器會告訴CPU。

由於Java虛擬機的多線程是通過線程輪流切換並分配處理器執行時間的方式來實現的,CPU 只有把數據裝載到寄存器才能夠運行。寄存器存儲指令相關的現場信息,由於CPU 時間片輪限制,眾多線程在併發執行過程中,任何一個確定的時刻,一個處理器或者多核處理器中的一個內核,只會執行某個線程中的一條指令

因此,為了線程切換後能恢復到正確的執行位置,每條線程都需要有一個獨立的程序計數器,各條線程之間計數器互不影響,獨立存儲。每個線程在創建後,都會產生自己的程序計數器和棧幀,程序計數器用來存放執行指令的偏移量和行號指示器等,線程執行或恢復都要依賴程序計數器。此區域也不會發生內存溢出異常。

六、直接內存

直接內存(Direct Memory)並不是虛擬機運行時數據區的一部分,也不是Java虛擬機規範中定義的內存區域。但是這部分內存也被頻繁地使用,而且也可能導致OutOfMemoryError異常出現,所以我們放到這裡一起講解。

在JDK 1.4中新加入了NIO(New Input/Output)類,引入了一種基於通道(Channel)與緩衝區(Buffer)的I/O方式,它可以使用Native函數庫直接分配堆外內存,然後通過一個存儲在Java堆中的DirectByteBuffer對象作為這塊內存的引用進行操作。這樣能在一些場景中顯著提高性能,因為避免了在Java堆和Native堆中來回複製數據

顯然,本機直接內存的分配不會受到Java堆大小的限制,但是,既然是內存,肯定還是會受到本機總內存(包括RAM以及SWAP區或者分頁文件)大小以及處理器尋址空間的限制。如果內存區域總和大於物理內存的限制,也會出現OOM。

Code Cache

簡而言之, JVM代碼緩存是JVM將其字節碼存儲為本機代碼的區域 。我們將可執行本機代碼的每個塊稱為 nmethod 。該 nmethod可能是一個完整的或內聯Java方法。

實時(JIT)編譯器是代碼緩存區域的最大消費者。這就是為什麼一些開發人員將此內存稱為JIT代碼緩存的原因。

這部分代碼所佔用的內存空間成為CodeCache區域。一般情況下我們是不會關心這部分區域的且大部分開發人員對這塊區域也不熟悉。如果這塊區域OOM了,在日誌裡面就會看到 java.lang.OutOfMemoryErrorcode cache。

診斷選項

多圖:一文帶你入門掌握JVM所有知識點

作者:Richard_Yi
來源:http://suo.im/5vpKfN


分享到:


相關文章: