Java系列笔记(1) - Java 类加载与初始化
2019獨角獸企業(yè)重金招聘Python工程師標(biāo)準(zhǔn)>>>
目錄
類加載器
在了解Java的機制之前,需要先了解類在JVM(Java虛擬機)中是如何加載的,這對后面理解java其它機制將有重要作用。
每個類編譯后產(chǎn)生一個Class對象,存儲在.class文件中,JVM使用類加載器(Class Loader)來加載類的字節(jié)碼文件(.class),類加載器實質(zhì)上是一條類加載器鏈,一般的,我們只會用到一個原生的類加載器,它只加載Java API等可信類,通常只是在本地磁盤中加載,這些類一般就夠我們使用了。如果我們需要從遠(yuǎn)程網(wǎng)絡(luò)或數(shù)據(jù)庫中下載.class字節(jié)碼文件,那就需要我們來掛載額外的類加載器。
一般來說,類加載器是按照樹形的層次結(jié)構(gòu)組織的,每個加載器都有一個父類加載器。另外,每個類加載器都支持代理模式,即可以自己完成Java類的加載工作,也可以代理給其它類加載器。
類加載器的加載順序有兩種,一種是父類優(yōu)先策略,一種是是自己優(yōu)先策略,父類優(yōu)先策略是比較一般的情況(如JDK采用的就是這種方式),在這種策略下,類在加載某個Java類之前,會嘗試代理給其父類加載器,只有當(dāng)父類加載器找不到時,才嘗試自己去加載。自己優(yōu)先的策略與父類優(yōu)先相反,它會首先嘗試子經(jīng)濟加載,找不到的時候才要父類加載器去加載,這種在web容器(如tomcat)中比較常見。
動態(tài)加載
不管使用什么樣的類加載器,類,都是在第一次被用到時,動態(tài)加載到JVM的。這句話有兩層含義:
需要區(qū)分加載和初始化的區(qū)別,加載了一個類的.class文件,不以為著該Class對象被初始化,事實上,一個類的初始化包括3個步驟:
- 加載(Loading),由類加載器執(zhí)行,查找字節(jié)碼,并創(chuàng)建一個Class對象(只是創(chuàng)建);
- 鏈接(Linking),驗證字節(jié)碼,為靜態(tài)域分配存儲空間(只是分配,并不初始化該存儲空間),解析該類創(chuàng)建所需要的對其它類的應(yīng)用;
- 初始化(Initialization),首先執(zhí)行靜態(tài)初始化塊static{},初始化靜態(tài)變量,執(zhí)行靜態(tài)方法(如構(gòu)造方法)。
鏈接
Java在加載了類之后,需要進行鏈接的步驟,鏈接簡單地說,就是將已經(jīng)加載的java二進制代碼組合到JVM運行狀態(tài)中去。它包括3個步驟:
初始化
注 意:在《Java編程思想》中,說static{}子句是在類第一次加載時執(zhí)行且執(zhí)行一次(可能是筆誤或翻譯錯誤,因為此書的例子顯示static是在第 一次初始化時執(zhí)行的),《Java深度歷險》中說 static{}是在第一次實例化時執(zhí)行且執(zhí)行一次,這兩種應(yīng)該都是錯誤的,static{}是在第一次初始化時執(zhí)行,且只執(zhí)行一次;用下面的代碼可以判 定出來:
package myblog.classloader;/*** @project MyBlog* @create 2013年6月18日 下午7:00:45* @version 1.0.0* @author 張廣*/ public class Toy {private String name;public static final int price=10;static {System.out.println("Initializing");}Toy() {System.out.println("Building");}Toy(String name) {this.setName(name);}public static String playToy(String player) {String msg = buildMsg(player);System.out.println(msg);return msg;}private String buildMsg(String player) {String msg = player + " plays " + name;return msg;} }// 對上面的類,執(zhí)行下面的代碼:Class c = Class.forName("myblog.rtti.Toy");// c.newInstance();可以看到,不實例化,只執(zhí)行forName初始化時,仍然會執(zhí)行static{}子句,但不執(zhí)行構(gòu)造方法,因此輸出的只有Initializing,沒有Building。
關(guān)于初始化,@阿春阿曉 在本文的評論中給出了很詳細(xì)的場景,感謝@阿春阿曉:
根據(jù)java虛擬機規(guī)范,所有java虛擬機實現(xiàn)必須在每個類或接口被java程序首次主動使用時才初始化。
主動使用有以下6種:
1) 創(chuàng)建類的實例
2) 訪問某個類或者接口的靜態(tài)變量,或者對該靜態(tài)變量賦值(如果訪問靜態(tài)編譯時常量(即編譯時可以確定值的常量)不會導(dǎo)致類的初始化)
3) 調(diào)用類的靜態(tài)方法
4) 反射(Class.forName(xxx.xxx.xxx))
5) 初始化一個類的子類(相當(dāng)于對父類的主動使用),不過直接通過子類引用父類元素,不會引起子類的初始化(參見示例6)
6) Java虛擬機被標(biāo)明為啟動類的類(包含main方法的)
類與接口的初始化不同,如果一個類被初始化,則其父類或父接口也會被初始化,但如果一個接口初始化,則不會引起其父接口的初始化。
示例
1,通過上面的講解,將可以理解下面的程序(下面的程序部分來自于《Java編程思想》):
class Toy {static {System.out.println("Initializing");// 靜態(tài)子句,只在類第一次被加載并初始化時執(zhí)行一次,而且只執(zhí)行一次 }Toy() {System.out.println("Building");// 構(gòu)造方法,在每次聲明新對象時加載 } }對上面的程序段,第一次調(diào)用Class.forName("Toy"),將執(zhí)行static子句;如果在之后執(zhí)行new Toy()都只執(zhí)行構(gòu)造方法。
2,需要注意newInstance()方法
Class cc = Class.forName("Toy");//獲得類(注意,需要使用含包名的全限定名) Toy toy=(Toy)cc.newInstance(); //相當(dāng)于new一個對象,但Gum類必須有默認(rèn)構(gòu)造方法(無參)3,用類字面常量 .class和Class.forName都可以創(chuàng)建對類的應(yīng)用,但是不同點在于,用Gum.class創(chuàng)建Class對象的應(yīng)用時,不會自動初始化該Class對象(static子句不會執(zhí)行)
public class TestToy {public static void main(String[] args) {// try {// Class c = Class.forName("myblog.classloader.Toy");// } catch (ClassNotFoundException e) {// e.printStackTrace();// }Class c = Toy.class; // 不會輸出任何值} }使用Toy.class是在編譯期執(zhí)行的,因此在編譯時必須已經(jīng)有了Toy的.class文件,不然會編譯失敗,這與 Class.forName("myblog.classloader.Toy")不同,后者是運行時動態(tài)加載。
但是,如果該main方法是直接寫在Toy類中,那么調(diào)用Toy.class,會引起初始化,并輸出Initializing,原因并不是Toy.class引起的,而是該類中含有啟動方法main,該方法會導(dǎo)致Toy的初始化。
?4,編譯時常量。回到完整的類Toy,如果直接輸出:System.out.println(Toy.price),會發(fā)現(xiàn)static子句和構(gòu)造方法都沒有被執(zhí)行,這是因為Toy中,常量price被static final限定,這樣的常量叫做編譯時常量,對于這種常量,不需要初始化就可以讀取。
編譯時常量必須滿足3個條件:static的,final的,常量。
下面幾種都不是編譯時常量,對它們的應(yīng)用,都會引起類的初始化:
static int a;final int b;static final int c= ClassInitialization.rand.nextInt(100);static final int d; static {d=5; }5,static塊的本質(zhì)。注意下面的代碼:
class StaticBlock {static final int c = 3;static final int d;static int e = 5;static {d = 5;e = 10;System.out.println("Initializing");}StaticBlock() {System.out.println("Building");} }public class StaticBlockTest {public static void main(String[] args) {System.out.println(StaticBlock.c);System.out.println(StaticBlock.d);System.out.println(StaticBlock.e);} }這段代碼的輸出是什么呢?Initialing在c、d、e之前輸出,還是在之后?e輸出的是5還是10?
執(zhí)行一下,結(jié)果為:
3Initializing
5
10
答案是3最先輸出,Intializing隨后輸出,e輸出的是10,為什么呢?
原因是這樣的:輸出c時,由于c是編譯時常量,不會引起類初始化,因此直接輸出,輸出d時,d不是編譯時常量,所以會引起初始化操作,即static塊的執(zhí)行,于是d被賦值為5,e被賦值為10,然后輸出Initializing,之后輸出d為5,e為10。
但e為什么是10呢?原來,JDK會自動為e的初始化創(chuàng)建一個static塊(參考:http://www.java3z.com/cwbwebhome/article/article8/81101.html?id=2497),所以上面的代碼等價于:
class StaticBlock {static final int d;static int e;static {e=5; }static {d = 5;e = 10;System.out.println("Initializing");}StaticBlock() {System.out.println("Building");} }可見,按順序執(zhí)行,e先被初始化為5,再被初始化為10,于是輸出了10。
類似的,容易想到下面的代碼:
class StaticBlock {static {d = 5;e = 10;System.out.println("Initializing");}static final int d;static int e = 5;StaticBlock() {System.out.println("Building");} }在這段代碼中,對e的聲明被放到static塊后面,于是,e會先被初始化為10,再被初始化為5,所以這段代碼中e會輸出為5。
6,當(dāng)訪問一個Java類或接口的靜態(tài)域時,只有真正聲明這個域的類或接口才會被初始化(《Java深度歷險》)
/*** 例子來源于《Java深度歷險》第二章* @author 張廣**/ class B {static int value = 100;static {System.out.println("Class B is initialized");// 輸出 } }class A extends B {static {System.out.println("Class A is initialized"); // 不輸出 } }public class SuperClassTest {public static void main(String[] args) {System.out.println(A.value);// 輸出100 } }在該例子中,雖然通過A來引用了value,但value是在父類B中聲明的,所以只會初始化B,而不會引起A的初始化。
說明
筆者在開發(fā)過程中發(fā)現(xiàn)自己基礎(chǔ)太薄弱,讀書時除了系統(tǒng)學(xué)習(xí)了一下Java的基礎(chǔ)語法和用法、一點簡單的數(shù)據(jù)結(jié)構(gòu)和設(shè)計模式之外,再無深入系統(tǒng)的學(xué)習(xí),而工作中的學(xué)習(xí)也是東晃一槍西晃一槍,不夠扎實和系統(tǒng)。想到一個學(xué)習(xí)方法:學(xué)到的東西能夠系統(tǒng)的表達(dá)出來,才說明你學(xué)到手了;于是,筆者決定邊學(xué)邊寫,將學(xué)到的東西以博客的形式表達(dá)出來。
本文檔會因?qū)W習(xí)的深入或錯誤的訂正而持續(xù)更新。
《Java系列筆記》是本人對Java的應(yīng)用和學(xué)習(xí)過程中的筆記,按知識點分章節(jié),寫這一系列筆記的目的是學(xué)習(xí),由于筆者是邊學(xué)編寫的,水平有限,文中必定有疏漏之處,歡迎斧正。
文中多有參考前輩的書籍和博客之處,原則上不會原文引用,而是加入自己的理解改造之,如果有引用,必會注明出處,如有疑問,請聯(lián)系:daniel.zhguang@gmail.com
更新記錄
2013年6月25日:發(fā)表;
2013年6月26日:加入@阿春阿曉的評論:引起初始化的6種場景;
2013年6月28日:加入鏈接和初始化的內(nèi)容,加入示例6
本節(jié)參考資料
JAVA編程思想,第14章
Java深度歷險
Java中static塊的本質(zhì):http://www.java3z.com/cwbwebhome/article/article8/81101.html?id=2497
?java類的裝載(Loading)、鏈接(Linking)和初始化(Initialization):http://blog.csdn.net/biaobiaoqi/article/details/6909141
轉(zhuǎn)載于:https://my.oschina.net/91jason/blog/487120
《新程序員》:云原生和全面數(shù)字化實踐50位技術(shù)專家共同創(chuàng)作,文字、視頻、音頻交互閱讀總結(jié)
以上是生活随笔為你收集整理的Java系列笔记(1) - Java 类加载与初始化的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 环境变量修改方法
- 下一篇: 4路外线(NAT+PBR真实案例)