1. 程式人生 > >從類檔案分析Java類裝載過程方法的呼叫和的呼叫

從類檔案分析Java類裝載過程方法的呼叫和的呼叫

class Print{
	Print(String s){
		System.out.println("Step "+s);
	}
	Print(String s,double i){
		System.out.println("Step "+s+"  隨機數 "+i);
	}
}

abstract class Glyph {
	abstract void draw();
	Print p2 = new Print("父類成員變數");
	static Print p3= new Print("父類靜態成員變數");
	static Print p6= new Print("父類靜態成員變數",Math.random());
	Glyph() {
		System.out.println("父類構造方法");
		System.out.println("Glyph() before draw()");
		draw(); 
		System.out.println("Glyph() after draw()");
	}
}

class RoundGlyph extends Glyph {
      int radius = 1;
      String s = "abcdefg";
	  static Print p4= new Print("子類靜態成員變數");
	  static Print p5= new Print("子類靜態成員變數",Math.random());
	  Print p1 = new Print("子類成員變數");
	  RoundGlyph(int r) {
		  System.out.println("子類構造方法");
		  radius = r;
		  System.out.println(
	      "RoundGlyph.RoundGlyph(), radius = "
	      + radius);
	  }
	  void draw() { 
		  System.out.println("子類draw方法, radius = " + radius+" 字串 "+s);
	  }
}

public class PolyConstructors {
	public static void main(String[] args) {
		new RoundGlyph(5);
	}
}

執行結果如下:

Step 父類靜態成員變數
Step 父類靜態成員變數  隨機數 0.8817076586261154
Step 子類靜態成員變數
Step 子類靜態成員變數  隨機數 0.937529828526132
Step 父類成員變數
父類構造方法
Glyph() before draw()
子類draw方法, radius = 0 字串 null
Glyph() after draw()
Step 子類成員變數
子類構造方法
RoundGlyph.RoundGlyph(), radius = 5

結果分析:(根據執行結果大概分析)

當呼叫子類的構造方法時,會使觸發類的初始化。首先類裝載器裝載子類,發現子類有繼承時再裝載父類,此時並沒有對類進行初始化,在採取其他任何操作之前,為物件分配的儲存空間初始化成二進位制零。然後再按類的初始化順序依次初始化成員變數:

首先初始化父類靜態語句,其次是成員變數,最後是構造方法,然後初始化子類。

這是大概流程,還有很多問題:

1.為什麼子類載入到jvm知道要去載入父類,應為類的載入只是把類的二進檔案流載入到記憶體,還沒有執行任何程式碼,怎麼知道去載入父類?

類的具體載入過程

首先,那些情況會使系統去載入一個類:

1.遇到new,getstatic,putstatic,invokestatic這4條位元組碼指令時,假如類還沒進行初始化,則馬上對其進行初始化工作。其實就是3種情況:用new例項化一個類時、讀取或者設定類的靜態欄位時(不包括被final修飾的靜態欄位,因為他們已經被塞進常量池了)、以及執行靜態方法的時候。

2.使用java.lang.reflect.*的方法對類進行反射呼叫的時候,如果類還沒有進行過初始化,馬上對其進行。

3.初始化一個類的時候,如果他的父親還沒有被初始化,則先去初始化其父親。

4.當jvm啟動時,使用者需要指定一個要執行的主類(包含static void main(String[] args)的那個類),則jvm會先去初始化這個類。

然後,系統正式載入一個類:

這一塊虛擬機器要完成3件事:

1.通過一個類的全限定名來獲取定義此類的二進位制位元組流。

2.將這個位元組流所代表的靜態儲存結構轉化為方法區的執行時資料結構。

3.在java堆中生成一個代表這個類的java.lang.Class物件,作為方法區這些資料的訪問入口。

此時系統還沒有執行任何程式碼,接下來是對類的檢驗,驗證是否符合規範等

包括檔案格式驗證->元資料驗證->位元組碼驗證->符號引用驗證

接下來進入準備階段,這時應該就是上面粗略結果分析的:在採取其他任何操作之前,為物件分配的儲存空間初始化成二進位制零

此時也並沒有對靜態成員賦值

在對二進位制檔案進行解析後就進入初始化了:

此時就回到了會使系統載入一個類的第3條,當初始化一個類時,如果他的父親還沒有被初始化,則先去初始化其父親。

Object應該是第一個被初始化的

此時開始執行真正的程式碼,第一個被執行的是編譯時系統自動新增的<clinit>()方法,該方法是類載入時執行的第一個方法,<clinit>();方法與類構造方法不一樣,他不需要顯示得呼叫父類的<clinit>();方法,虛擬機器會保證子類的<clinit>();方法在執行前父類的這個方法已經執行完畢了,也就是說,虛擬機器中第一個被執行的<clinit>();方法肯定是Object類的<clinit>()方法。該方法只執行一次。

RoundGlyph類的<clinit>()方法:


可以看出,所有靜態成員都在這裡初始化

到上一步,類的載入已經完成,接下來就是類的初始化:

類的初始化是系統執行<init>方法(這個也是系統自動新增)檢視init方法可以看出,init執行的順序是:先初始化成員變數,最後再呼叫類的構造方法,所以構造方法總是最後呼叫


由上圖可以看出,在程式退出之前呼叫了構造方法,所以類的初始化順序總是先成員變數,然後再構造方法。在這之前已經在<clinit>中初始化了靜態變數。對於final常量比較特別,它是在類裝載的時候初始化的。
由上圖可以看出,對於類中的域,常量有一個特別的分類,其中有一個specific info資訊,是其他域沒有的,可以讓其在類裝載時初始化。

類檔案中成員變數的存貯

對一個簡單的類成員在類常量池中的儲存
public class TestMain {
	private int i = 222222;
	private int i1 = 11;
	private int i22 = 22;
	private long l = 66666;
	private double d = 99999;
	private Other o = new Other();
	private static String s = "abc";
	private static String s2 = "abcdef";
	private  String s3 = "abcdefghijk";
	TestMain(){
		i = 2;
	}
	public void f(){
		i = 111111;
		System.out.println("test");
	}
	public static void main(String[] args) {
		new TestMain();
	}
}
對於類檔案中的常量池,當類載入後會被放置到記憶體中的方法區中的常量池,其中儲存了各種常量和字面值。 但是當int型數值很大是,會被放置到常量池中,比較小時不會出現在常量池,double,float直接放置在常量池,還有static等,對於基本資料型別的值應該儲存在執行時堆疊的。怎麼在常量池中也存在。

其中對於int型的i,定義值為222222,存放在常量池,i1和i22直接用立即數入棧,直接在指令中儲存資料。