JVM系列一(Java內存區域和對象建立).

1、JVM 內存區域

堆 - Heap

線程共享,JVM中最大的一塊內存,此內存的惟一目的就是存放對象實例,Java 堆是垃圾收集器管理的主要區域,所以不少時候也被稱爲「GC堆」(Garbage Collected Heap),能夠經過 -Xmx 和 -Xms 參數來控制該區域大小。java

方法區 - Method Area

線程共享,它用來存儲已被虛擬機加載的類信息(版本、字段、方法、接口等描述信息)、常量、靜態變量、即時編譯器編譯後的代碼等數據。程序員

在 JDK 1.7 中,方法區被描述成堆(Heap)的一個邏輯部分,該區域也被稱爲 Non-Heap(非堆),HotSpot 虛擬機在 1.7 中使用永生代(Permanent Generation)來實現方法區,這樣垃圾收集器能夠像管理 Java 堆同樣管理這部份內存,可以省去專門爲方法區編寫內存管理代碼的工做,所以也經常有人將永生代和方法區等價,所以永生代的參數(-XX:PermSize、-XX:MaxPermSize)也限制了方法區的內存大小。算法

在 JDK 1.8 中,爲了減小方法區的內存溢出問題以及後續 HotSpot 和 JRockit 的合併事宜, HotSpots 取消了永久代(-XX:PermSize、-XX:MaxPermSize 參數即被廢棄),元空間(Metaspace)登上舞臺,方法區存在於元空間,同時,元空間再也不與堆連續,並且是存在於本地內存(Native memory)中,意味着只要本地內存足夠,它不會出現像永久代中 「java.lang.OutOfMemoryError: PermGen space」 這種錯誤,默認狀況下元空間能夠無限使用本地內存,能夠經過(-XX:MetaspaceSize、-XX:MaxMetaspaceSize)限制元空間的大小。數組

運行時常量池 - Runtime Constant Pool

線程共享,存儲的內容包括 Class 文件常量池(該部份內容在類編譯後進入)以及翻譯出來的直接引用。緩存

Class 常量池的內容包括:
服務器

對於運行時常量池,Java 虛擬機規範沒有作任何細節的要求,不一樣的提供商實現的虛擬機能夠按照本身的須要來實現這個內存區域。運行時常量池相對於 Class 文件常量池的一個重要特徵是具有動態性,也就是說並不是預置入 Class 文件常量池的內容才能進入方法區運行時常量池,運行期間也可能將新的常量放入池中,比較常見的好比 String 類的 intern() 方法。jvm

虛擬機棧/本地方法棧

線程私有,生命週期與線程相同,描述的是 Java 方法執行的內存模型:每一個方法執行的同時都會建立一個棧幀(Stack Frame)用於存儲局部變量表、操做數棧、動態連接、方法出口等信息。每一個方法從調用直到執行完成的過程,就對應着一個棧幀入棧到出棧的過程。函數

局部變量表存放了編譯器可知的各類基本類型(boolean、byte、char、short、int、float、long、double)、對象引用(reference 類型)、returnAddress 類型(指向了一條字節碼執行的地址)。其中64位長度的 long 和 double 類型的數據會佔用兩個局部變量空間(Slot)。局部變量表所需的內存空間在編譯期間完成分配,在方法運行期間不會改變局部變量表的大小。性能

虛擬機棧和本地方法棧的區別不過是虛擬機棧爲虛擬機執行 Java 方法服務,而本地方法棧爲虛擬機執行 Native 方法服務。HotSpot 虛擬機直接把虛擬機棧和本地方法棧合二爲一。可經過 -Xss 參數設置虛擬機棧大小,-Xoss 參數設置本地方法棧(HotSpot 虛擬機上該參數不生效)。spa

程序計數器

線程私有,一塊較小的內存空間,它能夠看做是當前線程所執行的字節碼的行號指示器,此內存區域是惟一一個在Java虛擬機規範中沒有規定任何 OutOfMemoryError 狀況的區域,所以該區域也變成了程序員最不關注的一個區域。

直接內存 - Direct Memory

線程私有,並非虛擬機運行時數據區的一部分,也不是 Java 虛擬機規範中定義的內存區域。Java NIO (New Input/Output)是一種基於通道(Channel)與緩存區(Buffer)的 I/O 方式,它可使用 Native 函數庫直接分配堆外內存,而後經過一個存儲在 Java 堆中的 DirectByteBuffer 對象做爲這塊內存的引用進行操做。這樣能在一些場景中顯著提升性能,由於避免了在 Java 堆和 Native 堆中來回複製數據。

該區域也可能致使內存溢出,一個明顯的特徵是在 Heap Dump 文件中不會看見明顯的異常。所以,服務器管理員在根據實際內存配置虛擬機參數時,須要考慮到直接內存須要的空間,能夠經過 -XX:MaxDirectMemorySize 來指定直接內存的大小,若是不指定,則默認與 Java 堆的最大值(-Xmx)同樣。

2、Java 對象建立

接下來看看咱們日常的一個 new 操做在 JVM 中又是怎樣一種過程呢?(討論的是普通 Java 對象,不包括數組和 Class 對象等)。

1. 棧空間分配

當執行 new 操做的時候,首先進行的是在Java 棧的局部變量表中分配一個對象引用(reference 類型,不等同於對象自己,多是一個指向對象起始地址的引用指針,也多是指向一個表明對象的句柄)。

2. 類加載檢查

JVM 檢查這個對象是否能在常量池(指的是 Class 文件常量池)中定位到一個類的符號引用,而且檢查這個符號引用表明的類是否已經被加載、解析和初始化過。若是沒有,那必須先執行類加載過程(靜態塊、靜態變量、靜態方法加載進靜態方法區等操做)。

3. 分配內存

對象所需的內存大小在類加載完成後即可徹底肯定,所以爲對象分配內存空間其實就是怎樣把一塊肯定大小的內存從 Java 堆中劃分出來。通常有兩種分配方式:

指針碰撞
Java 堆中的內存是絕對規整的,全部用過的內存放在一邊,空閒的內存放在另外一邊,中間放着一個指針做爲分界點的指示器,分配內存就僅僅是把那個指針向空閒空間那邊挪動一段與對象大小相等的距離。

空閒列表
Java 堆中的內存並非規整的,虛擬機維護了一個列表,記錄了哪些內存塊是可用的,在分配的時候從列表中找到一塊足夠大的空間劃分給對象實例,並更新列表上的記錄。

內存分配的方式由 Java 堆是否規整決定, Java 堆是否規整又是由所採用的垃圾收集器是否帶有 compact(壓縮整理)功能決定。好比 Serial、ParNew 等基於 stop-and-copy 算法的收集器就具備 compact 功能,而 CMS 這種基於 mark-and-sweep 算法的收集器就不具備 compact 功能。

虛擬機默認使用 CAS 配上失敗重試的方式保證內存分配操做的原子性,可經過 -XX:+/-UseTLAB 指定使用 TLAB(Thread Local Allocation Buffer, 本地線程分配緩衝);

HotSpot VM 的自動內存管理系統要求對象起始地址必須是 8 字節的整數倍,換句話說,就是對象的大小必須是 8 字節的整數倍。所以,當對象實例數據部分沒有對齊時,就須要經過對齊填充來補全。

4. 初始化工做

接下來虛擬機加載非靜態塊、非靜態方法、非靜態變量,並將分配到的內存空間都初始化零值(引用類型初始化爲 null,int 類型初始化爲 0 等),這一步操做保證了對象的實例字段在 Java 代碼中能夠不賦初始值就能直接使用。

5. 對象頭設置

接下來虛擬機將進行對象頭的填充設置,HotSpot 虛擬機的對象頭包括通常兩部分信息:

第一部分(Mark Word)
存儲對象自身的運行時數據,如哈希碼、GC 分代年齡、鎖狀態標誌、線程持有的鎖、偏向線程ID、偏向時間戳等,這部分數據的長度在 32 位和 64 位虛擬機(未開啓壓縮指針)中分別爲 32bit 和 64 bit。

第二部分(類型指針)
對象指向它的類元數據的指針,虛擬機經過這個指針來肯定這個對象是哪一個類的實例。可是並非全部的虛擬機實現都必須在對象數據上保留類型指針,好比經過句柄訪問。下文會提到。

若是對象是一個數組,那麼對象頭中還必須有一塊用於記錄數組長度的數據,由於虛擬機從數組的元數據中沒法肯定數組的大小。

6.構造器工做

若是有父類,則父類按上述流程保證被加載。

7. 對象的訪問定位

如今堆中的對象實例有了,棧中的 reference 也有了,怎麼將二者關聯在一塊兒呢?目前主流的方式有使用句柄和直接指針兩種:

使用句柄
Java 堆中劃分出一塊內存做爲句柄池,reference 中存儲的就是對象的句柄地址,而句柄中包含了對象的實例數據與類型數據各自的具體地址信息。它的優勢就是 reference 存儲的是穩定的句柄地址,在對象被移動時只會改變句柄中的實例數據指針,而 reference 自己不須要修改。

直接指針
reference 中存儲的直接就是對象地址。它的好處就是速度更快,節省了一次指針定位的時間開銷。

HotSpot VM 使用的直接指針進行對象訪問。

相關文章
相關標籤/搜索