概念java
內存是很是重要的系統資源,是硬盤和 CPU 的中間倉庫及橋樑,承載着操做系統和應用程序的實時運行。JVM 內存佈局規定了 Java 在運行過程當中內存申請、分配、管理的策略,保證了 JVM 的高效穩定運行。算法
上圖描述了當前比較經典的 JVM 內存佈局。(堆區畫小了 2333,按理來講應該是最大的區域)數組
若是按照線程是否共享來分類的話,以下圖所示:緩存
PS:線程是否共享這點,實際上理解了每塊區域的實際用處以後,就很天然而然的就記住了。不須要死記硬背。服務器
下面讓咱們來了解下各個區域。數據結構
Heap (堆區)多線程
1. 堆區的介紹 併發
咱們先來講堆。堆是 OOM 故障最主要的發生區域。它是內存區域中最大的一塊區域,被全部線程共享,存儲着幾乎全部的實例對象、數組。全部的對象實例以及數組都要在堆上分配,可是隨着 JIT 編譯器的發展與逃逸分析技術逐漸成熟,棧上分配、標量替換優化技術將會致使一些微妙的變化發生,全部的對象都分配在堆上也漸漸變得不是那麼「絕對」了。jvm
延伸知識點:JIT 編譯優化中的一部份內容 - 逃逸分析。推薦閱讀:深刻理解 Java 中的逃逸分析函數
Java 堆是垃圾收集器管理的主要區域,所以不少時候也被稱作「GC 堆」。從內存回收的角度來看,因爲如今收集器基本都採用分代收集算法,因此 Java 堆中還能夠細分爲:新生代和老年代。再細緻一點的有 Eden 空間、From Survivor 空間、To Survivor 空間等。從內存分配的角度來看,線程共享的 Java 堆中可能劃分出多個線程私有的分配緩衝區(Thread Local Allocation Buffer,TLAB)。不過不管如何劃分,都與存放內容無關,不管哪一個區域,存儲的都仍然是對象實例,進一步劃分的目的是爲了更好地回收內存,或者更快地分配內存。
根據 Java 虛擬機規範的規定,Java 堆能夠處於物理上不連續的內存空間中,只要邏輯上是連續的便可,就像咱們的磁盤空間同樣。在實現時,既能夠實現成固定大小的,也能夠在運行時動態地調整。
如何調整呢?
經過設置以下參數,能夠設定堆區的初始值和最大值,好比 -Xms256M -Xmx 1024M,其中 -X這個字母表明它是 JVM 運行時參數,ms是memory start的簡稱,中文意思就是內存初始值,mx是memory max的簡稱,意思就是最大內存。
值得注意的是,在一般狀況下,服務器在運行過程當中,堆空間不斷地擴容與回縮,會造成沒必要要的系統壓力因此在線上生產環境中 JVM 的Xms和Xmx會設置成一樣大小,避免在 GC 後調整堆大小時帶來的額外壓力。
另外,再強調一下堆空間內存分配的大致狀況。
這裏可能就會有人來問了,你從哪裏知道的呢?若是我想配置這個比例,要怎麼修改呢?
我先來告訴你怎麼看虛擬機的默認配置。命令行上執行以下命令,就能夠查看當前 JDK 版本全部默認的 JVM 參數。
java -XX:+PrintFlagsFinal -version
對應的輸出應該有幾百行,咱們這裏去看和堆內存分配相關的兩個參數
`>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)`
由於新生代是由Eden + S0 + S1組成的,因此按照上述默認比例,若是eden區內存大小是 40M,那麼兩個survivor區就是 5M,整個young區就是 50M,而後能夠算出Old區內存大小是 100M,堆區總大小就是150M。
`/**
*/
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 異常時,輸出堆內信息,特別是對相隔數月纔出現的 OOM 異常尤其重要。
建立一個新對象內存分配流程
看完上面對堆的介紹,咱們趁熱打鐵再學習一下 JVM 建立一個新對象的內存分配流程。
絕大部分對象在Eden區生成,當Eden區裝填滿的時候,會觸發Young Garbage Collection,即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 called Metaspace.
對應的 JVM 調參:
延伸閱讀:關於 Metaspace 比較好的兩篇文章Metaspace in Java 8
Java 虛擬機棧
對於每個線程,JVM 都會在線程被建立的時候,建立一個單獨的棧。也就是說虛擬機棧的生命週期和線程是一致,而且是線程私有的。除了 Native 方法之外,Java 方法都是經過 Java 虛擬機棧來實現調用和執行過程的(須要程序技術器、堆、元空間內數據的配合)。因此 Java 虛擬機棧是虛擬機執行引擎的核心之一。而 Java 虛擬機棧中出棧入棧的元素就稱爲「棧幀」。
棧幀(Stack Frame)是用於支持虛擬機進行方法調用和方法執行的數據結構。棧幀存儲了方法的局部變量表、操做數棧、動態鏈接和方法返回地址等信息。每個方法從調用至執行完成的過程,都對應着一個棧幀在虛擬機棧裏從入棧到出棧的過程。
棧對應線程,棧幀對應方法
在活動線程中, 只有位於棧頂的幀纔是有效的, 稱爲當前棧幀。正在執行的方法稱爲當前方法。在執行引擎運行時, 全部指令都只能針對當前棧幀進行操做。而StackOverflowError表示請求的棧溢出, 致使內存耗盡, 一般出如今遞歸方法中。
虛擬機棧經過 pop 和 push 的方式,對每一個方法對應的活動棧幀進行運算處理,方法正常執行結束,確定會跳轉到另外一個棧幀上。在執行的過程當中,若是出現了異常,會進行異常回溯,返回地址經過異常處理表肯定。
能夠看出棧幀在整個 JVM 體系中的地位頗高。下面也具體介紹一下棧幀中的存儲信息。
局部變量表就是存放方法參數和方法內部定義的局部變量的區域。
局部變量表所需的內存空間在編譯期間完成分配,當進入一個方法時,這個方法須要在幀中分配多大的局部變量空間是徹底肯定的,在方法運行期間不會改變局部變量表的大小。
這裏直接上代碼,更好理解。
`public int test(int a, int b) {
Object obj = new Object(); return a + b;
}`
若是局部變量是 Java 的 8 種基本基本數據類型,則存在局部變量表中,若是是引用類型。如 new 出來的 String,局部變量表中存的是引用,而實例在堆中。
操做數棧(Operand Stack)看名字能夠知道是一個棧結構。Java 虛擬機的解釋執行引擎稱爲「基於棧的執行引擎」,其中所指的「棧」就是操做數棧。當 JVM 爲方法建立棧幀的時候,在棧幀中爲方法建立一個操做數棧,保證方法內指令能夠完成工做。
仍是用實操理解一下。
`/**
*/
public class OperandStackTest {
public int sum(int a, int b) { return a + b; }
}`
編譯生成 .class 文件以後,再反彙編查看彙編指令
`> javac OperandStackTest.java
javap -v OperandStackTest.class > 1.txt`
`public int sum(int, int);
descriptor: (II)I flags: ACC_PUBLIC Code: stack=2, locals=3, args_size=3 // 最大棧深度爲2 局部變量個數爲3 0: iload_1 // 局部變量1 壓棧 1: iload_2 // 局部變量2 壓棧 2: iadd // 棧頂兩個元素相加,計算結果壓棧 3: ireturn LineNumberTable: line 10: 0`
每一個棧幀中包含一個在常量池中對當前方法的引用, 目的是支持方法調用過程的動態鏈接。
方法執行時有兩種退出狀況:
不管何種退出狀況,都將返回至方法當前被調用的位置。方法退出的過程至關於彈出當前棧幀,退出可能有三種方式:
本地方法棧
本地方法棧(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.OutOfMemoryError code cache。