對象的創建和分配

對象的創建

創建方式

1、 new 關鍵字直接創建。 new ObjectName()。

2、通過 Class 反射對象的 newInstance() 方法。ObjectName  obj  =  ObjectName.class.newInstance()。

3、通過 Class 反射對象獲取 Constructor 類,再調用其 newInstance() 方法。 ObjectName obj = ObjectName.class.getConstructor.newInstance()。

4、在類實現 Cloneable 介面的前提下,使用對象的 clone() 方法。ObjectName obj = obj.clone()。(如果內部有自定義類屬性,並且想要實現深克隆(新創建的對象和原有的對象不是同一個),那麼就需要讓該屬性類也實現 Cloneable 介面。

5、使用反序列化。(為了避免屬性丟失,需要讓類實現 Serializable 介面)

public static void main(String[] args){
        try {
            ObjectInputStream ois = new ObjectInputStream(new FileInputStream(FilePath))
            ObjectName obj = ois.readObject();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }

 

對象的記憶體布局

在對象身上,存儲了關於這個對象的所有資訊。

 

創建過程

1、根據創建對象的資訊去記憶體中存放類資訊的常量池中尋找是否存在要載入的類資訊,如果存在直接創建對象;如果不存在就先進行該類的載入。

2、為對象分配空間。這裡涉及到執行緒位置分配的安全和效率,比較複雜,會在下面詳細來說。

3、初始化分配到對應的位置。

4、設置對象的對象頭。

5、執行 init 方法(執行非靜態代理塊和實例屬性的初始化以及執行實例構造方法)

 

對象的記憶體分配

分配方式

1、指針碰撞:如果 Java 堆記憶體是規整的,也就是對象的創建位置都是緊挨著的,這樣的話直接將指針指示器向空閑方向移動要創建對象大小的距離就可以了。

2、空閑列表:如果 Java 堆記憶體是不規整的,那麼就需要維護一個空閑列表來記錄哪些位置是空閑的以及多大。在分配時就在列表上查詢,找到合適的位置分配。

 

並發安全

由於在堆的執行緒共享的,所以對象的創建分配的空間可能同時也是另外一個執行緒對象創建的分配位置,這就導致了並發問題,所以為了保證對象創建的並發安全,可以有下面兩種方式:

1、在分配空間時進行同步處理(採用 CAS +迴旋鎖的方式來保證)

2、TLAB:新的執行緒創建時會在堆中劃分一塊區域給該執行緒,後面該執行緒創建的對象都會在該位置存放,當空間不足時才使用第一種方式。(HotSpot 使用)。

 

程式碼優化

1、棧上分配。通過逃逸分析判斷創建的對象是否逃逸出方法(也就是這個對象是否在當前方法的外部被調用),如果沒有逃逸出方法,那麼就有可能直接在棧上分類空間來保存。

2、同步省略。JIT 在編譯時會判斷同步塊所使用的鎖對象是否只能被一個執行緒訪問而沒有被發布到其他的執行緒。如果沒有,那麼 JIT 編譯器在編譯這段程式碼時就會取消這段程式碼的同步。

3、分離對象(標量替換)。有的對象可能不需要作為一個連續的記憶體結構存在也可以被訪問到,那麼對象的部分(或全部)可以不存儲在記憶體,而是存儲在棧中。

標量:無法再被分解的數據。如一個類的基本數據類型屬性。

聚合量:還可以被分解的數據。如一個類的自定義屬性。

 

逃逸分析的不成熟性

關於逃逸分析目前還是處於不穩定的階段,因為無法保證逃逸分析的性能消耗一定高於其節省的性能。簡單來說就是可能執行了逃逸分析,結果發現都是逃逸出方法的對象,這樣逃逸分析並沒有提高性能,同時執行逃逸分析也消耗了一定的性能,造成得不償失。所以,逃逸分析在 JVM 中沒有實現 棧上分配的功能的,但是其還是在 JIT 中起到了優化作用。所以可以說對象都是創建在堆上的。而我們一般所說的對象創建在棧上,實際情況是因為標量替換的作用。

 

實際的對象空間分配過程

首先會判斷是否可以進行標量替換,如果可以直接使用標量替換,然後結束。不可以的話再嘗試在當前執行緒劃分的區域創建,如果區域不夠再嘗試使用 CAS+ 自旋鎖在其他位置劃分,失敗就再次嘗試,直到成功。

 

對象的訪問

Java 程式通過棧上的引用訪問堆中的對象。對象的訪問方式取決於 JVM 虛擬機上的實現,目前主流的訪問方式是句柄和直接指針。

句柄

句柄相當於一個中間表,存儲著對應實例對象的地址以及實例數據所對應類資訊的地址。

優勢:比較穩定,當對象被移動後(垃圾回收時移動對象是非常常見的事)時只需要改變句柄中的指針就可以了。句柄本身不需要改變。

 

直接指針

引用直接指向實例對象,在對象上保存對應的類資訊所在的地址。

優勢:查找快,在棧上的引用可以很快找到對應的對象。這也是 HotSpot 默認的訪問方式。

Tags: