一、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 使用的直接指針進行對象通路。