Java對象的創建及存儲位置

對象的創建

對象創建過程(“微觀”)

  1. 當使用構造器創建對象或靜態方法、非常數靜態域首次被訪問時,Java解釋器必須查找類路徑,定位.class文件
  2. 加載.class文件,創建Class對象,有關靜態初始化的所有動作都會執行(靜態初始化)
  3. 當使用new關鍵字創建對象時,在堆上分配足夠的存儲空間
  4. 存儲空間清零/null
  5. 執行所有出現於字段定義處的初始化動作(非靜態初始化)
  6. 執行構造器

對象的創建過程(“宏觀”)

  1. 基類靜態(static)字段初始化
  2. 本類靜態(static)字段初始化
  3. 基類非靜態字段初始化
  4. 本類非靜態字段初始化
  5. 執行基類構造器
  6. 執行本類構造器

列:

public class Test{
	public static void main(String[] args){
		B b=new B();
	}
}
class A{
    static int ai=10;
    static{
        System.out.println("ai="+ai);
    }
    public A(){
        System.out.println("A");
    }
}
class B extends A{
    static int bi=100;
    static{
        System.out.println("bi="+bi);
    }
    public B(){
        System.out.println("B");
	}
}

根據對象的創建過程來看,創建B類的對象過程:

  • 第一步:基類靜態字段ai初始化,且輸出ai=10;
  • 第二步:本類靜態字段bi初始化,且輸出bi=100;
  • 第三步:執行基類構造器,輸出A;
  • 第四步:執行本類構造器,輸出B;

故程序輸出:

ai=10

bi=100

A

B

對象的存儲

Java採用了動態內存分配方式。每當想要創建新對象時,就要使用new關鍵字來構建此對象的動態實例。

很多時候我們都認爲通過new關鍵字創建的對象都是被分配在堆上,包括Java編程思想第四版P14也有說到“只能以一種方式創建對象(在堆上創建)”,但這種說法是有一定問題的,Java中new出來的對象並不一定分配在堆上,主要受逃逸分析和TLAB(Thread Local Allocation Buffer)影響,對象可能會被分配到棧上。

逃逸分析:這裏不說逃逸分析是什麼,只需要知道它是一種算法,目的在於分析出Java對象引用的使用範圍,然後由編譯器利用逃逸分析的結果,對程序進行優化。Java在Java SE 6u23及以後的版本中默認開啓了逃逸分析。經過逃逸分析可以得到三種對象的逃逸狀態:

第一種:方法逃逸,對象引用逃出了方法,分配到堆內存上

第二種:線程逃逸,如類變量或實例變量,可能被其他線程訪問到,分配到堆內存上

第三種:NoEscape(無逃逸),同步消除、棧上分配、標量替換。如:可以將這個對象分配到棧內存上

舉個例子:

public class Test{
	public static A a2;
//方法逃逸
	public A run1(){
		A a1=new A();
		return a1;
	}
//線程逃逸
	public void run2(A a2){
		this.a2=a2;
	}
//無逃逸
	public void run3(){
		A a3=new A();
	}
}
class A{}

程序中:

對象引用a1在方法run1中被定義並返回,此爲方法逃逸,該對象分配到堆

類變量a2在方法run2被賦值,因爲該類變量可被其他線程訪問,此爲線程逃逸,該對象分配到堆

對象引用a3在方法run3中被定義,且爲無逃逸,該對象可以被分配到棧

TLAB:TLAB是JVM在內存新生代(Eden Space)中開闢的一小塊線程私有區域,在Java程序中有很多用過即丟的小對象,它們不存在線程共享也適合被快速GC,所以對於這些小對象通常JVM會優先分配到TLAB上

Java中每個線程都有一個自己的緩衝區TLAB,每個TLAB只有一個線程可以操作

 

強調

  1. 編譯器強制要求初始化基類,並要求在構造器起始處就要這麼做,這也是創建對象時先執行基類構造器,再執行本類構造器的原因。

  2. 棧上分配:在一般應用中,不會逃逸的局部對象佔比很大,如果使用棧上分配,那大量對象會隨着方法結束而自動銷燬,垃圾回收系統壓力就小很多。
  3. 同步消除:線程同步本身比較耗時,如果確定一個變量不會逃逸出線程,無法被其它線程訪問到,那這個變量的讀寫就不會存在競爭,對這個變量的同步措施可以清除。
  4. 標量替換:標量就是不可分割的量,java中基本數據類型,reference類型都是標量。相對的一個數據可以繼續分解,它就是聚合量。把一個對象拆散,將其成員變量恢復到基本類型來訪問就叫做變量替換。如果逃逸分析證明一個對象不會被外部訪問,並且這個對象可以被拆散的話,程序真正執行的時候將可能不創建這個對象,而是直接在棧上創建若干個成員變量。

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章