【JVM】物件的例項化與記憶體佈局(十一)
一、物件的例項化
1.1、物件建立的方式
1、new 關鍵字
① 最常見的方式就是直接 new 加構造器的方式建立
② 變形一:XXX(類名). 靜態方法,本質這種方式還去呼叫類中構造器,比如說:單例模式、日曆類(Calendar) 和一些工具類等等。
③ 變形二:XXXBuilder / XXXFactory的靜態方法
2、Class的newInstance() 反射的方式
這種方式jdk9就過時了,為什麼呢,就是它只能呼叫空參構造器,許可權必須是是public。
3、Constructor的newInstance(XXX),也是反射的方式
這種方式就可以呼叫空參或帶參的構造器,許可權沒有要求。
4、使用clone()(克隆)
這種方式不呼叫任何構造器,當前類需要是心啊Cloneable介面,實現clone()方法(通過一個物件去呼叫clone()方法,然後複製一個新的物件)。
5、使用反序列化
從檔案或者網路中獲取一個物件的二進位制流(序列化機制:可以實現資料從一個程式到另一個程式的傳遞,可以實基於本地、網路)。
6、第三方庫Objenesis
利用位元組碼技術我們可以動態的生成Constructor類物件。
程式碼示例如下:
1 public class Employee implements Cloneable, Serializable { 2 3 privateEmployee.javaint id; 4 5 private String name; 6 7 public int getId() { 8 return id; 9 } 10 11 public void setId(int id) { 12 this.id = id; 13 } 14 15 public String getName() { 16 return name; 17 } 18 19 public void setName(String name) { 20 this.name = name; 21 } 22 23 @Override 24 public String toString() { 25 return "Employee{" + 26 "id=" + id + 27 ", name='" + name + '\'' + 28 '}'; 29 } 30 31 @Override 32 public Object clone() throws CloneNotSupportedException { 33 return super.clone(); 34 } 35 }
1 public class CreateObjectTest { 2 3 // 1、new 關鍵字 4 @Test 5 public void testNew(){ 6 Employee emp = new Employee(); 7 } 8 9 // 2、Class的newInstance() 反射的方式 10 @Test 11 public void testNewINstance() throws IllegalAccessException, InstantiationException { 12 // 通過反射傳教物件 13 Employee emp = Employee.class.newInstance(); 14 System.out.println(emp); 15 } 16 17 // 3、Constructor的newInstance(XXX),也是反射的方式 18 @Test 19 public void testConstructor() throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { 20 // 通過反射得到構造器 21 Constructor<Employee> constructor = Employee.class.getConstructor(); 22 // 通過構造器建立物件 23 Employee emp = constructor.newInstance(); 24 System.out.println(emp); 25 } 26 27 // 4、使用clone()(克隆) 28 @Test 29 public void testClone() throws CloneNotSupportedException { 30 Employee emp = new Employee(); 31 emp.setId(1); 32 emp.setName("小白"); 33 System.out.println(emp.hashCode()); 34 35 // 克隆物件 36 Object emp2 = emp.clone(); 37 System.out.println(emp2); 38 System.out.println(emp2.hashCode()); 39 } 40 41 42 // 5、使用反序列化 43 @Test 44 public void testSeri() throws IOException, ClassNotFoundException { 45 Employee emp = new Employee(); 46 emp.setId(1); 47 emp.setName("小白"); 48 System.out.println(emp.hashCode()); 49 50 ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("emp.bat")); 51 ObjectInputStream ois = new ObjectInputStream(new FileInputStream("emp.bat")); 52 53 oos.writeObject(emp); 54 // 反序列化獲取物件 55 Employee emp2 = (Employee) ois.readObject(); 56 System.out.println(emp2); 57 System.out.println(emp2.hashCode()); 58 59 // 關閉流 60 ois.close(); 61 oos.close(); 62 } 63 }CreateObjectTest.java
1.2、物件建立的步驟
1.2.1、從位元組碼看待物件的建立過程
示例程式碼
1 public class ObjectTest { 2 public static void main(String[] args) { 3 Object obj = new Object(); 4 } 5 }
反編譯class檔案,命令:javap -v
main() 方法對應的位元組碼:
- 呼叫 new 指令後後,載入 Object 類
- 呼叫 Object 類的 init() 方法
1.2.2、建立物件的六個步驟
1)判斷物件對應的類是否載入、連結、初始化
-
虛擬機器遇到一條new指令,首先去檢查這個指令的引數能否在Metaspace的常量池中定位到一個類的符號引用,並且檢查這個符號引用代表的類是否已經被載入,解析和初始化。(即判斷類元資訊是否存在)。
-
如果該類沒有載入,那麼在雙親委派模式下,使用當前類載入器以ClassLoader + 包名 + 類名為key進行查詢對應的.class檔案,如果沒有找到檔案,則丟擲ClassNotFoundException異常,如果找到,則進行類載入,並生成對應的Class物件。
2)為物件分配記憶體
-
首先計算物件佔用空間的大小,接著在堆中劃分一塊記憶體給新物件。如果例項成員變數是引用變數,僅分配引用變數空間即可,即4個位元組大小
-
如果記憶體規整:採用指標碰撞分配記憶體
- 如果記憶體是規整的,那麼虛擬機器將採用的是指標碰撞法(Bump The Point)來為物件分配記憶體。
- 意思是所有用過的記憶體在一邊,空閒的記憶體放另外一邊,中間放著一個指標作為分界點的指示器,分配記憶體就僅僅是把指標往空閒記憶體那邊挪動一段與物件大小相等的距離罷了。
- 如果垃圾收集器選擇的是Serial ,ParNew這種基於壓縮演算法的,虛擬機器採用這種分配方式。一般使用帶Compact(整理)過程的收集器時,使用指標碰撞。
- 標記壓縮(整理)演算法會整理記憶體碎片,堆記憶體一存物件,另一邊為空閒區域
-
如果記憶體不規整:採用的是空閒列表來為物件分配記憶體
- 如果記憶體不是規整的,已使用的記憶體和未使用的記憶體相互交錯,那麼虛擬機器將採用的是空閒列表來為物件分配記憶體。
- 意思是虛擬機器維護了一個列表,記錄上哪些記憶體塊是可用的,再分配的時候從列表中找到一塊足夠大的空間劃分給物件例項,並更新列表上的內容。這種分配方式成為了 “空閒列表(Free List)”
- 選擇哪種分配方式由Java堆是否規整所決定,而Java堆是否規整又由所採用的垃圾收集器是否帶有壓縮整理功能決定
- 標記清除演算法清理過後的堆記憶體,就會存在很多記憶體碎片。
3)處理併發問題
-
採用CAS+失敗重試保證更新的原子性
-
每個執行緒預先分配TLAB - 通過設定 -XX:+UseTLAB引數來設定(區域加鎖機制)
-
在Eden區給每個執行緒分配一塊區域
4)初始化分配到的記憶體
所有屬性設定預設值,保證物件例項欄位在不賦值可以直接使用
5)設定物件的物件頭
將物件的所屬類(即類的元資料資訊)、物件的HashCode和物件的GC資訊、鎖資訊等資料儲存在物件的物件頭中。這個過程的具體設定方式取決於JVM實現。
6)執行init方法進行初始化
-
在Java程式的視角看來,初始化才正式開始。初始化成員變數,執行例項化程式碼塊,呼叫類的構造方法,並把堆內物件的首地址賦值給引用變數
-
因此一般來說(由位元組碼中跟隨invokespecial指令所決定),new指令之後會接著就是執行init方法,把物件按照程式設計師的意願進行初始化,這樣一個真正可用的物件才算完成創建出來。
1.2.2、從位元組碼角度看 init 方法
給物件屬性賦值的順序:
-
屬性的預設值初始化
-
顯示初始化/程式碼塊初始化(並列關係,誰先誰後看程式碼編寫的順序)
-
構造器初始化
- 示例程式碼
1 public class Customer{ 2 int id = 1001; 3 String name; 4 Account acct; 5 6 { 7 name = "匿名客戶"; 8 } 9 10 public Customer(){ 11 acct = new Account(); 12 } 13 14 } 15 class Account{ 16 17 }
- init() 方法的位元組碼指令:
- 屬性的預設值初始化:
id = 1001;
- 顯示初始化/程式碼塊初始化:
name = "匿名客戶";
- 構造器初始化:
acct = new Account();
- 屬性的預設值初始化:
總結:
程式碼參考:
1 public class ParentTest { 2 public static String PARENT_STATIC_FIELD = "父類-靜態屬性"; 3 4 // 父類-靜態塊 5 static { 6 System.out.println(PARENT_STATIC_FIELD); 7 System.out.println("父類-靜態程式碼塊"); 8 } 9 10 public static String parentField = "父類-非靜態屬性"; 11 12 // 父類-非靜態塊 13 { 14 System.out.println(parentField); 15 System.out.println("父類-非靜態程式碼塊"); 16 } 17 18 public ParentTest() { 19 System.out.println("父類—無參建構函式"); 20 } 21 } 22 23 class InitOder extends ParentTest { 24 public static String STATIC_FIELD = "子類-靜態屬性"; 25 26 // 靜態塊 27 static { 28 System.out.println(STATIC_FIELD); 29 System.out.println("子類-靜態程式碼塊"); 30 } 31 32 public String field = "子類-非靜態屬性"; 33 34 // 非靜態塊 35 { 36 System.out.println(field); 37 System.out.println("子類-非靜態程式碼塊"); 38 } 39 40 public InitOder() { 41 System.out.println("子類-無參建構函式"); 42 } 43 44 public static void main(String[] args) { 45 InitOder test = new InitOder(); 46 System.out.println("~~~~~~~~第二次建立物件~~~~~~~"); 47 InitOder test2 = new InitOder(); 48 } 49 }View Code
2、物件的記憶體佈局