類的生命周期:分為裝載,鏈接,初始化
如圖:
1)裝載:查找并裝載類型的二進制數據
2)連接:執行驗證,準備,和解析(可選)
a) 驗證:確保導入類型正確
b) 準備:為類變量分配內存,并將其初始化為默認值
c) 解析:把類型中的符號引用轉換成直接引用
3)初始化:把類變量初始化為默認初值
隨著Java虛擬機裝載了一個類,并執行了一些它選擇進行的驗證之后,類就可以進入準備階
段了。在準備階段,Java虛擬機為類變量分配內存,設置默認初始值:但在到達初始化階段之前,
類變量都沒有被初始化為真正的初始值。(在準備階段是不會執行Java代碼的。)在準備階段,虛
擬機把給類變量新分配的內存根據類型設置為默認值。
為了準備讓一個類或者接口被"首次主動"使用,最后一個步驟就是初始化,也就是為類變量
賦予正確的初始值。這里的”正確”初始值指的是程序員希望這個類變量所具備的起始值。正
確的初始值是和在準備階段賦予的默認初始值對比而言的。前面說過,根據類型的不同,類變
量已經被賦予了默認初始值。而正確的初始值是根據程序員制定的主觀計劃面生成的。
在Java代碼中,一個正確的初始值是通過類變量初始化語句或者靜態初始化語句給出的。
1)一個類變量初始化語句是變量聲明后面的等號和表達式:
2)靜態初始化語句是一個以static開頭的程序塊
example :
public class Example1 {
// 類變量初始化語句
static int value = (int) (Math.random()*6.0);
// 靜態初始化語句
static{
System.out.println("this is example");
}
}
所有的類變量初始化語句和類型的靜態初始化器都被Java編譯器收集在—起,放到——個特殊
的方法中。對于類來說,這個方法被稱作類初始化方法;對于接口來說,它被稱為接口初始化
方法。在類和接口的Javaclass文件中,這個方法被稱為”<clinit>”。通常的Java程序方法是無法
調用這個<clinit>方法的。這種方法只能被Java虛擬機調用
clinit>()方法
前面說過,Java編譯器把類變量初始化語句和靜態初始化浯句的代碼都放到class文件的
<clinit>()方法中,順序就按照它們在類或者接門聲明中出現的順序。
example:
public class Example1 {
static int width;
static int height = (int) (Math.random()*6.0);
static{
width = (int) (Math.random()*3.0);
}
}
java 編譯器生成下面<clinit>方法:
0 invokestatic java.lang.Math.random
3 ldc2_w 6.0 (double)
6 dmul
7 d2i
8 putstatic Example1.height
11 invokestatic java.lang.Math.random
14 ldc2_w 3.0 (double) 17 dmul
18 d2i
19 putstatic Example1.width
22 return
clinit 方法首先執行唯一的類變量初始化語句初始化heght,然后在靜態初始化語句中
初始化width(雖然它聲明在height之前,但那僅僅是聲明了類變量而不是類變量初始化語句).
除接口以外,初始化一個類之前必須保證其直接超類已被初始化,并且該初始化過程是由 Jvm 保證線程安全的。
另外,并非所有的類都會擁有一個 <clinit>() 方法。
1)如果類沒有聲明任何類變量,也沒有靜態初始化語句,那么它不會有<clinit>()方法。
2)如果聲明了類變量但是沒有使用類變量初始化語句或者靜態初始化語句初始它們,那么類不會有<clinit>()方法。
example:
public class example{
static int val;
}
3)如果類僅包含靜態 final 變量的類變量初始化語句,并且類變量初始化語句是編譯時常量表達式,類不會有<clinit>()方法。
example:
public class Example {
static final String str ="abc";
static final int value = 100;
}
這種情況java編譯器把 str 和 value 被看做是常量,jvm會直接使用該類的常量池或者在字節碼中直接存放常量值。該類不會被加載。
如果接口不包含在編譯時解析成常量的字段初始化語句,接口中就包含一個<clinit>()方法。
example:
interface Example{
int i =5;
int hoursOfSleep = (int) (Math.random()*3.0);
}
字段hoursOfSleep會被放在<clinit>()方法中(比較詭異???它被看作類變量了),而字段i被看作是編譯時常量特殊處理(JAVA語法規定,接口中的變量默認自動隱含是public static final)。
java 編譯器生成下面<clinit>方法:
0 invokestatic java.lang.Math.random
3 ldc2_w 3.0 (double)
6 dmul
7 d2i
8 putstatic Example.hoursOfSleep
11 return
主動使用和被動使用
在前面講過,Java虛擬機在首次主動使用類型時初始化它們。只有6種活動被認為是主動使
用:
1)創建類的新實例,
2)調用類中聲明的靜態方法,
3)操作類或者接口中聲明的非常量靜態字段,
4)調用JavaAPI中特定的反射方法
5)初始化一個類的子類;
6)以及指定一個類作為Java虛擬機啟動時的初始化類。
使用一個非常量的靜態字段只有當類或者接口的確聲明了這個字段時才是主動使用、比如,
類中聲明的字段可能會被子類引用;接口中聲明的字段可能會被子接口或者實現了這個接口的
類引用。對于子類、子接口和實現接口的類來說.這就是被動使用(使用它們并不會觸發
它們的初始化)。下面的例子說明了這個原理:
class NewParement{
static int hoursOfSleep = (int) (Math.random()*3.0);
static{
System.out.println("new parement is initialized.");
}
}
class NewbornBaby extends NewParement{
static int hoursOfCry = (int) (Math.random()*2.0);
static{
System.out.println("new bornBaby is initialized.");
}
}
public class Example1 {
public static void main(String[] args){
int hours = NewbornBaby.hoursOfSleep;
System.out.println(hours);
}
static{
System.out.println("example1 is initialized.");
}
}
運行結果:
example1 is initialized.
new parement is initialized.
0
NewbornBaby 沒有被初始化,也沒有被加載。
對象的生命周期
當java虛擬機創建一個新的類實例時不管明確的還是隱含的,首先要在堆中為保存對象的實例變量分配內存,包含所有在對象類中和它超類中
聲明的變量(包括隱藏的實例變量)都要分配內存。其次賦默認初值,最后賦予正確的初始值。
java編譯器為每個類都至少生成一個實例初始化方法 "<init>()"與構造方法相對應。
如果構造方法調用同一個類中的另一個構造方法(構造方法重載),它對應的init<>():
1)一個同類init<>()調用。
2)對應構造方法體代碼的調用。
如果構造方法不是通過this()調用開始,且對象不是Object 它對應的init<>():
1)一個超類init<>()調用。
2)任意實例變量初始化代碼調用。
3)對應構造方法體代碼的調用。
如果上述對象是Object,則去掉第一條。如果構造方法明確使用super()首先調用對應超類init<>()其余不變。
下面的例子詳細說明了實例變量初始化(摘自Java Language Specification)
class Point{
int x,y;
Point(){x=1;y=1;}
}
class ColoredPoint extends Point{
int color = OxFF00FF;
}
class Test{
public static void main(String[] args){
ColoredPoint cp = new ColoredPoint();
System.out.println(cp.color);
}
}
首先,為新的ColoredPoint實例分配內存空間,以存儲實例變量x,y和color;然后將這些變量初始化成默認值
在這個例子中都是0。
接下來調用無參數的ColoredPoint(),由于ColorPoint沒有聲明構造方法,java編譯器會自動提供如下的構造方
法:ColoredPoint(){super();}。
該構造方法然后調用無參數的Point(),而Point()沒有顯示的超類,編譯器會提供一個對其無參數的構造方法的
隱式調用:Point(){super();x=1;y=1}。
因此將會調用到Object();Object類沒有超類,至此遞歸調用會終止。接下來會調用Object任何實例初始化語句
及任何實例變量初始化語句。
接著執行Object()由于Object類中未聲明這樣的構造方法。因此編譯器會提供默認的構造方法object(){}。
但是執行該構造方法不會產生任何影響,然后返回。
接下來執行Point類實例變量初始化語句。當這個過程發生時,x,y的聲明沒有提供任何初始化表達式,因此這個
步驟未采取任何動作(x,y 仍為0);
接下來執行Point構造方法體,將x,y賦值為1。
接下來會執行類ColoredPoint的實例變量初始化語句。把color賦值0xFF00FF,最后執行ColoredPoint構造方法體
余下的部分(super()調用之后的部分),碰巧沒有任何語句,因此不需要進一步的動作,初始化完成。
與C++不同的是,在創建新的類實例期間,java編程語言不會為方法分派來指定變更的規則。如果調用的方法在被
初始化對象的子類中重寫,那么就是用重寫的方法。甚至新對象被完全初始化前也是如此。編譯和運行下面的例子
class Super{
Super(){printThree();}
void printThree{System.out.println("Three");}
}
class Test extends Super{
int three = (int)Math.PI; // That is 3
public static void main(String args[]){
Test t = new Test();
t.printThree();
}
void printThree(){System.out.println(three);}
}
輸出:
0
3
這表明Super類中的printThree()沒有被執行。而是調用的Test中的printThree()。