前言
YY:Kitty兼雄,我最近在看Thinking in Java 這本書
Kitty:喔?是么案怯,你不是一直覺得那本書又厚又乏味君旦,代碼還非常不用戶友好,難以閱讀嘲碱,而總是停留在第一章么,這次不會還是停留在第一章吧(_ _)
YY:好啦局蚀,這不人家感覺Java基礎還是需要打打扎實么麦锯,所以就只能硬著頭皮看這本被譽為Java屆圣經的神書咯!好不容易下定決心一定要認認真真看琅绅,你就別拿人家打趣兒了(捧臉害羞狀)
Kitty:好樣的嘛扶欣,那祝你收獲大大的咯!
YY:嘿嘿千扶!昨天看了第五章--初始化和清理料祠,才發(fā)現(xiàn)寫了那么久的Java,都還沒認真研究過從點擊“運行”那一刻開始澎羞,我們的代碼都是以怎樣的規(guī)則和順序一條一條被JVM執(zhí)行的呢髓绽,你有想過這個問題么?
Kitty:呃呃妆绞。顺呕。。這個嘛括饶,還真沒考慮過株茶,那么你說說是怎么回事唄!
YY:好呀图焰,正好我昨天寫了一個小Demo幫助我理解启盛,那么我就用這個小Demo和你講講,順便鞏固一下我的知識吧,要是有講錯的或者講得不好的地方僵闯,你可得幫我指出來哈卧抗,不然我還糊里糊涂的以為自己都理解對了呢!
Kitty:好的好的棍厂,木有問題(親愛的讀者朋友們颗味,你們要是發(fā)現(xiàn)文中有不足之處,一定一定要給小編我指出來哈牺弹,在此先行謝過了_)
YY:先上小Demo
<a name="demo">小Demo</a>
// 父類
public class Father {
// 非靜態(tài)變量
C fa = new C("fa");
// 靜態(tài)變量
static C fb = new C("fb");
// 靜態(tài)語句塊
static {
OutUtil.print("Static blocks 1 in Father! ");
}
// 靜態(tài)語句塊
static {
OutUtil.print("Static blocks 2 in Father! ");
}
// 靜態(tài)常量
static final int T = 28;
// 構造方法
public Father() {
super();
OutUtil.print("Construct method in Father! ");
}
// 帶參構造器
public Father(String name) {
OutUtil.print("Construct method in Father! " + "Name = " + name);
}
// 非靜態(tài)代碼塊
{
OutUtil.print("Common blocks in Father! ");
}
// 靜態(tài)方法
static void staticShow() {
OutUtil.print("Static method in Father! ");
}
// 非靜態(tài)方法
void show() {
OutUtil.print("Common method in Father! ");
}
}
// 子類
public class Child extends Father {
// 非靜態(tài)變量
C ca = new C("ca");
// 靜態(tài)常量
static final int T = 28;
// 構造方法
public Child() {
super();
OutUtil.print("Construct method in Child! ");
}
// 帶參構造器
public Child(String name) {
OutUtil.print("Construct method in Child! " + "Name = " + name);
}
// 非靜態(tài)代碼塊
{
OutUtil.print("Common blocks in Child! ");
}
// 靜態(tài)方法
static void staticShow() {
OutUtil.print("Static method in Child! ");
}
// 非靜態(tài)方法
void show() {
OutUtil.print("Common method in Child! ");
}
// 靜態(tài)變量
static C cb = new C("cb");
// 靜態(tài)語句塊
static {
OutUtil.print("Static blocks 1 in Child! ");
}
// 靜態(tài)語句塊
static {
OutUtil.print("Static blocks 2 in Child! ");
}
}
// 輔助類
public class C {
public static final String A = "A in C";
public static String showC() {
return "showC method in C!";
}
public C(){
OutUtil.print("Construct method in C!");
}
public C(String msg){
OutUtil.print("Construct method in C! " + msg);
}
}
// 入口程序所在類
public class Main {
C ma = new C("ma"); // 打印結果顯示ma并未進行初始化
static C mb = new C("mb");
public Main(){
OutUtil.print("I am Main!");
}
static{
OutUtil.print(mb.getClass().getCanonicalName());
}
public static void main(String[] args) {
OutUtil.print("Main");
Child child = new Child();
child.show();
OutUtil.print(C.A);
OutUtil.print(C.showC());
}
static Child mc = new Child("mc");
}
// 打印輸出的工具類
public class OutUtil {
public static void print(Object o) {
System.out.println(o);
}
}
// 程序運行輸出結果
Construct method in C! mb
classloadertest.C
Construct method in C! fb
Static blocks 1 in Father!
Static blocks 2 in Father!
Construct method in C! cb
Static blocks 1 in Child!
Static blocks 2 in Child!
Construct method in C! fa
Common blocks in Father!
Construct method in Father!
Construct method in C! ca
Common blocks in Child!
Construct method in Child! Name = cb
Main
Construct method in C! fa
Common blocks in Father!
Construct method in Father!
Construct method in C! ca
Common blocks in Child!
Construct method in Child!
Common method in Child!
A in C
showC method in C!
Kitty:控制臺輸出了好多語句呀浦马,看得我眼睛都花了,你給我指點指點撒
YY:OK张漂,先給你畫個圖吧晶默!
YY:看了上面的圖應該可以大致看出哪些語句被執(zhí)行了,哪些語句沒有被執(zhí)行航攒,下面再給你分析分析磺陡。
Demo分析
上面的小Demo中含有四個類:Father、Child漠畜、C和Main币他,其中Child類繼承自Father類;C類是一個輔助類憔狞,主要用于打印輸出蝴悉,便于分析程序的運行流程;Main類是Java程序入口方法main方法所在的類瘾敢,也是Java程序運行時加載的第一個類拍冠。
下面根據(jù)上圖分析該Demo的運行流程。從圖中可以看出簇抵,程序首先執(zhí)行了Main類中的靜態(tài)域和靜態(tài)代碼塊(靜態(tài)域和靜態(tài)代碼塊的具體執(zhí)行順序依代碼中定義的順序而定)庆杜,然后執(zhí)行main方法中的代碼,根據(jù)main方法中的代碼需要碟摆,再加載所需要的類并進行初始化晃财。
注:
- 整個程序執(zhí)行過程中并沒有執(zhí)行Main類中的非靜態(tài)域,也沒有執(zhí)行Main類的初始化方法焦履,由此可知拓劝,并沒有實例化Main類;
- 圖中第一個大矩形框執(zhí)行的是Main類最后一行語句嘉裤,即初始化靜態(tài)域mc郑临。從矩形框中可以看出創(chuàng)建一個類實例的流程:
a. 首先執(zhí)行父類中的靜態(tài)域和靜態(tài)代碼塊(靜態(tài)域和靜態(tài)代碼塊的具體執(zhí)行順序依代碼中定義的順序而定)蝗敢;
b. 然后執(zhí)行子類中的靜態(tài)域和靜態(tài)代碼塊岸售;
c. 然后執(zhí)行父類中的非靜態(tài)域和非靜態(tài)代碼塊阱表;
d. 接著執(zhí)行父類的初始化方法(如果子類在構造函數(shù)中明確指明了調用父類的哪一個構造函數(shù),則調用相應的構造函數(shù)弹囚,否則調用父類的無參構造函數(shù))
e. 然后執(zhí)行子類中的非靜態(tài)域和非靜態(tài)代碼塊董朝;
d. 最后執(zhí)行子類的初始化方法(即被調用的那一個構造函數(shù))等舔。 - static Child mc = new Child("mc")和Child child = new Child()這兩句都是實例化一個Child對象眷柔,從下圖可以看出,類中的靜態(tài)域只進行了一次初始化操作公你,而非靜態(tài)域則進行了兩次初始化踊淳,由此可知,一個類中的靜態(tài)域和靜態(tài)代碼塊只會在類第一次加載時進行初始化陕靠,而非靜態(tài)域和非靜態(tài)代碼塊則會在每一次實例化時均執(zhí)行迂尝。
Kitty:那也可能是因為上面的代碼中第一次是初始化一個靜態(tài)實例,第二次只是初始化一個非靜態(tài)實例呀剪芥?
YY:問得很好垄开,要解開這個疑惑很簡單,我們變一下程序流程就好了税肪。
public class Main {
C ma = new C("ma");
static C mb = new C("mb");
public Main(){
OutUtil.print("I am CallbackMain!");
}
static{
OutUtil.print(mb.getClass().getName());
}
public static void main(String[] args) {
OutUtil.print("Main");
Child mc = new Child("mc");
Child child = new Child();
child.show();
OutUtil.print(C.A);
OutUtil.print(C.showC());
}
// static Child mc = new Child("mc");
}
YY:上面的代碼和圖顯示了同時在main方法中實例化兩個Child對象的執(zhí)行流程溉躲,可以看到
上面的第3點觀察結論是正確的--類中的靜態(tài)域只會在類第一次加載時執(zhí)行,而非靜態(tài)域則會在每一次實例化時均執(zhí)行益兄。
Kitty:確實是這樣的呀锻梳,看來!
YY:下面用一個流程圖簡易表示Java中對象的初始化順序以加深記憶吧净捅!Java中唱蒸,沒有顯式使用extends繼承的類都默認繼承自Object類,也就是說灸叼,除Object類以外,每個類都會有一個顯式或隱式的父類庆捺,并且任何對象的初始化都會自Object類開始古今。
Kitty:好啦,通過你的小Demo演示以及你的觀察結論滔以,我已經大致清楚了Java程序的執(zhí)行流程以及在Java中捉腥,創(chuàng)建一個對象會經歷哪些過程了∧慊可是抵碟。。坏匪。
YY:可是什么拟逮??适滓?
Kitty:你不說還好敦迄,被你這么一說吧,我的腦子里冒出了一堆?罚屋?苦囱?
YY:哈哈,有問號說明你還在思考脾猛,說說都有哪些疑問撕彤,我們一起把它們變成!C退羹铅!唄
Kitty:我現(xiàn)在主要有一下幾個困惑:
- 為什么一個對象的初始化過程是這樣的呢?
- 上面的Demo顯示Father類和Child類會執(zhí)行非靜態(tài)域漆弄、非靜態(tài)初始化塊和構造方法睦裳,但是Main類中的非靜態(tài)域與初始化方法卻并未執(zhí)行,這又是怎么一回事呢撼唾?
- 那么廉邑,Java中一個類的初始化過程又是怎樣的呢,和對象的初始化過程又有什么區(qū)別和聯(lián)系倒谷?
- static關鍵字好神奇的樣子蛛蒙,可是該怎么用呢?
YY:問題已經拋出來了渤愁,接下來就開始解決它們牵祟。
預備知識
YY:看樣子你的Java基礎也不咋地呀,那么先給你介紹幾個基本概念作為預備知識熱熱身吧抖格,不然后面你又該冒诺苹??雹拄?了(_)
普通代碼塊:在方法或語句中出現(xiàn)的 {} 就稱為普通代碼塊收奔,普通代碼塊和一般的語句執(zhí)行順序由他們在代碼中出現(xiàn)的次序決定--“先出現(xiàn)先執(zhí)行”;
構造塊:直接在類中定義且沒有加static關鍵字的代碼塊 {} 稱為構造代碼塊(構造塊)滓玖。用來初始化每一個對象的非靜態(tài)變量坪哄,構造塊在每次創(chuàng)建 **對象 **時均會被調用,并且 **構造塊的執(zhí)行次序優(yōu)先于類構造函數(shù) **势篡;
-
靜態(tài)代碼塊: 在 Java 中使用 static 關鍵字聲明的代碼塊 {} 稱為靜態(tài)代碼塊(靜態(tài)初始塊)翩肌。靜態(tài)塊用于初始化類,為類的屬性初始化禁悠。每個靜態(tài)代碼塊只會在Class對象首次加載時執(zhí)行一次念祭。由于JVM在加載類時會執(zhí)行靜態(tài)代碼塊,所以靜態(tài)代碼塊先于主方法執(zhí)行绷蹲。如果類中包含多個靜態(tài)代碼塊棒卷,那么將按照 **"先出現(xiàn)先執(zhí)行" **的順序執(zhí)行顾孽。
- 靜態(tài)代碼塊不能存在于任何方法體內
- 靜態(tài)代碼塊不能直接訪問非靜態(tài)實例變量和實例方法,需要通過類的實例對象來訪問
- 即便沒有顯式使用使用static關鍵字比规,構造器實質上也是靜態(tài)方法(出自Thinking in Java)
-
JVM中的內存區(qū)域: Java 程序執(zhí)行時需要先被 JVM 加載進內存若厚,為了提高程序運算效率, JVM 會將不同類型的數(shù)據(jù)加載進內存中的不同區(qū)域蜒什,因為每一片區(qū)域均有不同的內存管理方式和數(shù)據(jù)處理方式测秸!JVM 中幾個比較重要的區(qū)域為:
- 程序計數(shù)器:每個線程擁有一個PC寄存器,在線程創(chuàng)建時創(chuàng)建灾常,指向下一條指令的地址霎冯,執(zhí)行本地方法時,PC的值為undefined
- 方法區(qū):保存裝載的類信息钞瀑,如類型的常量池沈撞、字段、方法信息雕什、方法字節(jié)碼等缠俺,通常和永久區(qū)(Perm)關聯(lián)在一起
- 堆區(qū):用于存放類的對象實例,為所有線程所共享
- 棧區(qū): 也叫java虛擬機棧贷岸,是由一個一個的棧幀組成的后進先出的棧式結構壹士,棧楨中存放方法運行時產生的局部變量、方法出口等信息偿警。當調用一個方法時躏救,虛擬機棧中就會創(chuàng)建一個棧幀存放這些數(shù)據(jù),當方法調用完成時螟蒸,棧幀消失盒使,如果方法中調用了其他方法,則繼續(xù)在棧頂創(chuàng)建新的棧楨七嫌。棧區(qū)是 線程私有 的忠怖,生命周期和線程相同
YY:熱身活動完成,進入正題抄瑟!
Java類的生命周期
當我們編寫一個java的源文件后,經過編譯會生成一個后綴名為 **.class **的文件枉疼,這種文件叫做字節(jié)碼文件皮假,只有這種字節(jié)碼文件才能夠在 Java 虛擬機中運行, **Java 類的生命周期就是指一個class文件從加載到卸載的全過程 **骂维,如上圖所示惹资。
- 裝載:在裝載階段,JVM會通過一個類的全限定名獲取描述此類的.class文件航闺,然后通過這個.class文件將類的信息加載到 JVM 的方法區(qū)褪测,然后在堆區(qū)中實例化一個java.lang.Class對象猴誊,作為方法區(qū)中這個類的信息的入口。虛擬機設計團隊把加載動作放到 JVM 外部實現(xiàn)侮措,以便讓應用程序決定如何獲取所需的類懈叹,實現(xiàn)這個動作的代碼被稱為 “類加載器” 。至于何時加載一個類分扎, JVM 并沒有一個統(tǒng)一的規(guī)范澄成,所以不同的虛擬機可能采取不同的加載策略,有些虛擬機會選擇 在執(zhí)行前就預先加載類 畏吓,而另一些虛擬機則會在 真正需要使用到一個類的時候才會加載 墨状。但無論如何,一個類總是會在 JVM “預期”到即將會使用之前被加載菲饼。常用的hotspot虛擬機采取的是懶加載原則肾砂,即等到真正需要使用到一個類時才加載這個類;
- 連接: JVM 將已讀入的二進制文件合并到 JVM 的運行時狀態(tài)的過程宏悦,這個過程由 驗證镐确、準備和解析 三個子步驟構成
- 驗證:確認該類型符合 Java 語言的語義,并且該類型不會危及 JVM 的完整性肛根,主要包括 格式驗證辫塌、元數(shù)據(jù)驗證、字節(jié)碼驗證和符號引用驗證 等派哲;
- 準備:在準備階段臼氨,JVM 為 類變量 (所謂類變量就是被 static 關鍵字修飾的變量)分配內存,設置默認的初始值芭届,(默認值的設置過程是通過將此片內存區(qū)清零實現(xiàn)的储矩,即通過將對象內存設為二進制零值而一舉生成)此時默認值設置如下,而非在代碼中賦予的值(在準備階段并不會執(zhí)行 Java 代碼):
- 基本類型(int褂乍、long持隧、short、char逃片、byte屡拨、boolean、float褥实、double): 默認值為0呀狼;
- 引用類型: 默認值為 null;
- 常量: 默認值為程序中設定的值损离,比如我們在程序中定義final static int a = 8哥艇,則準備階段中a的初值就是8;
private static int a = 8;
// 上面這句在準備階段只會將 a 初始化為0僻澎,需要等到后面的初始化階段貌踏,才會將 a 賦值為8
private static final int A = 8;
// 這句例外十饥,因為上面這句表明 A 是一個編譯期常量
// 所以在編譯階段會為 A 生成 ConstantValue 屬性,在準備階段虛擬機會根據(jù) ConstantValue 屬性將 A 賦值為8
-
解析: 在類的常量池中尋找類祖乳、接口逗堵、方法和字段的符號引用,將符號引用替換為直接引用的過程凡资,實質上砸捏,在符號引用被程序首次使用以前,這個過程都是可選的隙赁。
- 符號引用:使用一組符號來描述所引用的目標垦藏,可以是任何形式的字面常量,定義在Class文件格式中
- 直接引用:可以是直接指向目標的指針伞访、相對偏移量或能間接定位到目標的句柄
初始化:即為 **類變量 **賦予 **“正確” **的初始值的過程掂骏,(“正確”的初始值是指代碼中希望這個類變量擁有的初始值)也就是上面的小Demo中的執(zhí)行靜態(tài)域和靜態(tài)代碼塊的過程(后面詳述類變量初始化過程);
對象生命:這個就很好理解了厚掷,一旦一個類完成了裝載弟灼、連接和初始化這三個過程,這個類就隨時可以被使用了冒黑,包括調用類變量田绑、類方法以及實例化類等。每次對類進行實例化操作時都會創(chuàng)建該類的一個新的對象抡爹,開啟該對象的生命周期掩驱。對象的生命周期包含三個階段:
1).*** 對象實例化:*** 即對象的初始化階段,在本階段完成對象初始化工作冬竟,回看上面的Demo欧穴,即執(zhí)行類中的非靜態(tài)域和非靜態(tài)代碼塊部分,然后執(zhí)行類的構造函數(shù)中的代碼泵殴。具體流程為:當通過顯式或隱式的方式創(chuàng)建一個類的實例時涮帘,JVM 會首先為該類及其所有超類中的實例變量在堆中分配內存,然后 JVM 會將該塊內存空間清零笑诅,從而將實例變量初始化為 默認的初始值 (數(shù)字调缨、字符和布爾型變量初始化為0,引用類型變量初始化為null)吆你,然后根據(jù)我們在代碼中書寫的內容同蜻,為實例變量賦予正確的初始值;完成對象實例化過程后早处,就可以通過該對象的引用使用對象了,如調用對象的方法瘫析、獲取對象中某個域的值等砌梆;
2). *** 垃圾收集:*** 當一個對象不再被引用的時候默责,JVM 就可以將這個對象所占據(jù)的內存回收,從而使得該部分內存可以被再次使用咸包,垃圾回收時機桃序、策略等是一個非常復雜的過程,具體可以參見深入Java虛擬機一書烂瘫;
3). 對象終結:當一個對象被垃圾收集器收集后媒熊,該對象就不復存在,也就是說該對象的生命周期結束坟比;
- 類卸載:類卸載是類生命周期的最后一個過程芦鳍,當程序不再引用某一類型時,那么這個類型就無法再對未來的計算過程產生影響葛账,從而該類就可以被 JVM 垃圾回收柠衅。
YY:上面大致描述了 Java 中一個類的生命周期流程,本文并不會對每個過程都進行深入細致的分析籍琳,那樣的話會陷入到細節(jié)陷阱中無法自拔菲宴,如果你對哪個部分有疑惑或者感興趣的話,可以去查閱相關資料詳加了解趋急,下面詳細講解一下 Java 類的初始化過程喝峦,畢竟這才是本文的重點嘛_
Kitty:好的,好的呜达!我現(xiàn)在正興趣濃厚谣蠢,快開始講吧!
YY:瞧你那猴急猴急的樣闻丑,平時也沒見你這么認真學習了漩怎!那我開始咯,你好好聽哈嗦嗡。
Java類初始化
為了讓一個類/接口被首次主動使用勋锤,在加載、連接過程完成后侥祭,JVM 會執(zhí)行類初始化過程叁执。前面已經簡要介紹過,類初始化時會執(zhí)行類變量初始化語句和靜態(tài)語句塊矮冬,將準備階段賦予類變量的默認初始值替換為“正確”的初始化值谈宛。
Kitty:** 首次 **我知道,不就是第一次么胎署,可是 主動使用 是個什么鬼吆录?
YY:別急撒,接下來就為你揭開它的神秘面紗
主動使用 VS 被動使用
-
主動使用:在 Java 中只有如下幾種活動被視為主動使用
- 創(chuàng)建類的新實例琼牧;
- 調用類中聲明的靜態(tài)方法恢筝;
- 操作類/接口中聲明的 非常量 靜態(tài)域哀卫;
- 調用 Java 中的反射方法;
- 初始化一個類的子類撬槽;
- 指定一個類作為 JVM 啟動時的初始化類此改,即 main 函數(shù)所在的類。
- 被動使用:不屬于上述六種情況的活動均被視之為被動使用
** 注:被動使用一個類時并不會觸發(fā)類初始化過程**侄柔, 如
- 非首次主動使用一個類時不會觸發(fā)類的初始化過程共啃,也就是所第二次主動使用一個類也不再會觸發(fā)類的初始化
- 使用一個類的非常量靜態(tài)字段時,只有當該字段確實是由當前類/接口聲明的時才可以稱之為主動使用暂题,否則是被動使用移剪。比如說,當通過子類調用父類的 public 的非常量靜態(tài)域時敢靡,對于子類來說這是被動使用挂滓,對于父類才是主動使用,所以會觸發(fā)父類的初始化啸胧,而不會觸發(fā)子類的初始化
- 如果一個變量被 static 和 final 同時修飾赶站,并且使用一個編譯期常量表達式進行初始化,那么對這樣的字段的使用就不是對聲明該字段的類的主動使用纺念,因為 Java編譯器會把這樣的字段解析成對常量的本地拷貝(該常量存在于引用者的字節(jié)碼流中或者常量池中贝椿,或二者均有)
- 定義一個類的對象數(shù)組時并不會觸發(fā)該類的初始化
Kitty:哎呀,一會兒是主動使用陷谱,一會兒又不是主動使用的烙博,這些抽象的概念一點都不好理解,我的腦子里現(xiàn)在就像小燕子說的了--全是漿糊了(衰)
YY:抽象的東西確實不太好理解和記憶烟逊,下面沿用上面小 Demo 中出現(xiàn)的類寫幾個小例子渣窜,你就會有所理解了。
示例說明
- 非首次主動使用一個類時也不會觸發(fā)類的初始化過程
public class InitTest {
public static void main(String[] args) {
Father.staticShow(); //首次主動使用
Father.staticShow(); //非首次主動使用
}
}
程序輸出為:
Construct method in C! fb
Static blocks 1 in Father!
Static blocks 2 in Father!
Static method in Father!
Static method in Father!
由輸出可知宪躯,雖然在代碼中先后兩次執(zhí)行了Father中的靜態(tài)方法staticShow()乔宿,但是Father類的初始化過程只執(zhí)行了一次。
- 通過子類調用父類的靜態(tài)域
public class InitTest {
public static void main(String[] args) {
OutUtil.print(Child.str); // str是Father類中的靜態(tài)字符串访雪,初始值為“str in Father”
}
}
程序輸出為:
Construct method in C! fb
Static blocks 1 in Father!
Static blocks 2 in Father!
str in Father
輸出顯示详瑞,只執(zhí)行了Father類的初始化代碼,而未執(zhí)行Child類的初始化代碼臣缀。
- 調用編譯期常量不會觸發(fā)類初始化
public class InitTest {
public static void main(String[] args) {
OutUtil.print(Father.T);
}
}
程序輸出為:
28
由此可見并未觸發(fā)類Father的初始化操作坝橡。
- 定義一個類的對象數(shù)組時并不會觸發(fā)該類的初始化
public class InitTest {
public static void main(String[] args) {
Father[] cArray = new Father[8];
}
}
執(zhí)行上面這段代碼后,控制臺并未產生輸出精置,這就說明并未初始化Father類计寇。可以通過查看這段代碼產生的字節(jié)碼文件加以驗證。
由上圖可以看到番宁,首先執(zhí)行了Object類的初始化方法蹲堂,然后執(zhí)行InitTest類中的main方法,其中anewarray指令為新數(shù)組分配空間贝淤,但并未觸發(fā)類Father的初始化。
Kitty:喔U2ゴ稀!原來主動使用和被動使用是這樣的呀布隔,Java 加載類离陶,對類進行初始化的時機為首次主動使用的時候,可是你還是沒有講 JVM 執(zhí)行類初始化操作的具體流程呀衅檀。
YY:OK招刨,接下來就是了。
如何執(zhí)行類的初始化操作哀军?
在 Java 類和接口的 class 文件中有一個只能夠被 JVM 調用的<clinit>()方法沉眶,這個方法會將類/接口的所有類變量初始化語句和靜態(tài)初始化塊均收集起來,然后在需要執(zhí)行類初始化操作時杉适,JVM 便調用該方法為類變量賦予“正確”的初始值谎倔。具體由以下兩個步驟構成:
- 如果類存在直接超類,并且直接超類還未被初始化猿推,則先初始化超類片习;
- 如果類存在類初始化方法,則執(zhí)行該初始化方法蹬叭;
在執(zhí)行超類的初始化時也是通過這兩個步驟完成藕咏,因此,程序中第一個被初始化的類永遠是Object類秽五,其次是被主動使用的類繼承層次樹上的所有類孽查,超類總是先于子類被初始化。
注:
- 初始化接口時并不需要先初始化其父接口筝蚕,只有使用父接口中定義的變量時卦碾,才會執(zhí)行父接口的初始化動作
- <clinit>()方法的代碼并不會顯式調用超類的<clinit>()方法,JVM 在調用類的<clinit>()方法時會先確認超類的<clinit>()方法已經被正確調用
- 為了防止多次執(zhí)行<clinit>起宽,JVM 會確保<clinit>()方法在多線程環(huán)境下被正確的加鎖同步執(zhí)行洲胖。當有多個線程需要對一個類執(zhí)行初始化操作時,只能由一個線程來執(zhí)行坯沪,其它線程均處于等待狀態(tài)绿映,當活動線程執(zhí)行完成后,必須通知其它線程
- 并非所有的類均會在class文件中擁有<clinit>()方法,只有那些的確需要執(zhí)行 Java 代碼來賦予類變量正確的初始值的類才會有<clinit>()方法叉弦。下面幾種情況下丐一,類的class文件中不會包含<clinit>()方法:
- 類中沒有聲明任何類變量,也沒有包含靜態(tài)初始化塊淹冰;
public class Test1 {
int a = 8;
int add(){return ++a;}
}
- 雖然類聲明了類變量库车,但是并沒有明確使用類變量初始化語句或靜態(tài)初始化語句對它們進行初始化;
public class Test2 {
static int c;
}
- 類中僅包含static final變量的初始化語句樱拴,并且初始化語句是編譯期常量表達式
public class Test3 {
static final int A = 8;
static final int B = A * 8;
}
上面代碼中柠衍,A和B均是編譯時常量,JVM 在加載 test 類時晶乔,不會將A珍坊、B作為類變量保存到方法區(qū),而是會被當做常量正罢,被 Java 編譯器特殊處理阵漏。因此,不需要<clinit>()方法來對它們進行初始化翻具。
YY:OK履怯,到此 Java 類的初始化部分就結束啦,下面由Kitty你來說說你的理解呛占,然后再回顧一下上面的小 Demo唄虑乖。
Kitty:好的,正好回顧一下晾虑,不然知識都是零零散散的疹味,一下就忘了。
Demo回顧
下面將 Java 程序入口類 Main 的代碼提出來了帜篇,其余代碼見小Demo
// 入口程序所在類
public class Main {
C ma = new C("ma"); // 打印結果顯示ma并未進行初始化
static C mb = new C("mb");
public Main(){
OutUtil.print("I am Main!");
}
static{
OutUtil.print(mb.getClass().getCanonicalName());
}
public static void main(String[] args) {
OutUtil.print("Main");
Child child = new Child();
child.show();
OutUtil.print(C.A);
OutUtil.print(C.showC());
}
static Child mc = new Child("mc");
}
當前程序運行流程:
- main 方法所在的類為 Main糙捺, 所以 JVM 會先加載 Main類;
- 完成 Main 類的連接步驟笙隙,將 Main 類中的靜態(tài)域 mb 和 mc 初始化為null洪灯;
- 初始化 Main 類,執(zhí)行 Main 類中的靜態(tài)塊竟痰,此時會執(zhí)行如下幾句:
static C mb = new C("mb");
static{
OutUtil.print(mb.getClass().getName());
}
static Child mc = new Child("mc");
- 執(zhí)行 main 方法
static Child mc = new Child("mc")一句可以更加詳細地說明類初始化流程签钩。
- 首先,JVM 會加載該句所需要的類坏快,因為 Child 類是 Father 類的子類铅檩,所以首先加載 Father 類;
- 連接 Father 類莽鸿;
- 初始化 Father 類(即會執(zhí)行 Father 類中的靜態(tài)塊和靜態(tài)域初始化語句)昧旨;
- 加載 Child 類拾给;
- 連接 Child 類;
- 初始化 Child 類兔沃;
- 執(zhí)行 Father 類的非靜態(tài)域初始化語句和構造塊蒋得;
- 執(zhí)行 Father 類的構造方法;
- 執(zhí)行 Child 類的非靜態(tài)域初始化語句和構造塊乒疏;
- 執(zhí)行 Child 類的構造方法额衙。
總結
- 只有在應用程序 首次主動使用 一個類時,JVM 才會對這個類進行初始化怕吴;
- 類的生命周期主要有如下幾個階段:加載--連接--初始化--[對象生命]--卸載入偷,其中對象生命階段是可選的,也就是說械哟,一旦完成類的加載、連接和初始化工作殿雪,就可以使用類了暇咆,當程序中不再有該類的引用時,就可以被 JVM 回收丙曙,至此類生命周期結束爸业;
- 對象的生命周期為:初始化--使用--回收--終結,對象的生命周期依賴于類的生命周期亏镰,只有當完成了類的加載扯旷、連接和初始化工作后,才會創(chuàng)建對象索抓;
- Java 類在進行初始化時钧忽,會先執(zhí)行父類的初始化步驟,再執(zhí)行子類的初始化逼肯,所以所有的類初始化工作均起始于 Object 類
YY:Well done耸黑!
Kitty:嘿嘿,疑惑消除篮幢,可以愉快地玩耍了~~~