java中的類加載器ClassLoader和類初始化

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

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

ClassLoader中的幾個實現類
java

    一、Bootstrap ClassLoader 這個是JVM加載自身工做須要的類,徹底由JVM本身來控制,外部沒法訪問到這個;web

    二、ExtClassLoader比較特殊的,服務的特定目標在System.getProperty("java.ext.dirs");redis

    三、AppClassLoader,父類是ExtClassLoader,java中參數-classpath中的類均可以被這個類加載器加載;數據庫

    四、URLClassLoader,通常這個類幫咱們實現了大部分的工做,自定義能夠繼承這個類,這樣僅僅在須要的地方作修改就好了;tomcat

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

動態加載數據結構

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

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

JVM加載clas文件到內存的方式
函數

    一、顯示加載:不經過代碼裏的ClassLoader調用,而是JVM來自動加載類到內存中的方式;

            1.一、經過Class中的forName;

            1.二、經過ClassLoader中的loadClass

            1.三、經過ClasLoader中的findSystemClass

    二、隱身加載:經過代碼中ClassLoader來加載的方式;

如何加載class文件

    1)加載(Loading),由類加載器執行,查找字節碼,並建立一個Class對象(只是建立);

             a)經過類的全名產生對應類的二進制數據流。(注意,若是沒找到對應類文件,只有在類實際使用時才拋出錯誤。)

              b)分析並將這些二進制數據流轉換爲方法區(JVM 的架構:方法區、堆,棧,本地方法棧,pc 寄存器)特定的數據結構(這些數據結構是實現有關的,不一樣 JVM 有不一樣實現)。這裏處理了部分檢驗,好比類文件的魔數的驗證,檢查文件是否過長或者太短,肯定是否有父類(除了 Obecjt 類)。

              c)建立對應類的 java.lang.Class 實例(注意,有了對應的 Class 實例,並不意味着這個類已經完成了加載鏈連接!)。

     2)連接(Linking),驗證字節碼,爲靜態域分配存儲空間(只是分配,並不初始化該存儲空間),解析該類建立所須要的對其它類的應用;

              a)驗證(verification)

連接的第三部解析會把類中成員方法、成員變量、類和接口的符號引用替換爲直接引用,而在這以前,須要檢測被引用的類型正確性和接入屬性是否正確(就是 public ,private 的的問題),諸如檢查 final class 又沒有被繼承,檢查靜態變量的正確性等等。(注意到實際上有一部分驗證過程已經在加載的過程當中執行了。)

              b)準備(preparation)

對類的成員變量分配空間。雖然有初始值,但這個時候不會對他們進行初始化(由於這裏不會執行任何 Java 代碼)。具體以下:全部原始類型的值都爲 0。如 float: 0f, int: 0, boolean: 0(注意 boolean 底層實現大多使用 int),引用類型則爲 null。值得注意的是,JVM 可能會在這個時期給一些有助於程序運行效率提升的數據結構分配空間。好比方發表(相似與 C++中的虛函數表,參見另外一篇博文《Java:方法的虛分派和方法表》)。

              c)解析(Resolution)

首先,爲類、接口、方法、成員變量的符號引用定位直接引用(若是符號引用先到常量池中尋找符號,再找先應的類型,無疑會耗費更多時間),完成內存結構的佈局。

而後,這一步是可選的。能夠在符號引用第一次被使用時完成,即所謂的延遲解析(late resolution)。但對用戶而言,這一步永遠是延遲解析的,即便運行時會執行 early resolution,但程序不會顯示的在第一次判斷出錯誤時拋出錯誤,而會在對應的類第一次主動使用的時候拋出錯誤!

最後,這一步與以後的類初始化是不衝突的,並不是必定要全部的解析結束之後才執行類的初始化。不一樣的 JVM 實現不一樣。詳情見另外一篇博文《Java 類加載的延遲初始化》

     3)初始化(Initialization)。

注 意:static{}子句是在類第一次被java程序主動使用時才執行且執行一次,也就是執行Class.forName()方法時。若是用類.class並不會執行static{}子句,由於它是在編譯時就已經存在類的.class文件,否則會編譯失敗。代碼能夠說明問題,注意類A的輸出結果:

動態加載類:


public class BeanUtilsTest
{
    public static void main(String[] args)
        throws Exception
    {
        Class clz = Class.forName("com.ai.redis.A");
    }
}

class A
{
    public static int VALUE;
    static
    {
        System.out.println("run parent static code.");
    }
}

輸出結果:打印run parent static code.

類.class:

public class BeanUtilsTest
{
    public static void main(String[] args)
        throws Exception
    {
        Class clz1 = A.class;
    }
}

class A
{
    public static int VALUE;
    static
    {
        System.out.println("run parent static code.");
    }
}

輸出結果:啥也沒有。

經過以上比較,下面這段代碼應該知道打印什麼了吧。

public class BeanUtilsTest
{
    public static void main(String[] args)
        throws Exception
    {
        System.out.println(A.VALUE);
    }
}

class A
{
    public static final int VALUE = 10;
    static
    {
        System.out.println("run parent static code.");
    }
}


輸出結果:10

有人要問了,爲何不打印run parent static code.由於VALUE變量是在編譯時就已經肯定的一個常量值跟類.class文件是一個道理,因此不打印。

注:編譯時常量必須知足3個條件:static的,final的,常量。

而下面的代碼都會致使打印run parent static code.

<pre class="html" name="code">    static int a;
    final int b;
    static final int c = Math.abs(10);
    static final int d;
    static
    {
        d = 5;
    }

 

PS:

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

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

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

爲何接口不能定義成員變量,而只能定義 final static 變量。

  • 1.接口是不可實例化,它的全部元素都沒必要是實例(對象)層面的。static 知足了這一點。
  • 2.若是接口的變量能被修改,那麼一旦一個子類實現了這個接口,並修改了接口中的非 final 變量,而該子類的子類再次修改這個非 final 的變量後,形成的結果就是雖然實現了相同的接口,但接口中的變量值是不同的。

綜上述,static final 更適合於接口。

參考:

一、《經過類字面常量解釋接口常量爲何只能定義爲 static final,類加載過程—Thinking in java》

二、http://blog.csdn.net/biaobiaoqi/article/details/6909141

三、http://www.cnblogs.com/zhguang/p/3154584.html

四、http://iamzhongyong.iteye.com/blog/2091549

相關文章
相關標籤/搜索