1. 程式人生 > >JVM的藝術-物件建立與記憶體分配機制深度剖析

JVM的藝術-物件建立與記憶體分配機制深度剖析

# JVM的藝術-物件建立與記憶體分配機制深度剖析 > ##### 引言 本章將介紹jvm的物件建立與記憶體分配。徹底帶你瞭解jvm的建立過程以及記憶體分配的原理和區域,以及包含的內容。 > ### 物件的建立 ![](https://img2020.cnblogs.com/other/1187061/202012/1187061-20201231144506085-439010581.png) > ### 類載入的過程 ![](https://img2020.cnblogs.com/other/1187061/202012/1187061-20201231144506409-2140420186.png) **固定的類載入執行順序: 載入 驗證 準備 初始化 解除安裝 的執行順序是一定的 為什麼解析過程沒有在這個執行順序中?(接下來分析)** 什麼時候觸發類載入不一定,但是類的初始化如下四種情況就要求一定初始化。 但是初始化之前 就一定會執行 **載入 驗證 準備 三個階段**。 **觸發類載入的過程(由初始化過程引起的類載入)** 1):**使用new 關鍵字 獲取一個靜態屬性 設定一個靜態屬性 呼叫一個靜態方法。** ​ int myValue = SuperClass.value;會導致父類初始化,但是不會導致子類初始化 ​ SuperClass.Value = 3 ; 會導致父類初始化,不會導致子類初始化。 ​ SubClass.staticMethod(); 先初始化父類 再初始化子類 ​ SubClass sc = new SubClass(); 先初始化父類 子類初始化子類 2):**使用反射的時候,若發現類還沒有初始化,就會進行初始化** ​ Class clazz = Class.forName("com.hnnd.classloader.SubClass"); 3):**在初始化一個類的時,若發現其父類沒有初始化,就會先初始化父類** ​ SubClass.staticMethod(); 先初始化父類 在初始化子類 4):**啟動虛擬機器的時候,需要載入包含main方法的類.** ![](https://img2020.cnblogs.com/other/1187061/202012/1187061-20201231144507174-1073225802.png) ```java class SuperClass{ public static int value = 5; static { System.out.println("Superclass ...... init........"); } }      class SubClass extends SuperClass { static { System.out.println("subClass********************init"); } public static void staticMethod(){ System.out.println("superclass value"+SubClass.value); } } ``` **1:載入** 1.1)根據全類名獲取到對應類的位元組碼流(位元組流的來源 class 檔案,網路檔案,還有反射的Proxygeneraotor.generaotorProxyClass) 1.2)把位元組流中的靜態資料結構載入到方法區中的執行時資料結構 1.3)在記憶體中生成java.lang.Class物件,可以通過該物件來操作方法區中的資料結構(通過反射) **2:驗證** **檔案格式的驗證**: 驗證class檔案開頭的0XCAFFBASE 開頭 ​ 驗證主次版本號是否在當前的虛擬機器的範圍之類 ​ 檢測jvm不支援的常量型別 **元資料的校驗:** ​ 驗證本類是否有父類 ​ 驗證是否繼承了不允許繼承的類(final)修飾的類 ​ 驗證本類不是抽象類的時候,是否實現了所有的介面和父類的介面 **位元組碼驗證:**驗證跳轉指令跳轉到 方法以外的指令. ​ 驗證型別轉換是否為有效的, 比如子類物件賦值父類的引用是可以的,但是把父類物件賦值給子類引用是危險的 ​ 總而言之:位元組碼驗證通過,並不能說明該位元組碼一定沒有問題,但是位元組碼驗證不通過。那麼該位元組碼檔案一定是有問題:。 ​ **符號引用的驗證(發生在解析的過程中):** ​ **通過字串描述的全類名是否能找到對應的類。** ​ **指定類中是否包含欄位描述符,以及簡單的欄位和方法名稱。** **3:準備:為類變數分配記憶體以及設定初始值。** ​ 比如public static int value = 123; ​ 在準備的過程中 value=0 而不是123 ,當執行類的初始化的方法的時候,value=123 ​ 若是一個靜態常量 ​ public static final int value = 9; 那麼在準備的過程中value為9. ​ **4:解析 :把符號引用替換成直接引用** ​ 符號引用分類: ​ CONSTANT_Class_info 類或者介面的符號引用 ​ CONSTANT_Fieldref_info 欄位的符號引用 ​ CONSTANT_Methodref_info 方法的符號引用 ​ CONSTANT_intfaceMethodref_info- 介面中方法的符號引用 ​ CONSTANT_NameAndType_info 子類或者方法的符號引用. ​ CONSTANT_MethodHandle_Info 方法控制代碼 ​ CONSTANT_InvokeDynamic_Info 動態呼叫 直接引用: ​ 指向物件的指標 ​ 相對偏移量 ​ 操作控制代碼 **5:初始化:類的初始化時類載入的最後一步:執行類的構造器,為所有的類變數進行賦值(編譯器生成CLInit<>)** ​ 類構造器是什麼?: 類構造器是編譯器按照Java原始檔總類變數和靜態程式碼塊出現的順序來決定 ​ 靜態語句只能訪問定義在靜態語句之前的類變數,在其後的靜態變數能賦值 但是不能訪問。 ​ 父類中的靜態程式碼塊優先於子類靜態程式碼塊執行。 ​ 若類中沒有靜態程式碼塊也沒有靜態類變數的話,那麼編譯器就不會生成 Clint<>類構造器的方法。 ```java public class TestClassInit { public static void main(String[] args) { System.out.println(SubClass.sub_before_v); } } class SubClass extends SuperClass{ public static int sub_before_v = 5; static { sub_before_v = 10; System.out.println("subclass init......."); sub_after_v=0; //拋錯,static程式碼塊中的程式碼只能賦值後面的類變數 但是不能訪問。 sub_before_v = sub_after_v; } public static int sub_after_v = 10; } class SuperClass { public static int super_before_v = 5; static{ System.out.println("superclass init......"); } public static int super_after_v = 10; } ``` **6:使用** **7:解除安裝** **1.****類載入檢查** 虛擬機器遇到一條new指令時,首先將去檢查這個指令的引數是否能在常量池中定位到一個類的符號引用,並且檢查這個 符號引用代表的類是否已被載入、解析和初始化過。如果沒有,那必須先執行相應的類載入過程。 new指令對應到語言層面上講是,new關鍵詞、物件克隆、物件序列化等。 **2.****分配記憶體** 在類載入檢查通過後,接下來虛擬機器將為新生物件分配記憶體。物件所需記憶體的大小在類 載入完成後便可完全確定,為 物件分配空間的任務等同於把 一塊確定大小的記憶體從Java堆中劃分出來。 這個步驟有兩個問題: 1.如何劃分記憶體。 2.在併發情況下, 可能出現正在給物件A分配記憶體,指標還沒來得及修改,物件B又同時使用了原來的指標來分配記憶體的 情況。 **劃分記憶體的方法:** **記憶體的方法:** “指標碰撞”(Bump the Pointer)(預設用指標碰撞) 假設Java堆中記憶體時完整的,已分配的記憶體和空閒記憶體分別在不同的一側,通過一個指標作為分界點,需要分配記憶體時, 僅僅需要把指標往空閒的一端移動與物件大小相等的距離。使用的GC收集器:Serial、ParNew,適用堆記憶體規整(即沒有記憶體碎片)的情況下。 “空閒列表”(Free List) 事實上,Java堆的記憶體並不是完整的,已分配的記憶體和空閒記憶體相互交錯,JVM通過維護一個列表,記錄可用的記憶體塊資訊,當分配操作發生時,從列表中找到一個足夠大的記憶體塊分配給物件例項,並更新列表上的記錄。使用的GC收集器:CMS,適用堆記憶體不規整的情況下。 **解決併發問題的方法:** CAS(compare and swap) 虛擬機器採用CAS配上失敗重試的方式保證更新操作的原子性來對分配記憶體空間的動作進行同步處理。 本地執行緒分配緩衝(Thread Local Allocation Buffer,TLAB) 把記憶體分配的動作按照執行緒劃分在不同的空間之中進行,即每個執行緒在Java堆中預先分配一小塊記憶體。通過**­XX:+/­** **UseTLAB**引數來設定虛擬機器是否使用TLAB(JVM會預設開啟**­XX:+****UseTLAB**),­XX:TLABSize 指定TLAB大小。 **3.****初始化** 記憶體分配完成後,虛擬機器需要將分配到的記憶體空間都初始化為零值(不包括物件頭), 如果使用TLAB,這一工作過程也 可以提前至TLAB分配時進行。這一步操作保證了物件的例項欄位在Java程式碼中可以不賦初始值就直接使用,程式能訪問 到這些欄位的資料型別所對應的零值。 # 什麼是 TLAB **TLAB (Thread Local Allocation Buffer,執行緒本地分配緩衝區)是 Java 中記憶體分配的一個概念,它是在 Java 堆中劃分出來的針對每個執行緒的記憶體區域,專門在該區域為該執行緒建立的物件分配記憶體。它的主要目的是在多執行緒併發環境下需要進行記憶體分配的時候,減少執行緒之間對於記憶體分配區域的競爭,加速記憶體分配的速度。TLAB 本質上還是在 Java 堆中的,因此在 TLAB 區域的物件,也可以被其他執行緒訪問。** **如果沒有啟用 TLAB,多個併發執行的執行緒需要建立物件、申請分配記憶體的時候,有可能在 Java 堆的同一個位置申請,這時就需要對擬分配的記憶體區域進行加鎖或者採用 CAS 等操作,保證這個區域只能分配給一個執行緒。** **啟用了 TLAB 之後(-XX:+UseTLAB, 預設是開啟的),JVM 會針對每一個執行緒在 Java 堆中預留一個記憶體區域,在預留這個動作發生的時候,需要進行加鎖或者採用 CAS 等操作進行保護,避免多個執行緒預留同一個區域。一旦某個區域確定劃分給某個執行緒,之後該執行緒需要分配記憶體的時候,會優先在這片區域中申請。這個區域針對分配記憶體這個動作而言是該執行緒私有的,因此在分配的時候不用進行加鎖等保護性的操作。** **4.****設定物件頭** 初始化零值之後,虛擬機器要對物件進行必要的設定,例如這個物件是哪個類的例項、如何才能找到類的元資料資訊、對 象的雜湊碼、物件的GC分代年齡等資訊。這些資訊存放在物件的物件頭Object Header之中。 在HotSpot虛擬機器中,物件在記憶體中儲存的佈局可以分為3塊區域:物件頭(Header)、 例項資料(Instance Data) 和對齊填充(Padding)。 HotSpot虛擬機器的物件頭包括兩部分資訊,第一部分用於儲存物件自身的執行時資料, 如哈 希碼(HashCode)、GC分代年齡、鎖狀態標誌、執行緒持有的鎖、偏向執行緒ID、偏向時 間戳等。物件頭的另外一部分 是型別指標,即物件指向它的類元資料的指標,虛擬機器通過這個指標來確定這個物件是哪個類的例項。 ![](https://img2020.cnblogs.com/other/1187061/202012/1187061-20201231144517971-890954994.png) 物件頭在hotspot的C++原始碼裡的註釋如下: ```c++ 1 Bit‐format of an object header (most significant first, big endian layout below): 2 // 3 // 32 bits: 4 // ‐‐‐‐‐‐‐‐ 5 // hash:25 ‐‐‐‐‐‐‐‐‐‐‐‐>| age:4 biased_lock:1 lock:2 (normal object) 6 // JavaThread*:23 epoch:2 age:4 biased_lock:1 lock:2 (biased object) 7 // size:32 ‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐>| (CMS free block) 8 // PromotedObject*:29 ‐‐‐‐‐‐‐‐‐‐>| promo_bits:3 ‐‐‐‐‐>| (CMS promoted object) 9 // 10 // 64 bits: 11 // ‐‐‐‐‐‐‐‐ 12 // unused:25 hash:31 ‐‐>| unused:1 age:4 biased_lock:1 lock:2 (normal object) 13 // JavaThread*:54 epoch:2 unused:1 age:4 biased_lock:1 lock:2 (biased object) 14 // PromotedObject*:61 ‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐>| promo_bits:3 ‐‐‐‐‐>| (CMS promoted object) 15 // size:64 ‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐>| (CMS free block) 16 // 17 // unused:25 hash:31 ‐‐>| cms_free:1 age:4 biased_lock:1 lock:2 (COOPs && normal object) 18 // JavaThread*:54 epoch:2 cms_free:1 age:4 biased_lock:1 lock:2 (COOPs && biased object) 19 // narrowOop:32 unused:24 cms_free:1 unused:4 promo_bits:3 ‐‐‐‐‐>| (COOPs && CMS promoted object) 20 // unused:21 size:35 ‐‐>| cms_free:1 unused:7 ‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐>| (COOPs && CMS free block) ``` **5.****