1. 程式人生 > >JAVA類的載入、連線與初始化

JAVA類的載入、連線與初始化

JAVA類的載入、連線與初始化

類的宣告週期總共分為5個步驟1、載入2、連線3、初始化4、使用5、解除安裝

當java程式需要某個類的時候,java虛擬機器會確保這個類已經被載入、連線和初始化,而連線這個類的過程分為3個步驟

1、 載入:查詢並載入這個類的二進位制資料

類的載入是指把.class檔案中的二進位制資料讀入到內從中,把他放在執行時的資料區的方法區內,後在堆區建立一個Class的物件,用來封裝類在方法區內的資料結構

java虛擬機器可以從多種來源載入類的二進位制資料,

a)       從本地檔案系統中載入類的.class檔案,常用的方式

b)       通過網路下載.class檔案

c)        從ZIP、JAR或其他型別提取.class檔案

d)       從一個專有資料庫中提取.class檔案

e)       把一個Java原始檔動態編譯為.class檔案

類的載入最終產品時位於執行時資料區的堆區的Class物件,Class物件封裝了類在方法區的資料結構,並向java程式提供類訪問該類在方法區資料結構的介面

 

 

類的載入是由類的載入器完成的,類的載入器分為兩種:

a)       java虛擬機器自帶的載入器,包括啟動類載入器,擴充套件類載入器,和系統類家在西

b)       使用者自定義載入器:ClassLoader類的子類,使用者可以通過實現該類來定製類的載入方式

類的載入器並不需要等到某個類被首次使用時初始化,java虛擬機器規範允許類載入器在預料某個類將要被使用時優先載入它,如果在預先載入的過程沒有找到.class檔案或者存在錯誤,類載入器必須等到程式首次主動使用該類時才丟擲LinkageError異常,如果這個類一直沒有被程式主動使用,則類載入器不會丟擲異常

2、 連線:包括驗證階段、準備階段、和解析二進位制資料階段

a)       驗證階段:驗證類的正確性,如類的語法

類的驗證目的是確定java類二進位制資料的正確性,也因為java虛擬機器不知道.class檔案是如何被建立的,有可能是正常建立,也有可能是黑客特質破壞虛擬機器的所以要有驗證環節,提高程式的健壯性

類的驗證包括:

01、           類檔案的結構檢查:確保類檔案遵循java類的固定格式

02、           語義檢查:確保類本身符合java語法規定,如驗證final修飾的類是否有子類、final修飾的方法是否有重寫

03、           位元組碼驗證:確保位元組碼流可以被java虛擬機器正確的執行,它是由操作碼的單位元組指令組成的序列,每一個操作碼都跟著一個或多個運算元,java虛擬機器會驗證該運算元是否合法

04、           二進位制相容驗證:確保類與類之間引用的協調性;如A類中a方法引用B類中b方法,虛擬機器會驗證A類時會檢查方法區內是否有B類的b方法,如果不存在或不相容時,會丟擲NoSuchMethodError異常

b)       準備階段:為類的靜態變數分配記憶體空間,並將其賦予預設值,注意是初始值 如staticint型別初始值為0

c)        解析階段:將類中的符號引用轉換為直接引用,主要針對類或介面、欄位、類方法、介面方法、方法型別、方法控制代碼和呼叫點限定符類符號引用進行。如在A類中a方法中引用了B類中的b方法,將b方法放入A類中

3、 初始化:為類的靜態變數賦予正確的初始值;如static inta=50;這時50才賦給a這個靜態變數,所有java虛擬機器在每隔類或介面被java程式首次主動使用時才初始化它們,並且初始化階段時執行類構造器<clinit>()方法的過程,<clinit>()方法是由編譯器自動收集類中的所有類變數賦值動作與靜態程式碼塊中的語句合併產生的,編譯器收集順序是由語句在原始檔出現的順序所決定的;如靜態程式碼塊只能訪問定義在靜態程式碼塊之前的變數,而在之後定義的變數可以賦值,但不可以訪問,如:

publicclass Test{

        static{

               i=1

               System.out.println(i)//該行程式碼會編譯錯誤:Cannot reference a field before it is defined

}

static int i;

}

上面說到<clinit>()方法,<init>()方法這兩個方法是class檔案的兩種編譯產生方法

它們的區別

<clinit>方法是在虛擬機器裝在一個類的時候呼叫<clinit>方法。而<init>方法則是在一個類例項化的時候呼叫

<clinit>方法與<init>方法的不同是<clinit>不需要顯示的呼叫父類構造器,虛擬機器會在保證子類<init>方法執行之前父類的<clinit>方法已經執行完畢,著也就意味著在父類中定義的靜態語句會優先於子類變數以及靜態程式碼塊

而<clinit>方法對於類或介面不是必須的,如:一個類或者介面沒有靜態語句塊,也沒有對變數的賦值操作,那麼編譯器可以不為該類生產<clinit>方法,因此,一個父介面不會因為子介面或實現類的初始化而初始化,只有當程式首次使用父介面或特定介面中的靜態變數時才會導致初始化;

虛擬機器會保證一個類<clinit>方法在多執行緒環境中被正確的加鎖、同步,如果由多個執行緒同時去初始化一個類,那麼只有一個類回去執行<clinit>操作,其他執行緒會阻塞等待,直到該類<clinit>執行完畢,而如果一個類的<clinit>方法要做的事情很多,就可能造成多個執行緒阻塞,在實際應用中這種阻塞是被隱藏的

測試程式碼:

public class Text{

static class DeadLoopClass

    {

        static

        {

            if(true)

            {

                System.out.println(Thread.currentThread()+"init DeadLoopClass");

                while(true)

                {

                }

            }

        }

    }

public static void main(String[] args) {

        Runnable runnable=new Runnable() {

              

               @Override

               public void run() {

                      System.out.println(Thread.currentThread()+" start");

                DeadLoopClass dlc = new DeadLoopClass();

                System.out.println(Thread.currentThread()+" run over");                    

               }

        };

        Thread a=new Thread(runnable);

        Thread b=new Thread(runnable);

        a.start();

        b.start();

}

}

執行結果:(即一條執行緒在死迴圈以模擬長時間操作,另一條執行緒在阻塞等待)

Thread[Thread-0,5,main] start

Thread[Thread-1,5,main] start

Thread[Thread-1,5,main]init DeadLoopClass

需要注意的是:雖然其他執行緒會被阻塞,但如果執行了<clinit>方法那條執行緒退出<clinit>方法之後,其他執行緒喚醒之後也不會在進入<clinit>方法,在同一個類載入器下,一個型別只會被初始化一次

測試程式碼:(將靜態內部類程式碼換為)

static class DeadLoopClass{

              static

        {

            System.out.println(Thread.currentThread() + "init DeadLoopClass");

            try {

                            TimeUnit.SECONDS.sleep(10);

                     } catch (InterruptedException e) {

                            e.printStackTrace();

                     }

        }

}

執行結果:

Thread[Thread-0,5,main] start

Thread[Thread-1,5,main] start

Thread[Thread-0,5,main]init DeadLoopClass

Thread[Thread-0,5,main] run over

Thread[Thread-1,5,main] run over

虛擬機器嚴格規定了有5中情況必須要對類進行初始化

1、 當遇到new,getstatic,putstatic,invokestatic這種失調位元組碼指定時,如果類沒有進行初始化則先初始化,生成這4條指令的常見java場景:使用new關鍵字建立物件時、讀取或者設定一些類的靜態欄位(被final修飾或已經在編譯器常量池的靜態欄位除外,但final型別的靜態常量,如果在編譯是不能計算出常量的取值,則會看作對類的主動使用,會初始化該類)或靜態方法的時候,以及要呼叫靜態方法的時候

2、 當使用java.lang.reflect包進行反射呼叫的時候如果類沒有初始化,則先初始化

3、 當初始化一個類的時候發現父類沒有進行初始化,這時要先初始化父類

4、 虛擬機器啟動時,使用者要執行的主類(如包含main方法的類),如果沒有初始化要先進行初始化

5、 當使用jdk1.7動態語言支援時,如果一個java.lang.invoke.MethodHandle例項最後的解析結果REF_getstatic,REF_putstatic,REF_invokeStatic的方法控制代碼,並且這個方法控制代碼所對應的類沒有進行初始化,則需要先出觸發其初始化。

當定義陣列的引用類不會觸發此類的初始化階段

測試程式碼

public static void main(String[] args) {

              DeadLoopClass[] loopClasses=new DeadLoopClass[10];

}

結果什麼都沒有

常量在編譯階段會存入類的常量池中,本質想沒有直接引用定義常量的這個類,所以不會觸發該類的初始化

測試程式碼:

public class Text{

       final static String mm="abc";

       static{

              System.out.println("執行了靜態程式碼塊");

       }

}

public class Test01 {

       public static void main(String[] args) {

              System.out.println(Text.mm);

       }

}

注意:main方法不可以寫在Text類中因為含有main方法的類會被初始化

初始化大體步驟

1、 假如這個類還滅有被載入和連線,那就先載入和連線

2、 假如類中存在直接的父類,或者間接父類,並卻該父類沒有被初始化,則先初始化父類

3、 類中的初始化語句從上到下執行

類的載入器

java虛擬機器自帶了以下幾種載入器

根(bootstrap)載入器:該載入器沒有父類載入器。他負責載入虛擬機器核心類庫,如java.lang.*等。從下面例子可以看出java.lang.Object就是由根類載入器載入的,根載入器是從系統屬性sun.boot.class.path所指定的目錄載入類庫。根載入器的實現依賴於底層的作業系統,屬於虛擬機器實現的一部分。

擴充套件(Extension)類載入器:它的父載入器是根載入器。他從java.ext.dirs系統屬性所指定的目錄中載入類庫,或者從JDK的安裝目錄的jre\lib\ext子目錄(擴充套件目錄)下載入類庫,如果把使用者JAR檔案放在該目錄下,會自動由擴充套件類載入,擴充套件類載入器是純java類,是java.lang.ClassLoader類的子類

系統(System)類載入器:也成應用載入器,它的父類是擴充套件類載入器,它的環境變數classpath或者系統屬性java.class.path所指定的目錄的載入類,它是使用者自定義的類載入器的預設父載入器。系統載入器是純java類,是java.lang.ClassLoader類的子類

使用者自定義類載入器:實現java提供的系統載入器ClassLoader抽象類

類載入器關係圖:

 

 

測試Object的類載入器是根載入器:

public class Text{

public static void main(String[] args) {

        Class c;

        ClassLoader cl,cll;

        cl=ClassLoader.getSystemClassLoader();//獲取系統載入器

        System.out.println(cl);

        while(cl!=null){

               cll=cl;

               cl=cl.getParent();

               System.out.println(cll+"這個類的父類載入器是"+cl);

        }

        try {

               c = Class.forName("java.lang.Object");

               cll=c.getClassLoader();//獲取Object的類載入器

               System.out.println("Object的類載入器是"+cll);

               c = Class.forName("test.Text");

               cll=c.getClassLoader();//獲取當前類的載入器

               System.out.println("Text的類載入器是"+cll);

        } catch (Exception e) {

               e.printStackTrace();

        }

}

}

列印結果:

[email protected]

[email protected]4這個類的父類

載入器是[email protected]

[email protected]這個類的父類載入器是null

Object的類載入器是null

Text的類載入器是[email protected]

第一行獲取了系統載入器

第二行是系統載入器的父載入器是擴充套件載入器,

第三行是系統載入器的父載入器是null(根載入器,根載入器是用null表示,這是為了保護虛擬機器的安全,防止黑客利用根載入器,載入非法的類,從而去破壞虛擬機器)

第四行Text的類載入器是系統載入器ClassLoader類載入

類載入器的父親委託(Parent Delegation)機制

在父親委託機制種,每個載入器都按照父子關係形成樹形結構,出了類的跟載入器以外,其他載入器都有且只有一個父類載入器,

例:

loader2繼承了loader1,loader1繼承了ClassLoader這時loader2去載入一個類

Class c=loader2.loadClass(“Text”)

執行過程是loader2回去自己的名稱空間查詢該類是否被載入,如被載入直接返回該類的Class引用

如果沒有載入則loader2會請求loader1代載入,loader1在請求ClassLoader,ClassLoder在請求擴充套件載入器,擴充套件載入器在請求根載入器,如果父類載入器不能載入則子類載入器載入,一次類推,有一級可以載入則返回Class引用,如果所有載入器都不能載入則丟擲ClassNotFoundException

如有一個類能夠成功載入Text類,那麼這個類載入器被稱為定義載入器,能夠返回Class物件引用的類載入器和定義類載入器都稱為初始類載入器,

假設loader1成功載入了Text類那麼loader1是定義類載入器,而loader2是Text類的引用載入器,所以loader1,loader2都是Text的初始類載入器

注意:載入器之間的父子關係實際上是載入器物件之間的包裝關係,而不是類之間的繼承關係,一對父子載入器可能是同一個載入器類的兩個例項,也可能不是。在子載入器物件中包裝了一個父載入器物件,如:

public class MyClassLoader extends ClassLoader{

private ClassLoader loader;

       public MyClassLoader(){}

       public Test01(ClassLoader loader) {

               super(loader);

       }

       @Override

       public Class<?> loadClass(String name) throws ClassNotFoundException {

               return super.loadClass(name);

       }

}

ClassLoaderloader1=new MyClassLoader();

ClassLoader loader2=new MyClassLoader(loader1);

父親委託機制的優點是提高軟體體統的安全性。因為在刺激之下,使用者自定義的載入器不可能載入父類載入器的可靠類,從而防止不可靠的程式碼去代替父載入器去載入可靠的程式碼,例如java.lang.Object類只能由根類載入器載入,其他使用者任何自定義的類載入器都不不可能載入含有惡意程式碼的Object類

名稱空間

每個類載入器都有自己的名稱空間,名稱空間是由該載入器以所有父載入器所載入的類組成。在同一個名稱空間中不會出現類的完全限定名一樣的兩個類,但在不同的名稱空間中有可能會出現兩個完全限定名一樣的類

當同一個.class檔案被一個使用者自定義loader1載入器載入和使用者自定義loader3載入器載入這時方法區會生成兩個該class類,也就是說在loader1和loader3各自的名稱空間中都存在Sample和Dog類

 

 

不同的類載入器的名稱空間存在以下關係:

1、 同意名稱空間內的類是相互可見的

2、 子載入器的名稱空間包含所有附加在其的名稱空間,因此子載入器載入的類能看見父載入器載入的類,如系統載入器載入的類可以看見根載入器載入的類。

3、 由父載入器載入的類對子載入器載入的類是不可見的

4、 兩個載入器之間沒有直接或間接父子關係,則這兩個載入器載入的類相互不可見

所謂A類可見B類是在A類中可以引用B類的名字,如:

class A{

B b=new B();

}

兩個不同的名稱空間內的類是專案不可見的,但可以通過java的反射機制來訪問物件的例項與方法。

執行時包

由同一個類載入器載入屬於同包的類組成可執行時包,決定兩個類是不是一執行時包要看它們的包名是否相同,還要看載入器是否相同。只有屬於同意執行時包的類才能訪問預設許可權修飾符的類和類的成員,這樣避免了使用者自定義的類去冒充核心類庫的類,如自定義了一個java.lang.Spy,並由使用者自定義的類載入器載入,由於java.lang.Spy和核心類庫不是一個載入器載入的,它們屬於不同執行時包,所以java.lang.Spy不能訪問核心類的java.lang包下面的預設許可權修飾符的成員

URLClassLoader

在JDK的java.net包中,提供了一個功能強大的URLClassLoader類,它不僅能從本地檔案中載入類,還可以從網上下載類。java程式可直接用URLClassLoader類作為使用者自定義的類載入器

URLClassLoader類的構造方法:

public URLClassLoader(URL[] urls);//urls是存放URL的陣列

public URLClassLoader(URL[] urls, ClassLoader parent)//parent是指定父載入器

URLClassLoader類的預設父載入器是系統載入器

簡單運用:

URLurl=new URL(“www.XXXX.com/java/classes/”);

URLClassLoader loader=new URLClassLoader(new URL[]{url});

Class<?> clazz=loader.loadClass(“XXX”);

clazz.newInstance();

4、 類的解除安裝

由java虛擬機器自帶的類載入器載入的類,在虛擬機器的整個宣告週期中,始終不會解除安裝,如根載入器、擴充套件載入器、系統載入器,虛擬機器本身會始終引用這些類載入器,而類載入器會始終引用它們所載入的Class物件,因此這些Class物件始終是可觸及的;

一個類的何時被解除安裝的是當該類的Class物件不再被引用時,該類在方法區內的資料也會被解除安裝

由使用者自定義的類載入器所載入的類是可以被解除安裝的,在類載入器的內部視線中,是用java集合來存放所有載入類的引用,

執行程式碼:

 

 

 

 

當將引用變數變為null的時候,此時Sample物件結束宣告週期,和類載入器MyClassLoader也結束生命週期這時,Sample類在方法區的二進位制資料就被解除安裝

執行結果:

 

objClass物件引用的雜湊碼改變了說明objClass變數兩次引用了不同的Class物件