Java系列筆記(1) - Java 類加載與初始化

目錄html

  1. 類加載器
  2. 動態加載
  3. 連接
  4. 初始化
  5. 示例

類加載器java

在瞭解Java的機制以前,須要先了解類在JVM(Java虛擬機)中是如何加載的,這對後面理解java其它機制將有重要做用。web

每一個類編譯後產生一個Class對象,存儲在.class文件中,JVM使用類加載器(Class Loader)來加載類的字節碼文件(.class),類加載器實質上是一條類加載器鏈,通常的,咱們只會用到一個原生的類加載器,它只加載Java API等可信類,一般只是在本地磁盤中加載,這些類通常就夠咱們使用了。若是咱們須要從遠程網絡或數據庫中下載.class字節碼文件,那就須要咱們來掛載額外的類加載器。數據庫

通常來講,類加載器是按照樹形的層次結構組織的,每一個加載器都有一個父類加載器。另外,每一個類加載器都支持代理模式,便可以本身完成Java類的加載工做,也能夠代理給其它類加載器。編程

類加載器的加載順序有兩種,一種是父類優先策略,一種是是本身優先策略,父類優先策略是比較通常的狀況(如JDK採用的就是這種方式),在這種策略下,類在加載某個Java類以前,會嘗試代理給其父類加載器,只有當父類加載器找不到時,才嘗試本身去加載。本身優先的策略與父類優先相反,它會首先嚐試子經濟加載,找不到的時候纔要父類加載器去加載,這種在web容器(如tomcat)中比較常見。設計模式

動態加載tomcat

無論使用什麼樣的類加載器,類,都是在第一次被用到時,動態加載到JVM的。這句話有兩層含義:網絡

  1. Java程序在運行時並不必定被完整加載,只有當發現該類尚未加載時,纔去本地或遠程查找類的.class文件並驗證和加載;
  2. 當程序建立了第一個對類的靜態成員的引用(如類的靜態變量、靜態方法、構造方法——構造方法也是靜態的)時,纔會加載該類。Java的這個特性叫作:動態加載

須要區分加載和初始化的區別,加載了一個類的.class文件,不覺得着該Class對象被初始化,事實上,一個類的初始化包括3個步驟:數據結構

  • 加載(Loading),由類加載器執行,查找字節碼,並建立一個Class對象(只是建立);
  • 連接(Linking),驗證字節碼,爲靜態域分配存儲空間(只是分配,並不初始化該存儲空間),解析該類建立所須要的對其它類的應用;
  • 初始化(Initialization),首先執行靜態初始化塊static{},初始化靜態變量,執行靜態方法(如構造方法)。

連接學習

Java在加載了類以後,須要進行連接的步驟,連接簡單地說,就是將已經加載的java二進制代碼組合到JVM運行狀態中去。它包括3個步驟:

  1. 驗證(Verification),驗證是保證二進制字節碼在結構上的正確性,具體來講,工做包括檢測類型正確性,接入屬性正確性(public、private),檢查final class 沒有被繼承,檢查靜態變量的正確性等。
  2. 準備(Preparation),準備階段主要是建立靜態域,分配空間,給這些域設默認值,須要注意的是兩點:一個是在準備階段不會執行任何代碼,僅僅是設置默認值,二個是這些默認值是這樣分配的,原生類型所有設爲0,如:float:0f,int 0, long 0L, boolean:0(布爾類型也是0),其它引用類型爲null。
  3. 解析(Resolution),解析的過程就是對類中的接口、類、方法、變量的符號引用進行解析並定位,解析成直接引用(符號引用就是編碼是用字符串表示某個變量、接口的位置,直接引用就是根據符號引用翻譯出來的地址),並保證這些類被正確的找到。解析的過程可能致使其它的類被加載。須要注意的是,根據不一樣的解析策略,這一步不必定是必須的,有些解析策略在解析時遞歸的把全部引用解析,這是early resolution,要求全部引用都必須存在;還有一種策略是late resolution,這也是Oracle 的JDK所採起的策略,即在類只是被引用了,尚未被真正用到時,並不進行解析,只有當真正用到了,纔去加載和解析這個類。

初始化

注 意:在《Java編程思想》中,說static{}子句是在類第一次加載時執行且執行一次(多是筆誤或翻譯錯誤,由於此書的例子顯示static是在第 一次初始化時執行的),《Java深度歷險》中說 static{}是在第一次實例化時執行且執行一次,這兩種應該都是錯誤的,static{}是在第一次初始化時執行,且只執行一次;用下面的代碼能夠判 定出來:

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;
        }
}
  // 對上面的類,執行下面的代碼:
  Class c = Class.forName("myblog.rtti.Toy");
  // c.newInstance();

能夠看到,不實例化,只執行forName初始化時,仍然會執行static{}子句,但不執行構造方法,所以輸出的只有Initializing,沒有Building。

關於初始化,@阿春阿曉 在本文的評論中給出了很詳細的場景,感謝@阿春阿曉:

根據java虛擬機規範,全部java虛擬機實現必須在每一個類或接口被java程序首次主動使用時才初始化。

主動使用有如下6種:
1) 建立類的實例
2) 訪問某個類或者接口的靜態變量,或者對該靜態變量賦值(若是訪問靜態編譯時常量(即編譯時能夠肯定值的常量)不會致使類的初始化)
3) 調用類的靜態方法
4) 反射(Class.forName(xxx.xxx.xxx))
5) 初始化一個類的子類(至關於對父類的主動使用),不過直接經過子類引用父類元素,不會引發子類的初始化(參見示例6)
6) Java虛擬機被標明爲啓動類的類(包含main方法的)

類與接口的初始化不一樣,若是一個類被初始化,則其父類或父接口也會被初始化,但若是一個接口初始化,則不會引發其父接口的初始化。

示例

1,經過上面的講解,將能夠理解下面的程序(下面的程序部分來自於《Java編程思想》):

class Toy {
        static {
                System.out.println("Initializing");// 靜態子句,只在類第一次被加載並初始化時執行一次,並且只執行一次
        }

        Toy() {
                System.out.println("Building");// 構造方法,在每次聲明新對象時加載
        }
}

對上面的程序段,第一次調用Class.forName("Toy"),將執行static子句;若是在以後執行new Toy()都只執行構造方法。

2,須要注意newInstance()方法

Class cc = Class.forName("Toy");//得到類(注意,須要使用含包名的全限定名)
Toy toy=(Toy)cc.newInstance(); //至關於new一個對象,但Gum類必須有默認構造方法(無參)

3,用類字面常量 .class和Class.forName均可以建立對類的應用,可是不一樣點在於,用Gum.class建立Class對象的應用時,不會自動初始化該Class對象(static子句不會執行)

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是在編譯期執行的,所以在編譯時必須已經有了Toy的.class文件,否則會編譯失敗,這與 Class.forName("myblog.classloader.Toy")不一樣,後者是運行時動態加載。

可是,若是該main方法是直接寫在Toy類中,那麼調用Toy.class,會引發初始化,並輸出Initializing,緣由並非Toy.class引發的,而是該類中含有啓動方法main,該方法會致使Toy的初始化。

 4,編譯時常量。回到完整的類Toy,若是直接輸出:System.out.println(Toy.price),會發現static子句和構造方法都沒有被執行,這是由於Toy中,常量price被static final限定,這樣的常量叫作編譯時常量,對於這種常量,不須要初始化就能夠讀取。
編譯時常量必須知足3個條件:static的,final的,常量。

下面幾種都不是編譯時常量,對它們的應用,都會引發類的初始化:

static int a;

final  int b;

static final  int c= ClassInitialization.rand.nextInt(100);

static final  int d;
static {
    d=5;
}

5,static塊的本質。注意下面的代碼:

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?

執行一下,結果爲:

3
Initializing
5
10

答案是3最早輸出,Intializing隨後輸出,e輸出的是10,爲何呢?

緣由是這樣的:輸出c時,因爲c是編譯時常量,不會引發類初始化,所以直接輸出,輸出d時,d不是編譯時常量,因此會引發初始化操做,即static塊的執行,因而d被賦值爲5,e被賦值爲10,而後輸出Initializing,以後輸出d爲5,e爲10。

但e爲何是10呢?原來,JDK會自動爲e的初始化建立一個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");
        }
}    

可見,按順序執行,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,當訪問一個Java類或接口的靜態域時,只有真正聲明這個域的類或接口才會被初始化(《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的初始化。

說明

筆者在開發過程當中發現本身基礎太薄弱,讀書時除了系統學習了一下Java的基礎語法和用法、一點簡單的數據結構和設計模式以外,再無深刻系統的學習,而工做中的學習也是東晃一槍西晃一槍,不夠紮實和系統。想到一個學習方法:學到的東西可以系統的表達出來,才說明你學到手了;因而,筆者決定邊學邊寫,將學到的東西以博客的形式表達出來。

本文檔會因學習的深刻或錯誤的訂正而持續更新。

《Java系列筆記》是本人對Java的應用和學習過程當中的筆記,按知識點分章節,寫這一系列筆記的目的是學習,因爲筆者是邊學編寫的,水平有限,文中一定有疏漏之處,歡迎斧正。

文中多有參考前輩的書籍和博客之處,原則上不會原文引用,而是加入本身的理解改造之,若是有引用,必會註明出處,若有疑問,請聯繫:daniel.zhguang@gmail.com

更新記錄

2013年6月25日:發表;

2013年6月26日:加入@阿春阿曉的評論:引發初始化的6種場景;

2013年6月28日:加入連接和初始化的內容,加入示例6

本節參考資料

JAVA編程思想,第14章

Java深度歷險

Java中static塊的本質:http://www.java3z.com/cwbwebhome/article/article8/81101.html?id=2497

 java類的裝載(Loading)、連接(Linking)和初始化(Initialization):http://blog.csdn.net/biaobiaoqi/article/details/6909141

相關文章
相關標籤/搜索