1. 程式人生 > >java的類載入機制以及載入模型

java的類載入機制以及載入模型

1、概述

我們先來看下下面的java程式碼:(大家感覺會輸出什麼結果呢?)
public class TestClass {
	static
    {
        System.out.println("TestClass");
    }
	
	 public static void main(String[] args)
	    {
	        System.out.println(SubClass.value);
	    }
}
 class SuperClass extends TestClass
{
    static
    {
        System.out.println("SuperClass init!");
    }
 
    public static int value = 123;
 
    public SuperClass()
    {
        System.out.println("init SuperClass");
    }
}
 class SubClass extends SuperClass
{
    static
    {
        System.out.println("SubClass init");
    }
 
    static int a;
 
    public SubClass()
    {
        System.out.println("init SubClass");
    }
}
執行的結果:
TestClass
SuperClass init!
123

2、類載入的過程

整個類從載入到虛擬機器記憶體開始,再到卸載出記憶體結束的整的過程中包含了以下生命週期: 載入、驗證、準備、解析、初始化、使用、解除安裝 其中驗證、準備、解析三個階段統稱為連線。


2.1 載入 當一個類在載入的過程中需要以下操作: 1)通過一個類 的全限定名來獲取定義此類的二進位制位元組流。 2)將這個位元組流所代表的靜態儲存結構轉化為方法區的執行時資料結構。
3)在記憶體中生成一個代表這個類的java.lang.Class物件,作為方法區這個類的各種資料的訪問入口。
2.2 驗證 驗證是連線階段的第一步,這一階段的目的是為了確保Class檔案的位元組流中包含的資訊符合當前虛擬機器的要求,並且不會危害虛擬機器自身的安全。
驗證階段大致會完成4個階段的檢驗動作:
1)檔案格式驗證:驗證位元組流是否符合Class檔案格式的規範;例如:是否以魔術0xCAFEBABE開頭、主次版本號是否在當前虛擬機器的處理範圍之內、常量池中的常量是否有不被支援的型別。
2)元資料驗證:對位元組碼描述的資訊進行語義分析(注意:對比javac編譯階段的語義分析),以保證其描述的資訊符合Java語言規範的要求;例如:這個類是否有父類,除了java.lang.Object之外。
3)位元組碼驗證:通過資料流和控制流分析,確定程式語義是合法的、符合邏輯的。
4)符號引用驗證:確保解析動作能正確執行。

2.3 準備 準備階段是正式為類變數分配記憶體並設定類變數初始值的階段,這些變數所使用的記憶體都將在方法區中進行分配。這時候進行記憶體分配的僅包括類變數(被static修飾的變數),而不包括例項變數,例項變數將會在物件例項化時隨著物件一起分配在堆中。其次,這裡所說的初始值“通常情況”下是資料型別的零值,假設一個類變數的定義為:
public static int value=123;
那變數value在準備階段過後的初始值為0而不是123.因為這時候尚未開始執行任何java方法,而把value賦值為123的putstatic指令是程式被編譯後,存放於類構造器()方法之中,所以把value賦值為123的動作將在初始化階段才會行。
至於“特殊情況”是指:public static final int value=123,即當類欄位的欄位屬性是ConstantValue時,會在準備階段初始化為指定的值,所以標註為final之後,value的值在準備階段初始化為123而非0.

2.4 解析
解析階段是虛擬機器將常量池內的符號引用替換為直接引用的過程。解析動作主要針對類或介面、欄位、類方法、介面方法、方法型別、方法控制代碼和呼叫點限定符7類符號引用進行。

2.5 初始化
類初始化階段是類載入過程的最後一步,到了初始化階段,才真正開始執行類中定義的java程式程式碼。在準備極端,變數已經付過一次系統要求的初始值,而在初始化階段,則根據程式猿通過程式制定的主管計劃去初始化類變數和其他資源,或者說:初始化階段是執行類構造器<clinit>()方法的過程.<clinit>()方法是由編譯器自動收集類中的所有類變數的賦值動作和靜態語句塊static{}中的語句合併產生的,編譯器收集的順序是由語句在原始檔中出現的順序所決定的,靜態語句塊只能訪問到定義在靜態語句塊之前的變數,定義在它之後的變數,在前面的靜態語句塊可以賦值,但是不能訪問。如下:
public class Test
{
    static
    {
        i=0;
        System.out.println(i);//這句編譯器會報錯:Cannot reference a field before it is defined(非法向前應用)
    }
    static int i=1;
}
<clinit>()方法與例項構造器<init>()方法不同,它不需要顯示地呼叫父類構造器,虛擬機器會保證在子類<init>()方法執行之前,父類的<clinit>()方法方法已經執行完畢,回到本文開篇的舉例程式碼中,結果會列印輸出:TestClass就是這個道理。由於父類的<clinit>()方法先執行,也就意味著父類中定義的靜態語句塊要優先於子類的變數賦值操作。 <clinit>()方法對於類或者介面來說並不是必需的,如果一個類中沒有靜態語句塊,也沒有對變數的賦值操作,那麼編譯器可以不為這個類生產<clinit>()方法。
介面中不能使用靜態語句塊,但仍然有變數初始化的賦值操作,因此介面與類一樣都會生成<clinit>()方法。但介面與類不同的是,執行介面的<clinit>()方法不需要先執行父介面的<clinit>()方法。只有當父介面中定義的變數使用時,父接口才會初始化。另外,介面的實現類在初始化時也一樣不會執行介面的<clinit>()方法。
虛擬機器會保證一個類的<clinit>()方法在多執行緒環境中被正確的加鎖、同步,如果多個執行緒同時去初始化一個類,那麼只會有一個執行緒去執行這個類的<clinit>()方法,其他執行緒都需要阻塞等待,直到活動執行緒執行<clinit>()方法完畢。如果在一個類的<clinit>()方法中有好事很長的操作,就可能造成多個執行緒阻塞,在實際應用中這種阻塞往往是隱藏的。
我們看下面的程式碼:

public class TestClass3 {
	public static void main(String[] args) {
		Runnable script = new Runnable(){
            public void run()
            {
                System.out.println(Thread.currentThread()+" start");
                A dlc = new A();
                System.out.println(Thread.currentThread()+" run over");
            }
        };
        Thread thread1 = new Thread(script);
        Thread thread2 = new Thread(script);
        thread1.start();
        thread2.start();
	}

	
	static class A{
		static
        {
            if(true)
            {//死迴圈,將造成其他執行緒阻塞
                System.out.println(Thread.currentThread()+"init A");
                while(true)
                {
                }
            }
        }
		
	}
}

執行結果:
Thread[Thread-0,5,main] start
Thread[Thread-1,5,main] start
Thread[Thread-1,5,main]init A
我們對上面的程式碼進行修改後,看下:
public class TestClass3 {
	public static void main(String[] args) {
		Runnable script = new Runnable(){
            public void run()
            {
                System.out.println(Thread.currentThread()+" start");
                A dlc = new A();
                System.out.println(Thread.currentThread()+" run over");
            }
        };
        Thread thread1 = new Thread(script);
        Thread thread2 = new Thread(script);
        thread1.start();
        thread2.start();
	}

	
	static class A{
		static
        {
            if(true)
            {
                System.out.println(Thread.currentThread()+"init A");
                try {//讓該執行緒睡眠5s
					TimeUnit.SECONDS.sleep(5);
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
            }
        }
		
	}
}

執行結果:
Thread[Thread-0,5,main] start
Thread[Thread-0,5,main]init A物件
Thread[Thread-1,5,main] start
Thread[Thread-1,5,main] run over
Thread[Thread-0,5,main] run over

綜上:我們發現innt A物件只輸出了一次,所以說如果執行<clinit>()方法的那條執行緒退出<clinit>()方法後,其他執行緒喚醒之後不會再次進入<clinit>()方法。同一個類載入器下,一個型別只會初始化一次。