Java虛擬機類加載機制

以下圖所示,JVM類加載機制分爲五個部分:加載,驗證,準備,解析,初始化,下面咱們就分別來看一下這五個過程。html

加載

加載是類加載過程當中的一個階段,這個階段會在內存中生成一個表明這個類的java.lang.Class對象,做爲方法區這個類的各類數據的入口。注意這裏不必定非得要從一個Class文件獲取,這裏既能夠從ZIP包中讀取(好比從jar包和war包中讀取),也能夠在運行時計算生成(動態代理),也能夠由其它文件生成(好比將JSP文件轉換成對應的Class類)。java

驗證

這一階段的主要目的是爲了確保Class文件的字節流中包含的信息是否符合當前虛擬機的要求,而且不會危害虛擬機自身的安全。數據庫

準備

準備階段是正式爲類變量分配內存並設置類變量的初始值階段,即在方法區中分配這些變量所使用的內存空間。注意這裏所說的初始值概念,好比一個類變量定義爲:編程

1數組

public static int v = 8080;安全

實際上變量v在準備階段事後的初始值爲0而不是8080,將v賦值爲8080的putstatic指令是程序被編譯後,存放於類構造器<client>方法之中,這裏咱們後面會解釋。
可是注意若是聲明爲:網絡

1數據結構

public static final int v = 8080;多線程

在編譯階段會爲v生成ConstantValue屬性,在準備階段虛擬機會根據ConstantValue屬性將v賦值爲8080。jvm

解析

解析階段是指虛擬機將常量池中的符號引用替換爲直接引用的過程。符號引用就是class文件中的:

  • CONSTANT_Class_info
  • CONSTANT_Field_info
  • CONSTANT_Method_info

等類型的常量。

下面咱們解釋一下符號引用和直接引用的概念:

  • 符號引用與虛擬機實現的佈局無關,引用的目標並不必定要已經加載到內存中。各類虛擬機實現的內存佈局能夠各不相同,可是它們能接受的符號引用必須是一致的,由於符號引用的字面量形式明肯定義在Java虛擬機規範的Class文件格式中。
  • 直接引用能夠是指向目標的指針,相對偏移量或是一個能間接定位到目標的句柄。若是有了直接引用,那引用的目標一定已經在內存中存在。

初始化

初始化階段是類加載最後一個階段,前面的類加載階段以後,除了在加載階段能夠自定義類加載器之外,其它操做都由JVM主導。到了初始階段,纔開始真正執行類中定義的Java程序代碼。

初始化階段是執行類構造器<client>方法的過程。<client>方法是由編譯器自動收集類中的類變量的賦值操做和靜態語句塊中的語句合併而成的。虛擬機會保證<client>方法執行以前,父類的<client>方法已經執行完畢。p.s: 若是一個類中沒有對靜態變量賦值也沒有靜態語句塊,那麼編譯器能夠不爲這個類生成<client>()方法。

注意如下幾種狀況不會執行類初始化:

  • 經過子類引用父類的靜態字段,只會觸發父類的初始化,而不會觸發子類的初始化。
  • 定義對象數組,不會觸發該類的初始化。
  • 常量在編譯期間會存入調用類的常量池中,本質上並無直接引用定義常量的類,不會觸發定義常量所在的類。
  • 經過類名獲取Class對象,不會觸發類的初始化。
  • 經過Class.forName加載指定類時,若是指定參數initialize爲false時,也不會觸發類初始化,其實這個參數是告訴虛擬機,是否要對類進行初始化。
  • 經過ClassLoader默認的loadClass方法,也不會觸發初始化動做。

類加載器

虛擬機設計團隊把加載動做放到JVM外部實現,以便讓應用程序決定如何獲取所需的類,JVM提供了3種類加載器:

  • 啓動類加載器(Bootstrap ClassLoader):負責加載 JAVA_HOME\lib 目錄中的,或經過-Xbootclasspath參數指定路徑中的,且被虛擬機承認(按文件名識別,如rt.jar)的類。
  • 擴展類加載器(Extension ClassLoader):負責加載 JAVA_HOME\lib\ext 目錄中的,或經過java.ext.dirs系統變量指定路徑中的類庫。
  • 應用程序類加載器(Application ClassLoader):負責加載用戶路徑(classpath)上的類庫。

JVM經過雙親委派模型進行類的加載,固然咱們也能夠經過繼承java.lang.ClassLoader實現自定義的類加載器。

當一個類加載器收到類加載任務,會先交給其父類加載器去完成,所以最終加載任務都會傳遞到頂層的啓動類加載器,只有當父類加載器沒法完成加載任務時,纔會嘗試執行加載任務。

採用雙親委派的一個好處是好比加載位於rt.jar包中的類java.lang.Object,無論是哪一個加載器加載這個類,最終都是委託給頂層的啓動類加載器進行加載,這樣就保證了使用不一樣的類加載器最終獲得的都是一樣一個Object對象。

在有些情境中可能會出現要咱們本身來實現一個類加載器的需求,因爲這裏涉及的內容比較普遍,我想之後單獨寫一篇文章來說述,不過這裏咱們仍是稍微來看一下。咱們直接看一下jdk中的ClassLoader的源碼實現:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

protected synchronized Class<?> loadClass(String name, boolean resolve)

        throws ClassNotFoundException {

    // First, check if the class has already been loaded

    Class c = findLoadedClass(name);

    if (c == null) {

        try {

            if (parent != null) {

                c = parent.loadClass(name, false);

            } else {

                c = findBootstrapClass0(name);

            }

        } catch (ClassNotFoundException e) {

            // If still not found, then invoke findClass in order

            // to find the class.

            c = findClass(name);

        }

    }

    if (resolve) {

        resolveClass(c);

    }

    return c;

}

  • 首先經過Class c = findLoadedClass(name);判斷一個類是否已經被加載過。
  • 若是沒有被加載過執行if (c == null)中的程序,遵循雙親委派的模型,首先會經過遞歸從父加載器開始找,直到父類加載器是Bootstrap ClassLoader爲止。
  • 最後根據resolve的值,判斷這個class是否須要解析。

而上面的findClass()的實現以下,直接拋出一個異常,而且方法是protected,很明顯這是留給咱們開發者本身去實現的,這裏咱們之後咱們單獨寫一篇文章來說一下如何重寫findClass方法來實現咱們本身的類加載器。

1

2

3

protected Class<?> findClass(String name) throws ClassNotFoundException {

    throw new ClassNotFoundException(name);

}

Reference

看到這個題目,不少人會以爲我寫個人java代碼,至於類,JVM愛怎麼加載就怎麼加載,博主有很長一段時間也是這麼認爲的。隨着編程經驗的日積月累,愈來愈感受到了解虛擬機相關要領的重要性。閒話很少說,老規矩,先來一段代碼吊吊胃口。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

public class SSClass

{

    static

    {

        System.out.println("SSClass");

    }

}   

public class SuperClass extends SSClass

{

    static

    {

        System.out.println("SuperClass init!");

    }

 

    public static int value = 123;

 

    public SuperClass()

    {

        System.out.println("init SuperClass");

    }

}

public class SubClass extends SuperClass

{

    static

    {

        System.out.println("SubClass init");

    }

 

    static int a;

 

    public SubClass()

    {

        System.out.println("init SubClass");

    }

}

public class NotInitialization

{

    public static void main(String[] args)

    {

        System.out.println(SubClass.value);

    }

}

運行結果:

1

2

3

SSClass

SuperClass init!

123

答案答對了嚒?
也許有人會疑問:爲何沒有輸出SubClass init。ok~解釋一下:對於靜態字段,只有直接定義這個字段的類纔會被初始化,所以經過其子類來引用父類中定義的靜態字段,只會觸發父類的初始化而不會觸發子類的初始化。
上面就牽涉到了虛擬機類加載機制。若是有興趣,能夠繼續看下去。

類加載過程

類從被加載到虛擬機內存中開始,到卸載出內存爲止,它的整個生命週期包括:加載(Loading)、驗證(Verification)、準備(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸載(Unloading)7個階段。其中準備、驗證、解析3個部分統稱爲鏈接(Linking)。如圖所示。
這裏寫圖片描述
加載、驗證、準備、初始化和卸載這5個階段的順序是肯定的,類的加載過程必須按照這種順序循序漸進地開始,而解析階段則不必定:它在某些狀況下能夠在初始化階段以後再開始,這是爲了支持Java語言的運行時綁定(也稱爲動態綁定或晚期綁定)。如下陳述的內容都已HotSpot爲基準。

加載

在加載階段(能夠參考java.lang.ClassLoader的loadClass()方法),虛擬機須要完成如下3件事情:

  1. 經過一個類的全限定名來獲取定義此類的二進制字節流(並無指明要從一個Class文件中獲取,能夠從其餘渠道,譬如:網絡、動態生成、數據庫等);
  2. 將這個字節流所表明的靜態存儲結構轉化爲方法區的運行時數據結構;
  3. 在內存中生成一個表明這個類的java.lang.Class對象,做爲方法區這個類的各類數據的訪問入口;

加載階段和鏈接階段(Linking)的部份內容(如一部分字節碼文件格式驗證動做)是交叉進行的,加載階段還沒有完成,鏈接階段可能已經開始,但這些夾在加載階段之中進行的動做,仍然屬於鏈接階段的內容,這兩個階段的開始時間仍然保持着固定的前後順序。

驗證

驗證是鏈接階段的第一步,這一階段的目的是爲了確保Class文件的字節流中包含的信息符合當前虛擬機的要求,而且不會危害虛擬機自身的安全。
驗證階段大體會完成4個階段的檢驗動做:

  1. 文件格式驗證:驗證字節流是否符合Class文件格式的規範;例如:是否以魔術0xCAFEBABE開頭、主次版本號是否在當前虛擬機的處理範圍以內、常量池中的常量是否有不被支持的類型。
  2. 元數據驗證:對字節碼描述的信息進行語義分析(注意:對比javac編譯階段的語義分析),以保證其描述的信息符合Java語言規範的要求;例如:這個類是否有父類,除了java.lang.Object以外。
  3. 字節碼驗證:經過數據流和控制流分析,肯定程序語義是合法的、符合邏輯的。
  4. 符號引用驗證:確保解析動做能正確執行。

驗證階段是很是重要的,但不是必須的,它對程序運行期沒有影響,若是所引用的類通過反覆驗證,那麼能夠考慮採用-Xverifynone參數來關閉大部分的類驗證措施,以縮短虛擬機類加載的時間。

準備

準備階段是正式爲類變量分配內存並設置類變量初始值的階段,這些變量所使用的內存都將在方法區中進行分配。這時候進行內存分配的僅包括類變量(被static修飾的變量),而不包括實例變量,實例變量將會在對象實例化時隨着對象一塊兒分配在堆中。其次,這裏所說的初始值「一般狀況」下是數據類型的零值,假設一個類變量的定義爲:

1

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.

解析

解析階段是虛擬機將常量池內的符號引用替換爲直接引用的過程。解析動做主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調用點限定符7類符號引用進行。

初始化

類初始化階段是類加載過程的最後一步,到了初始化階段,才真正開始執行類中定義的java程序代碼。在準備極端,變量已經付過一次系統要求的初始值,而在初始化階段,則根據程序猿經過程序制定的主管計劃去初始化類變量和其餘資源,或者說:初始化階段是執行類構造器<clinit>()方法的過程.
<clinit>()方法是由編譯器自動收集類中的全部類變量的賦值動做和靜態語句塊static{}中的語句合併產生的,編譯器收集的順序是由語句在源文件中出現的順序所決定的,靜態語句塊只能訪問到定義在靜態語句塊以前的變量,定義在它以後的變量,在前面的靜態語句塊能夠賦值,可是不能訪問。以下:

1

2

3

4

5

6

7

8

9

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>()方法方法已經執行完畢,回到本文開篇的舉例代碼中,結果會打印輸出:SSClass就是這個道理。
因爲父類的<clinit>()方法先執行,也就意味着父類中定義的靜態語句塊要優先於子類的變量賦值操做。
<clinit>()方法對於類或者接口來講並非必需的,若是一個類中沒有靜態語句塊,也沒有對變量的賦值操做,那麼編譯器能夠不爲這個類生產<clinit>()方法。
接口中不能使用靜態語句塊,但仍然有變量初始化的賦值操做,所以接口與類同樣都會生成<clinit>()方法。但接口與類不一樣的是,執行接口的<clinit>()方法不須要先執行父接口的<clinit>()方法。只有當父接口中定義的變量使用時,父接口才會初始化。另外,接口的實現類在初始化時也同樣不會執行接口的<clinit>()方法。
虛擬機會保證一個類的<clinit>()方法在多線程環境中被正確的加鎖、同步,若是多個線程同時去初始化一個類,那麼只會有一個線程去執行這個類的<clinit>()方法,其餘線程都須要阻塞等待,直到活動線程執行<clinit>()方法完畢。若是在一個類的<clinit>()方法中有好事很長的操做,就可能形成多個線程阻塞,在實際應用中這種阻塞每每是隱藏的。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

package jvm.classload;

 

public class DealLoopTest

{

    static class DeadLoopClass

    {

        static

        {

            if(true)

            {

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

                while(true)

                {

                }

            }

        }

    }

 

    public static void main(String[] args)

    {

        Runnable script = new Runnable(){

            public void run()

            {

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

                DeadLoopClass dlc = new DeadLoopClass();

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

            }

        };

 

        Thread thread1 = new Thread(script);

        Thread thread2 = new Thread(script);

        thread1.start();

        thread2.start();

    }

}

運行結果:(即一條線程在死循環以模擬長時間操做,另外一條線程在阻塞等待)

1

2

3

Thread[Thread-0,5,main] start

Thread[Thread-1,5,main] start

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

須要注意的是,其餘線程雖然會被阻塞,但若是執行<clinit>()方法的那條線程退出<clinit>()方法後,其餘線程喚醒以後不會再次進入<clinit>()方法。同一個類加載器下,一個類型只會初始化一次。
將上面代碼中的靜態塊替換以下:

1

2

3

4

5

6

7

8

9

10

11

12

static

        {

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

            try

            {

                TimeUnit.SECONDS.sleep(10);

            }

            catch (InterruptedException e)

            {

                e.printStackTrace();

            }

        }

運行結果:

1

2

3

4

5

Thread[Thread-0,5,main] start

Thread[Thread-1,5,main] start

Thread[Thread-1,5,main]init DeadLoopClass (以後sleep 10s)

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

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

虛擬機規範嚴格規定了有且只有5中狀況(jdk1.7)必須對類進行「初始化」(而加載、驗證、準備天然須要在此以前開始):

  1. 遇到new,getstatic,putstatic,invokestatic這失調字節碼指令時,若是類沒有進行過初始化,則須要先觸發其初始化。生成這4條指令的最多見的Java代碼場景是:使用new關鍵字實例化對象的時候、讀取或設置一個類的靜態字段(被final修飾、已在編譯器把結果放入常量池的靜態字段除外)的時候,以及調用一個類的靜態方法的時候。
  2. 使用java.lang.reflect包的方法對類進行反射調用的時候,若是類沒有進行過初始化,則須要先觸發其初始化。
  3. 當初始化一個類的時候,若是發現其父類尚未進行過初始化,則須要先觸發其父類的初始化。
  4. 當虛擬機啓動時,用戶須要指定一個要執行的主類(包含main()方法的那個類),虛擬機會先初始化這個主類。
  5. 當使用jdk1.7動態語言支持時,若是一個java.lang.invoke.MethodHandle實例最後的解析結果REF_getstatic,REF_putstatic,REF_invokeStatic的方法句柄,而且這個方法句柄所對應的類沒有進行初始化,則須要先出觸發其初始化。

開篇已經舉了一個範例:經過子類引用付了的靜態字段,不會致使子類初始化。
這裏再舉兩個例子。
1. 經過數組定義來引用類,不會觸發此類的初始化:(SuperClass類已在本文開篇定義)

1

2

3

4

5

6

7

public class NotInitialization

{

    public static void main(String[] args)

    {

        SuperClass[] sca = new SuperClass[10];

    }

}

運行結果:(無)
2. 常量在編譯階段會存入調用類的常量池中,本質上並無直接引用到定義常量的類,所以不會觸發定義常量的類的初始化:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

public class ConstClass

{

    static

    {

        System.out.println("ConstClass init!");

    }

    public static  final String HELLOWORLD = "hello world";

}

public class NotInitialization

{

    public static void main(String[] args)

    {

        System.out.println(ConstClass.HELLOWORLD);

    }

}

運行結果:hello world

附:昨天從論壇上看到一個例子,頗有意思,以下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

package jvm.classload;

 

public class StaticTest

{

    public static void main(String[] args)

    {

        staticFunction();

    }

 

    static StaticTest st = new StaticTest();

 

    static

    {

        System.out.println("1");

    }

 

    {

        System.out.println("2");

    }

 

    StaticTest()

    {

        System.out.println("3");

        System.out.println("a="+a+",b="+b);

    }

 

    public static void staticFunction(){

        System.out.println("4");

    }

 

    int a=110;

    static int b =112;

}

問題是:請問輸出是什麼?

相關文章
相關標籤/搜索