一、JVM 內存區域
堆 - Heap
線程共享,JVM中最大的一塊內存,此內存的唯一目的就是存放對象實例,Java 堆是垃圾收集器管理的主要區域,因此很多時候也被稱為“GC堆”(Garbage Collected Heap),可以通過 -Xmx 和 -Xms 參數來控制該區域大小。
方法區 - 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() 方法。
虛擬機棧/本地方法棧
線程私有,生命周期與線程相同,描述的是 Java 方法執行的內存模型:每個方法執行的同時都會創建一個棧幀(Stack Frame)用於存儲局部變量表、操作數棧、動態鏈接、方法出口等信息。每個方法從調用直到執行完成的過程,就對應着一個棧幀入棧到出棧的過程。
局部變量表存放了編譯器可知的各種基本類型(boolean、byte、char、short、int、float、long、double)、對象引用(reference 類型)、returnAddress 類型(指向了一條字節碼執行的地址)。其中64位長度的 long 和 double 類型的數據會占用兩個局部變量空間(Slot)。局部變量表所需的內存空間在編譯期間完成分配,在方法運行期間不會改變局部變量表的大小。
虛擬機棧和本地方法棧的區別不過是虛擬機棧為虛擬機執行 Java 方法服務,而本地方法棧為虛擬機執行 Native 方法服務。HotSpot 虛擬機直接把虛擬機棧和本地方法棧合二為一。可通過 -Xss 參數設置虛擬機棧大小,-Xoss 參數設置本地方法棧(HotSpot 虛擬機上該參數不生效)。
程序計數器
線程私有,一塊較小的內存空間,它可以看作是當前線程所執行的字節碼的行號指示器,此內存區域是唯一一個在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)一樣。
二、Java 對象創建
接下來看看我們平常的一個 new 操作在 JVM 中又是怎樣一種過程呢?(討論的是普通 Java 對象,不包括數組和 Class 對象等)。
1. 棧空間分配
當執行 new 操作的時候,首先進行的是在Java 棧的局部變量表中分配一個對象引用(reference 類型,不等同於對象本身,可能是一個指向對象起始地址的引用指針,也可能是指向一個代表對象的句柄)。
2. 類加載檢查
JVM 檢查這個對象是否能在常量池(指的是 Class 文件常量池)中定位到一個類的符號引用,並且檢查這個符號引用代表的類是否已經被加載、解析和初始化過。如果沒有,那必須先執行類加載過程(靜態塊、靜態變量、靜態方法加載進靜態方法區等操作)。
3. 內存分配
對象所需的內存大小在類加載完成后便可完全確定,因此為對象分配內存空間其實就是怎樣把一塊確定大小的內存從 Java 堆中划分出來。一般有兩種分配方式:
指針碰撞
Java 堆中的內存是絕對規整的,所有用過的內存放在一邊,空閑的內存放在另一邊,中間放着一個指針作為分界點的指示器,分配內存就僅僅是把那個指針向空閑空間那邊挪動一段與對象大小相等的距離。
空閑列表
Java 堆中的內存並不是規整的,虛擬機維護了一個列表,記錄了哪些內存塊是可用的,在分配的時候從列表中找到一塊足夠大的空間划分給對象實例,並更新列表上的記錄。
內存分配的方式由 Java 堆是否規整決定, Java 堆是否規整又是由所采用的垃圾收集器是否帶有 compact(壓縮整理)功能決定。比如 Serial、ParNew 等基於復制算法的收集器就具有 compact 功能,而 CMS 這種基於標記-清除算法的收集器就不具有 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 使用的直接指針進行對象訪問。