1. 程式人生 > >JVM系列一(Java記憶體區域和物件建立).

JVM系列一(Java記憶體區域和物件建立).

一、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 等基於 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 使用的直接指標進行物件訪問