Java內存模型是每一個java程序員必須掌握理解的,這是Java的核心基礎,對咱們編寫代碼特別是併發編程時有很大幫助。因爲Java程序是交由JVM執行的,因此咱們在談Java內存區域劃分的時候事實上是指JVM內存區域劃分。java
首先,咱們回顧一下Java程序執行流程:程序員
如上圖所示,首先Java源代碼文件(.java後綴)會被Java編譯器編譯爲字節碼文件(.class後綴),而後由JVM中的類加載器加載各個類的字節碼文件,加載完畢以後,交由JVM執行引擎執行。在整個程序執行過程當中,JVM會用一段空間來存儲程序執行期間須要用到的數據和相關信息,這段空間通常被稱做爲Runtime Data Area(運行時數據區),也就是咱們常說的JVM內存。所以,在Java中咱們經常說到的內存管理就是針對這段空間進行管理(如何分配和回收內存空間)。編程
那麼本篇文章主要是要分析Runtime Data Area(運行時數據區)的結構。數組
1. 運行時數據區分爲幾個部分?數據結構
根據 JVM 規範,JVM 內存共分爲虛擬機棧、堆、方法區、程序計數器、本地方法棧五個部分。多線程
名稱架構
特徵併發
做用jvm
配置參數spa
異常
程序計數器
佔用內存小,線程私有,
生命週期與線程相同
大體爲字節碼行號指示器
無
無
虛擬機棧
線程私有,生命週期與線程相同,使用連續的內存空間
Java 方法執行的內存模型,存儲局部變量表、操做棧、動態連接、方法出口等信息
-Xss
StackOverflowError
OutOfMemoryError
java堆
線程共享,生命週期與虛擬機相同,能夠不使用連續的內存地址
保存對象實例,全部對象實例(包括數組)都要在堆上分配
-Xms
-Xsx
-Xmn
OutOfMemoryError
方法區
線程共享,生命週期與虛擬機相同,能夠不使用連續的內存地址
存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據
-XX:PermSize:
16M
-XX:MaxPermSize
64M
OutOfMemoryError
運行時常量池
方法區的一部分,具備動態性
存放字面量及符號引用
1.1 方法區
方法區是java虛擬機規範去中定義的一種概念上的區域,具備什麼功能,但並無規定這個區域到底應該位於何處,所以對於實現者來講,如何來實際方法區是有着很大自由度的。
永生代是hotspot中的一個概念,其餘jvm實現未必有,例如jrockit就沒這東西。java8以前,hotspot使用在內存中劃分出一塊區域來存儲類的元信息、類變量以及內部字符串(interned string)等內容,稱之爲永生代,把它做爲方法區來使用。
[JEP122][2]提議取消永生代,方法區做爲概念上的區域仍然存在。原先永生代中類的元信息會被放入本地內存(元數據區,metaspace),將類的靜態變量和內部字符串放入到java堆中。
爲了龍清楚方法區那麼須要解釋兩個名詞:永久代和元空間
PermGen(永久代)
絕大部分Java程序員應該都見過「java.lang.OutOfMemoryError: PremGen space」異常。這裏的「PermGen space」其實指的就是方法區。不過方法區和「PermGen space」又有着本質的區別。前者是JVM的規範,然後者則是JVM規範的一種實現,而且只有HotSpot纔有「PermGen space」,而對於其餘類型的虛擬機,如JRockit(Oracle)、J9(IBM)並無「PermGen space」。因爲方法區主要存儲類的相關信息,因此對於動態生成類的狀況比較容易出現永久代的內存溢出。而且JDK 1.8中參數PermSize和MaxPermSize已經失效。
元空間
其實,移除永久代的工做從JDK 1.7就開始了。JDK 1.7中,存儲在永久代的部分數據就已經轉移到Java Heap或者Native Heap。但永久代仍存在於JDK 1.7中,並無徹底移除,譬如符號引用(Symbols)轉移到了native heap;字面量(interned strings)轉移到了Java heap;類的靜態變量(class statics)轉移到了Java heap。
JDK1.8對JVM架構的改造將類元數據放到本地內存中,另外,將常量池和靜態變量放到Java堆裏。HotSpot VM將會爲類的元數據明確分配和釋放本地內存。在這種架構下,類元信息就突破了原來-XX:MaxPermSize的限制,如今可使用更多的本地內存。這樣就從必定程度上解決了原來在運行時生成大量類形成常常Full GC問題,如運行時使用反射、代理等。因此升級之後Java堆空間可能會增長。
元空間的本質和永久代相似,都是對JVM規範中方法區的實現。不過元空間與永久代之間的最大區別在於:元空間並不在虛擬機中,而是使用本地內存。所以,默認狀況下,元空間的大小僅受本地內存限制,但能夠經過如下參數指定元空間的大小:
-XX:MetaspaceSize,初始空間大小,達到該值就會觸發垃圾收集進行類型卸載,同時GC會對改值進行調整:若是釋放了大量的空間,就適當下降該值;若是釋放了不多的空間,那麼在不超過MaxMetaspaceSize時,適當提升該值。
-XX:MaxMetaspaceSize,最大空間,默認是沒有限制的。
除了上面的兩個指定大小的選項外,還有兩個與GC相關的屬性:
-XX:MinMetaspaceFreeRatio,在GC以後,最小的Metaspace剩餘空間容量的百分比,減小爲分配空間所致使的垃圾收集。
-XX:MaxMetaspaceFreeRatio,在GC以後,最大的Metaspace剩餘空間容量的百分比,減小爲釋放空間所致使的垃圾收集。
因此對於方法區,Java8以後的變化:
移除了永久代(PermGen),替換爲元空間(Metaspace);
永久代中的 class metadata 轉移到了 native memory(本地內存,而不是虛擬機);
永久代中的 interned Strings 和 class static variables 轉移到了 Java heap;
永久代參數 (PermSize MaxPermSize) -> 元空間參數(MetaspaceSize MaxMetaspaceSize)
1.2 虛擬機棧(線程棧)與 堆(Heap)
爲更好的理解Java線程棧和堆,咱們簡單的認爲Java內存模型把Java虛擬機內部劃分爲線程棧和堆。這張圖演示了Java內存模型的邏輯視圖。
每個運行在Java虛擬機裏的線程都擁有本身的線程棧。這個線程棧包含了這個線程調用的方法當前執行點相關的信息。一個線程僅能訪問本身的線程棧。一個線程建立的本地變量對其它線程不可見,僅本身可見。即便兩個線程執行一樣的代碼,這兩個線程任然在在本身的線程棧中的代碼來建立本地變量。所以,每一個線程擁有每一個本地變量的獨有版本。
全部原始類型的本地變量都存放在線程棧上,所以對其它線程不可見。一個線程可能向另外一個線程傳遞一個原始類型變量的拷貝,可是它不能共享這個原始類型變量自身。
堆上包含在Java程序中建立的全部對象,不管是哪個對象建立的。這包括原始類型的對象版本。若是一個對象被建立而後賦值給一個局部變量,或者用來做爲另外一個對象的成員變量,這個對象任然是存放在堆上。
下面這張圖演示了調用棧和本地變量存放在線程棧上,對象存放在堆上。
一個本地變量多是原始類型,在這種狀況下,它老是「呆在」線程棧上。
一個本地變量也多是指向一個對象的一個引用。在這種狀況下,引用(這個本地變量)存放在線程棧上,可是對象自己存放在堆上。
一個對象可能包含方法,這些方法可能包含本地變量。這些本地變量任然存放在線程棧上,即便這些方法所屬的對象存放在堆上。
一個對象的成員變量可能隨着這個對象自身存放在堆上。無論這個成員變量是原始類型仍是引用類型。
靜態成員變量跟隨着類定義一塊兒也存放在堆上。
存放在堆上的對象能夠被全部持有對這個對象引用的線程訪問。當一個線程能夠訪問一個對象時,它也能夠訪問這個對象的成員變量。若是兩個線程同時調用同一個對象上的同一個方法,它們將會都訪問這個對象的成員變量,可是每個線程都擁有這個本地變量的私有拷貝。
下圖演示了上面提到的點:
兩個線程擁有一些列的本地變量。其中一個本地變量(Local Variable 2)執行堆上的一個共享對象(Object 3)。這兩個線程分別擁有同一個對象的不一樣引用。這些引用都是本地變量,所以存放在各自線程的線程棧上。這兩個不一樣的引用指向堆上同一個對象。
注意,這個共享對象(Object 3)持有Object2和Object4一個引用做爲其成員變量(如圖中Object3指向Object2和Object4的箭頭)。經過在Object3中這些成員變量引用,這兩個線程就能夠訪問Object2和Object4。
1.3 程序計數器
程序計數器是一塊較小的內存空間,能夠看做是當前線程所執行的字節碼的行號指示器。分支、循環、跳轉、異常處理、線程恢復等基礎功能都須要依賴這個計數器來完成。
因爲Java 虛擬機的多線程是經過線程輪流切換並分配處理器執行時間的方式來實現的,在任何一個肯定的時刻,一個處理器(對於多核處理器來講是一個內核)只會執行一條線程中的指令。所以,爲了線程切換後能恢復到正確的執行位置,每條線程都須要有一個獨立的程序計數器,各條線程之間的計數器互不影響,獨立存儲,咱們稱這類內存區域爲「線程私有」的內存。
若是線程正在執行的是一個Java 方法,這個計數器記錄的是正在執行的虛擬機字節碼指令的地址;若是正在執行的是Natvie 方法,這個計數器值則爲空(Undefined)。
此內存區域是惟一一個在Java 虛擬機規範中沒有規定任何OutOfMemoryError狀況的區域。
1.4 本地方法棧
本地方法棧(Native MethodStacks)與虛擬機棧所發揮的做用是很是類似的,其區別不過是虛擬機棧爲虛擬機執行Java 方法(也就是字節碼)服務,而本地方法棧則是爲虛擬機使用到的Native 方法服務。虛擬機規範中對本地方法棧中的方法使用的語言、使用方式與數據結構並無強制規定,所以具體的虛擬機能夠自由實現它。甚至有的虛擬機(譬如Sun HotSpot 虛擬機)直接就把本地方法棧和虛擬機棧合二爲一。
與虛擬機棧同樣,本地方法棧區域也會拋出StackOverflowError和OutOfMemoryError異常。