類的初始化
在初始化階段,Java虛擬機執行類的初始化語句,為類的靜態變量賦予初始值。
在程序中,靜態變量的初始化有兩種途徑:
1.在靜態變量的聲明處進行初始化;
2.在靜態代碼塊中進行初始化。
沒有經過顯式初始化的靜態變量將原有的值。
一個比較奇怪的例子:
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
|
package com.mengdd.classloader; class Singleton { // private static Singleton mInstance = new Singleton();// 位置1 // 位置1輸出: // counter1: 1 // counter2: 0 public static int counter1; public static int counter2 = 0 ; private static Singleton mInstance = new Singleton(); // 位置2 // 位置2輸出: // counter1: 1 // counter2: 1 private Singleton() { counter1++; counter2++; } public static Singleton getInstantce() { return mInstance; } } public class Test1 { public static void main(String[] args) { Singleton singleton = Singleton.getInstantce(); System.out.println( "counter1: " + Singleton.counter1); System.out.println( "counter2: " + Singleton.counter2); } } |
可見將生成對象的語句放在兩個位置,輸出是不一樣的(相應位置的輸出已在程序注釋中標明)。
這是因為初始化語句是按照順序來執行的。
靜態變量的聲明語句,以及靜態代碼塊都被看做類的初始化語句,Java虛擬機會按照初始化語句在類文件中的先后順序來依次執行它們。
類的初始化步驟
1.假如這個類還沒有被加載和連接,那就先進行加載和連接。
2.假如類存在直接的父類,并且這個父類還沒有被初始化,那就先初始化直接的父類。
3.假如類中存在初始化語句,那就依次執行這些初始化語句。
類的初始化時機
Java程序對類的使用方式可以分為兩種:
1.主動使用
2.被動使用
所有的Java虛擬機實現必須在每個類或接口被Java程序首次主動使用時才初始化它們。
主動使用的六種情況:
1.創建類的實例。
1
2
|
new Test(); |
2.訪問某個類或接口的靜態變量,或者對該靜態變量賦值。
1
2
|
int b = Test.a; Test.a = b; |
3.調用類的靜態方法
1
|
Test.doSomething(); |
4.反射
1
|
Class.forName(“com.mengdd.Test”); |
5.初始化一個類的子類
1
2
3
4
5
6
|
class Parent{ } class Child extends Parent{ public static int a = 3 ; } Child.a = 4 ; |
6.Java虛擬機啟動時被標明為啟動類的類
1
|
java com.mengdd.Test |
除了以上六種情況,其他使用Java類的方式都被看作是對類的被動使用,都不會導致類的初始化。
接口的特殊性
當Java虛擬機初始化一個類時,要求它的所有父類都已經被初始化,但是這條規則并不適用于接口。
在初始化一個類時,并不會先初始化它所實現的接口。
在初始化一個接口時,并不會先初始化它的父接口。
因此,一個父接口并不會因為它的子接口或者實現類的初始化而初始化,只有當程序首次使用特定接口的靜態變量時,才會導致該接口的初始化。
final類型的靜態變量
final類型的靜態變量是編譯時常量還是變量,會影響初始化語句塊的執行。
如果一個靜態變量的值是一個編譯時的常量,就不會對類型進行初始化(類的static塊不執行);
如果一個靜態變量的值是一個非編譯時的常量,即只有運行時會有確定的初始化值,則就會對這個類型進行初始化(類的static塊執行)。
例子代碼:
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
|
package com.mengdd.classloader; import java.util.Random; class FinalTest1 { public static final int x = 6 / 3 ; // 編譯時期已經可知其值為2,是常量 // 類型不需要進行初始化 static { System.out.println( "static block in FinalTest1" ); // 此段語句不會被執行,即無輸出 } } class FinalTest2 { public static final int x = new Random().nextInt( 100 ); // 只有運行時才能得到值 static { System.out.println( "static block in FinalTest2" ); // 會進行類的初始化,即靜態語句塊會執行,有輸出 } } public class InitTest { public static void main(String[] args) { System.out.println( "FinalTest1: " + FinalTest1.x); System.out.println( "FinalTest2: " + FinalTest2.x); } } |
主動使用的歸屬明確性
只有當程序訪問的靜態變量或靜態方法確實在當前類或當前接口中定義時,才可以認為是對類或接口的主動使用。
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
|
package com.mengdd.classloader; class Parent { static int a = 3 ; static { System.out.println( "Parent static block" ); } static void doSomething() { System.out.println( "do something" ); } } class Child extends Parent { static { System.out.println( "Child static block" ); } } public class ParentTest { public static void main(String[] args) { System.out.println( "Child.a: " + Child.a); Child.doSomething(); // Child類的靜態代碼塊沒有執行,說明Child類沒有初始化 // 這是因為主動使用的變量和方法都是定義在Parent類中的 } } |
ClassLoader類
調用ClassLoader類的loadClass()方法加載一個類,并不是對類的主動使用,不會導致類的初始化。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
package com.mengdd.classloader; class CL { static { System.out.println( "static block in CL" ); } } public class ClassLoaderInitTest { public static void main(String[] args) throws Exception { ClassLoader loader = ClassLoader.getSystemClassLoader(); Class<?> clazz = loader.loadClass( "com.mengdd.classloader.CL" ); // loadClass方法加載一個類,并不是對類的主動使用,不會導致類的初始化 System.out.println( "----------------" ); clazz = Class.forName( "com.mengdd.classloader.CL" ); } } |
類加載器的父委托機制
類加載器
類加載器用來把類加載到Java虛擬機中。
類加載器的類型
有兩種類型的類加載器:
1.JVM自帶的加載器:
根類加載器(Bootstrap)
擴展類加載器(Extension)
系統類加載器(System)
2.用戶自定義的類加載器:
java.lang.ClassLoader的子類,用戶可以定制類的加載方式。
JVM自帶的加載器
Java虛擬機自帶了以下幾種加載器。
1.根(Bootstrap)類加載器:
該加載器沒有父加載器。
它負責加載虛擬機的核心類庫,如java.lang.*等。
根類加載器從系統屬性sun.boot.class.path所指定的目錄中加載類庫。
根類加載器的實現依賴于底層操作系統,屬于虛擬機的實現的一部分,它并沒有繼承java.lang.ClassLoader類,它是用C++寫的。
2.擴展(Extension)類加載器:
它的父加載器為根類加載器。
它從java.ext.dirs系統屬性所指定的目錄中加載類庫,或者從JDK的安裝目錄的jre\lib\ext子目錄(擴展目錄)下加載類庫,如果把用戶創建的JAR文件放在這個目錄下,也會自動由擴展類加載器加載。
擴展類加載器是純Java類,是java.lang.ClassLoader類的子類。
3.系統(System)類加載器:
也稱為應用類加載器,它的父加載器為擴展類加載器。
它從環境變量classpath或者系統屬性java.class.path所指定的目錄中加載類,它是用戶自定義的類加載器的默認父加載器。
系統類加載器是純Java類,是java.lang.ClassLoader類的子類。
注意:這里的父加載器概念并不是指類的繼承關系,子加載器不一定繼承了父加載器(其實是組合的關系)。
用戶自定義類加載器
除了以上虛擬機自帶的類加載器以外,用戶還可以定制自己的類加載器(User-defined Class Loader)。
Java提供了抽象類java.lang.ClassLoader,所有用戶自定義的類加載器都應該繼承ClassLoader類。
類加載的父委托機制
從JDK 1.2版本開始,類的加載過程采用父親委托機制,這種機制能更好地保證Java平臺的安全。
在父委托機制中,除了Java虛擬機自帶的根類加載器以外,其余的類加載器都有且只有一個父加載器,各個加載器按照父子關系形成了樹形結構。
當Java程序請求加載器loader1加載Sample類時,loader1首先委托自己的父加載器去加載Sample類,若父加載器能加載,則由父加載器完成加載任務,否則才由loader1本身加載Sample類。
說明具體過程的一個例子:
loader2首先從自己的命名空間中查找Sample類是否已經被加載,如果已經加載,就直接返回代表Sample類的Class對象的引用。
如果Sample類還沒有被加載,loader2首先請求loader1代為加載,loader1再請求系統類加載器代為加載,系統類加載器再請求擴展類加載器代為加載,擴展類加載器再請求根類加載器代為加載。
若根類加載器和擴展類加載器都不能加載,則系統類加載器嘗試加載,若能加載成功,則將Sample類所對應的Class對象的引用返回給loader1,loader1再返回給loader2,從而成功將Sample類加載進虛擬機。
若系統加載器不能加載Sample類,則loader1嘗試加載Sample類,若loader1也不能成功加載,則loader2嘗試加載。
若所有的父加載器及loader2本身都不能加載,則拋出ClassNotFoundException異常。
總結下來就是:
每個加載器都優先嘗試用父類加載,若父類不能加載則自己嘗試加載;若成功則返回Class對象給子類,若失敗則告訴子類讓子類自己加載。所有都失敗則拋出異常。
定義類加載器和初始類加載器
若有一個類加載器能成功加載Sample類,那么這個類加載器被稱為定義類加載器。
所有能成功返回Class對象的引用的類加載器(包括定義類加載器,即包括定義類加載器和它下面的所有子加載器)都被稱為初始類加載器。
假設loader1實際加載了Sample類,則loader1為Sample類的定義類加載器,loader2和loader1為Sample類的初始類加載器。
父子關系
需要指出的是,加載器之間的父子關系實際上指的是加載器對象之間的包裝關系,而不是類之間的繼承關系。
一對父子加載器可能是同一個加載器類的兩個實例,也可能不是。
在子加載器對象中包裝了一個父加載器對象。
例如loader1和loader2都是MyClassLoader類的實例,并且loader2包裝了loader1,loader1是loader2的父加載器。
當生成一個自定義的類加載器實例時,如果沒有指定它的父加載器(ClassLoader構造方法無參數),那么系統類加載器就將成為該類加載器的父加載器。
父委托機制優點
父親委托機制的優點是能夠提高軟件系統的安全性。
因為在此機制下,用戶自定義的類加載器不可能加載應該由父加載器加載的可靠類,從而防止不可靠甚至惡意的代碼代替由父加載器加載的可靠代碼。
例如,java.lang.Object類總是由根類加載器加載,其他任何用戶自定義的類加載器都不可能加載含有惡意代碼的java.lang.Object類。
命名空間
每個類加載器都有自己的命名空間,命名空間由該加載器及所有父加載器所加載的類組成。
在同一個命名空間中,不會出現類的完整名字(包括類的包名)相同的兩個類。
在不同的命名空間中,有可能會出現類的完整名字(包括類的包名)相同的兩個類。
運行時包
由同一類加載器加載的屬于相同包的類組成了運行時包。
決定兩個類是不是屬于同一個運行時包,不僅要看它們的包名是否相同,還要看定義類加載器是否相同。
只有屬于同一運行時包的類才能互相訪問包可見(即默認訪問級別)的類和類成員。
這樣的限制能避免用戶自定義的類冒充核心類庫的類,去訪問核心類庫的包可見成員。
假設用戶自己定義了一個類java.lang.Spy,并由用戶自定義的類加載器加載,由于java.lang.Spy和核心類庫java.lang.*由不同的類加載器加載,它們屬于不同的運行時包,所以java.lang.Spy不能訪問核心類庫java.lang包中的包可見成員。