天天看點

深入了解JVM中的對象1.對象的建立方式及是否調用構造方法2.對象的建立流程3.對象的記憶體布局4.對象的通路定位5.對象在哪配置設定記憶體

1.對象的建立方式及是否調用構造方法

  1. new 無參/有參構造
  2. clone 不調用構造方法,由 JVM 建立新的對象并指派字段。
  3. 反序列化 調用第一個沒有實作序列化接口的父類的無參構造,如果沒有,則調用 Object 類的無參構造
  4. Class.newInstance 無參構造,實際上是調用 5 的無參構造
  5. Constructor.newInstance(…args) 無參/有參構造

2.對象的建立流程

public class Test {
    private int anInt = 1;
    private static int staticInt = 2;

    public void func(){
    }
    
    public static int staticFunc(){
        return 6+9;
    }
}
           

Test test = new Test();

為例

new 關鍵字建立 Test 對象,被 javac 編譯器編譯為三個位元組碼指令 new ,dup ,invokespecial。

new 關鍵字觸發類的初始化階段,如果該類是第一次使用,還未經曆類的加載階段。則先檢查對象所屬類型是否已經被加載到記憶體中。如果沒有,則進行類的加載。

2.1.類的加載過程

1.加載階段

記含有

Test test = new Test();

代碼的類為 C,通過加載類 C 的類加載器 CL,根據 Test 類的全限定名去擷取描述該類的二進制位元組流。對于任意一個類,都是根據加載它的類加載器及其本身來确定類的唯一性。是以我們可以讓不同的類加載器,加載同一個類,那麼方法區中會存在兩個類型。類加載器不會直接嘗試加載 Test 類,會遵循雙親委派模型,将加載請求遞交給父加載器。直到父加載器無法找到目标類時,子加載器才會去加載 Test 類。

類加載器找到目标類後,首先進行檔案格式驗證包括驗證魔數,驗證版本号等

驗證通過後,讀取靜态類型資訊并存儲在運作時資料區域->方法區(jdk8:Metaspace)中,以 instanceKlass 對象的形式存在。同時在堆中建立 Class 對象作為通路方法區 instanceKlass 對象的入口。

instanceKlass 内容包括:類型資訊,常量池,靜态變量、即時編譯後的位元組碼。

2.驗證階段

各種驗證

3.準備階段

為類中靜态變量配置設定記憶體,并賦零值。

為類中 static final 修飾的常量(ConstantValue)配置設定記憶體并指派。

4.解析階段

将符号引用解析為直接引用。符号引用是指在編譯期間,找到目标的唯一辨別(Class_info、Fieldref_info、Methodref_info)。直接引用是指在運作期間,記憶體中找到目标的唯一辨別(記憶體位址)

5.初始化階段

由 JVM 執行類初始化方法< clinit>(),包括為靜态變量指派,執行靜态代碼塊。

類加載階段完成後,開始執行 new 位元組碼指令,建立對象,為對象配置設定記憶體空間。

2.2對象建立過程

1.配置設定記憶體

幾乎所有執行個體都在堆上配置設定,而堆又是線程共享的。為了避免在并發情況下為對象配置設定記憶體時,多個線程同時操作同一塊記憶體區域,需要有相應的線程安全方法。

為線程配置設定本地線程配置設定緩沖(Thread Local Allocate Buffer)TLAB,線程在自己的緩沖區中為對象配置設定記憶體,不夠用再去申請,申請記憶體過程才需要同步鎖定。

2.将配置設定到的記憶體空間賦零值(除對象頭)

這一過程保證了,對象的執行個體成員變量在不賦初始值的情況下也有預設零值,可以直接被通路。

3.JVM 對對象頭進行設定

JVM 中對象的存在形式為 Oop 對象。Oop 對象中分為1.對象頭 2.執行個體資料 3.對齊填充

對象頭包括 1.Markword(存儲對象哈希碼,鎖标記等) 2.Metadata類型指針(指向方法區中的 instanceKlass 對象)

執行個體資料指對象從父類繼承而來的執行個體資料 及 自己的執行個體成員變量。

JVM 在此階段根據運作狀态設定對象頭内容。

到這裡,對于 JVM 來說,一個對象已經建立好了。但對于java程式來說還沒完,還需要對執行個體成員進行初始化。

下一步執行 invokespecial 位元組碼指令,執行執行個體初始化方法< init>()。

執行執行個體初始化方法< init>()

包括執行個體成員變量指派,執行執行個體代碼塊,執行無參構造方法。

3.對象的記憶體布局

test 對象對應的堆中的 Oop 對象

深入了解JVM中的對象1.對象的建立方式及是否調用構造方法2.對象的建立流程3.對象的記憶體布局4.對象的通路定位5.對象在哪配置設定記憶體

對象頭:mark :1 未鎖定狀态 + metadata :類型指針

執行個體資料: anInt:1

Test 類對應的方法區中的 instanceKlass 對象

深入了解JVM中的對象1.對象的建立方式及是否調用構造方法2.對象的建立流程3.對象的記憶體布局4.對象的通路定位5.對象在哪配置設定記憶體

instanceKlass 中的 _java_mirror 屬性,就是 Test 類對應的 Class 對象。test.getClass() 實際上是通過 Oop 對象中的類型指針找到方法區中 instanceKlass 對象,通過 instanceKlass 找到 Class 對象。

類中的靜态變量在 Class 對象末尾存儲,staticInt:2

非私有執行個體方法(虛方法)在 instanceKlass 的 vtable 中存儲。

vtable_len:6 虛方法表長度為6,是由于 vtable 中首先存儲父類的 vtable, Test 的父類是 Object,Object 擁有 5 個虛方法(hashCode,equals,clone,finalize,toString)。

4.對象的通路定位

  1. 直接指針

通過棧幀中存儲的 reference 資料儲存的記憶體位址直接通路到堆中代表對象的 Oop 對象,再通過 Oop 對象中的類型指針找到方法區中代表類型的 instanceKlass 對象。

好處:快,一步到位

壞處:對象被GC移動時,得修改棧幀中 reference 的值

  1. 句柄

通過棧幀中存儲的 reference 資料儲存的句柄池中的句柄(堆中Oop對象記憶體位址+方法區instanceKlass類型對象記憶體位址)找到 Oop、instanceKlass 對象。

好處:對象被GC移動時,不需要修改棧幀中 reference 的值

壞處:較慢,需要經曆兩次尋址才能找到對象

5.對象在哪配置設定記憶體

java 中的所有執行個體都是在堆上配置設定記憶體的嗎?

public void aMethod(){
    Object anObject = new Object();
}
           

以上代碼中,anObject 對象與 aMethod() 方法生命周期一緻,方法被調用、方法棧幀被壓入虛拟機棧時,對象存活。方法執行結束,棧幀彈棧時,棧幀銷毀、對象不再使用。

如果 anObject 對象在堆上配置設定記憶體,因為堆被所有線程共享,在配置設定記憶體時需要進行同步。方法結束時,對象不再使用,需要由GC進行垃圾回收。

我們平時寫代碼時,類似以上代碼中與方法聲明周期一緻的對象有很多,這些對象在同一時刻隻會被一個線程通路,不會逃逸出線程之外。

試想,如果這些對象在棧上配置設定記憶體,那麼即不用擔心線程安全,因為虛拟機棧是線程私有的。也不用擔心垃圾回收,因為對象可以随着棧幀的銷毀而消亡,直接省去了垃圾回收的步驟。

何樂而不為呢?

5.1JVM 性能增強:逃逸分析

對執行個體的動态作用域進行分析,分析執行個體的逃逸範圍,進行優化。

  • 從不逃逸

    對象在方法中定義,不傳遞到其他方法,與方法的生命周期相同。

  • 方法逃逸

    對象在方法中定義,作為參數傳遞到其他方法,被外部方法所引用。

  • 線程逃逸

    對象會被外部線程通路到,譬如指派給可以在其他線程中通路到的執行個體成員變量。

1.棧上配置設定

如果一個對象符合從不逃逸的情況,與方法的生命周期一緻。那麼可以為這個對象在棧上配置設定記憶體。這樣做的好處 1.線程安全 2.不需要垃圾回收

2.标量替換

不允許對象逃逸出方法範圍内。

3.同步消除

如果可以确定一個對象不會被外部線程所通路,那麼就可以對這個對象進行同步消除,消除同步措施。