JVM 加載 class 文件的原理機制(類的生命週期、類加載器)

類的加載、連接與初始化


        
        • 1. 加載:查找並加載類的二進制數據
        • 2. 連接
            – 2.1 驗證:確保被加載的類的正確性
            – 2.2 準備:爲類的靜態變量分配內存,並將其初始化爲默認值 
            – 2.3 解析:把類中的符號引用轉換爲直接引用
        • 3. 初始化:爲類的靜態變量賦予正確的初始

        以下代碼執行結果可以更清楚的理解上面的過程
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
public class Test {
    public static void main(String[] args) {
        Count count = Count.getInstance();
        System.out.println("count1 = " + count.count1);
        System.out.println("count2 = " + count.count2);
    }
}
 
class Count {
    // 這個運行結果是 count1 = 1 count2 = 0 ; 因爲按順序執行1. Count(); 2. count1; 3. count2;
    private static Count count = new Count();
    public static int count1;
    public static int count2 = 0;
    // 所以這個運行結果是 count1 = 1 count2 = 1 ;
    // private static Count count = new Count();
 
    private Count() {
        count1++;
        count2++;
    }
 
    public static Count getInstance() {
        return count;
    }
}

        下面分別對上面幾個步驟進行深入的分析。

1. 類的加載


        類的加載指的是將類的 .class 文件中的二進制數據讀入到內存中,將其放在運行時數據區的方法區內,然後在堆區創建一個 java.lang.Class 對象,用來封裝類在方法區內的數據結構。類的加載的最終產品是位於堆區中的 Class 對象,Class 對象封裝了類在方法區內的數據結構 ,並且向 Java 程序員提供了訪問方法區內的數據結構的接口。
        

        類加載器並不需要等到某個類被“首次主動使用”時再加載它,JVM 規範允許類加載器在預料某個類將要被使用時就預先加載它,如果在預先加載的過程中遇到了.class 文件缺失或存在錯誤,類加載器必須在程序首次主動使用該類時才報告錯誤(LinkageError錯誤),如果這個類一直沒有被程序主動使用,那類加載器就不會報告錯誤。

        1.1 兩種類型的類加載器 (ClassLoader)

        1.1.1 Java虛擬機自帶的加載器

            • 根類加載器( Bootstrap,使用 c++ 編寫,無法在 Java 代碼中得到該類)
            • 擴展類加載器( Extension,使用 Java 實現)
            • 系統類加載器( System,應用加載器,使用Java代碼實現)

        1.1.2 用戶自定義的類加載器

            • java.lang.ClassLoader 的子類
            • 用戶可以定製類的加載方式
      
        類被加載後,就進入連接階段。連接就是將已經讀入到內存的類的二進制數據合併到虛擬機的運行時環境中去。

2. 連接


2.1 類的驗證


類的驗證主要包括以下內容

1. 類文件的結構檢査:

        確保類文件遵從 Java 類文件的固定格式。

2. 語義檢查:

        確保類本身符合 Java 語言的語法規定,比如驗證 final 類型的類沒有子類,以及 final 類型的方法沒有被覆蓋。

3. 子節碼驗證:

        確保字節碼流可以被 Java 虛擬機安全地執行。字節碼流代表 Java 方法(包括靜態方法和實例方法),它是由被稱做操作碼的單字節指令組成的序列,每一個操作碼後都跟着一個或多個操作數。字節碼驗證步驟會檢查每個操作碼是否合法,即是否有着合法的操作數。

4. 二進制兼容的驗證:

        確保相互引用的類之間協調一致。例如在 Worker 類的 gotoWork() 方法中會調用 Car 類的 run() 方法。Java 虛擬機在驗證 Worker 類時, 會檢查在方法區內是否存在 Car 類的 run() 方法,假如不存在(當 Worker 類和 Car 類的版本不兼容,就會出現這種問題,參考:JAVA類文件版本(class version)與JDK對應關係),就會拋出NoSuchMethodError 錯誤。

2.2 類的準備


        在準備階段,JVM 爲類的靜態變最分配內存,並設置默認的初始值。例如對於以下 Sample 類,在準備階段,將爲 int 類型的靜態變量 a 分配4個字節的內存空間,並且陚默認值0,爲 long 類型的靜態變最 b 分配8個字節的內存空間,並且陚予默認值0。
1
2
3
4
5
6
7
8
9
public class Sample {
    private static int a = 1;
    private static long b;
 
    static {
        b = 2;
    }
    // ...
}


2.3 類的解析


        在解析階段,JVM 會把類的二進制數據中的符號引用替換爲直接引用。例如在 Worker 類的gotoWork() 方法中會引用 Car 類的run() 方法。
1
2
3
public void gotoWord(){
    car.run(); //這段代碼在Worker類的二進制數據中表示爲符號引用
}

        在 Worker 類的二進制數據中,包含了一個對 Car 類的 run() 方法的符號引用,它由 run() 方法的全名和相關描述符組成。在解析階段,JVM 會把這個符號引用替換爲一個指針,該指針指向 Car 類的 run() 方法在方法區內的內存位置,這個指針就是直接引用

3. 類的初始化


        在初始化階段,JVM 執行類的初始化語句,爲類的靜態變最賦予初始值。在程序中,靜態變量的初始化有兩種途徑:
        (1)在靜態變量的聲明處進行初始化;
        (2)在靜態代碼塊中進行初始化。
        例如在以下代碼中,靜態變最 a 和 b 都被顯式初始化, 而靜態變最 c 沒有被顯式初始化,它將保持默認值0。
1
2
3
4
5
6
7
8
9
10
public class Sample {
    private static int a = 1;
    private static long b;
    private static long c;
 
    static {
        b = 2;
    }
    // ...
}
   
        靜態變量的聲明語句,以及靜態代碼塊都被看做類的初始化語句,JVM 會按照初始化語句在類文件中的先後順序來依次執行它們。例如當以下 Sample 類被初始化後,它的靜態變最 a 的取值爲4。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Sample {
    private static int a = 1;
 
    static {
        a = 2;
    }
 
    static {
        a = 4;
    }
 
    public static void main(String[] args) {
        System.out.println(a); // 輸出4
    }
}

類的初始化步驟

        (1) 假如這個類還沒有被加載和連接,那就先進行加載和連接。
        (2) 假如類存在直接的父類,並且這個父類還沒有被初始化,那就先初始化直接的父類(接口除外,下面有詳細介紹)。
        (3) 假如類中存在初始化語句,那就依次執行這些初始化語句。

3. 1 類的初始化時機


Java 程序對類的使用方式可分爲兩種

        – 主動使用
        – 被動使用

        所有的 JVM 實現必須在每個類或接口被 Java 程序“首次主動使用”時才初始化它們。

主動使用的情況(六種)
 
        – 創建類的實例
        – 訪問某個類或接口的靜態變量,或者對該靜態變量賦值 
        – 調用類的靜態方法 
        – 反射(如 Class.forName(“com.demo.Test”) ) 
        – 初始化一個類的子類
        –  JVM 啓動時被標明爲啓動類的類( Java  Test)

        除了以上六種情況,其他使用 Java 類的方式都被看作是對類的被動使用都不會導致類的初始化。

        以下代碼可以加深理解:
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
public class Test {
    public static void main(String[] args) {
        // x是一個編譯時的常量,編譯的時候就知道值是多少,不需要對類進行初始化
        // 如果 final 去除掉以後,就會執行“FinalTest staic block!”了
        System.out.println(FinalTest.x);
        // x非編譯時的常量,x在編譯時不知道是多少,
        // 運行才知道的就需要對類進行初始化,對類進行初始化static代碼快就會執行
        System.out.println(FinalTest2.x);
    }
}
 
class FinalTest {
    public static final int x = 6 3;
 
    static {
        System.out.println("FinalTest staic block!");
    }
}
 
class FinalTest2 {
    public static final int x = new Random().nextInt(100);
 
    static {
        System.out.println("FinalTest2 staic block!");
    }
}
運行結果:    
2
FinalTest2 staic block!
50

當 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
public class Test {
    static {
        System.out.println("Test static block!");
    }
 
    public static void main(String[] args) {
        System.out.println(Child.b);
    }
}
 
class Parent {
    static int a = 3;
 
    static {
        System.out.println("Parent static block!");
    }
}
 
class Child extends Parent {
    static int b = 4;
 
    static {
        System.out.println("Child static block!");
    }
}
運行結果:
Test static block!
Parent static block!
Child static block!
4

只有當程序訪問的靜態變量或靜態方法確實在當前類或當前接口中定義時,纔可以認爲是對類或接口的主動使用
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
public class Test {
    public static void main(String[] args) {
        System.out.println(Child.a);
        Child.doSomething();
    }
}
 
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!");
    }
}
運行結果:(不在當前類定義只在父類定義,參考以上 六種主動使用的情況)
Parent static block!
3
do something!

另外,調用 ClassLoader 類的 loadClass 方法加載一個類,並不是對類的主動使用,不會導致類的初始化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Test {
    public static void main(String[] args) throws ClassNotFoundException {
        // 獲取系統類加載器
        ClassLoader loader = ClassLoader.getSystemClassLoader();
        // 這行代碼沒有導致任何輸出 不會導致類的初始化
        Class<?> clazz = loader.loadClass("CL");
        System.out.println("------");
        clazz = Class.forName("CL");
    }
}
 
class CL {
    static {
        System.out.println("Class CL");
    }
}
運行結果:
------
Class CL

類加載器


        類加載器用來把類加載到 JVM 中。從 JDK 1.2 版本開始,類的加載過程採用父親委託機制,這種機制能更好地保證 Java 平臺的安全。在此委託機制中,除了 JVM 自帶的根類加載器以外,其餘的類加載器都有且只有一個父加載器。當 Java 程序請求加載器 loader1 加載 Sample 類時,loader1 首先委託自己的父加載器去加載 Sample 類,若父加載器能加載,則由父加載器完成加載任務,否則才由加載器 loader1 本身加載 Sample 類。

        JVM 自帶的類加載器之間的關係
        

根(Bootstrap)類加載器

        該加載器沒有父加載器。它負責加載虛擬機的核心類庫,如 java.lang.*  等。例如從下面代碼可以看出,java.lang.Object 就是由根類加載器加載的。根類加載器從系統屬性 sun.boot.class.path 所指定的目錄中加載類庫。根類加載器的實現依賴於底層操作系統,屬於虛擬機的實現的一部分,它並沒有繼承 java.lang.ClassLoader 類。

擴展(Extension)類加載器
 
       它的父加載器爲根類加載器。它從 java.ext.dirs 系統屬性所指定的目錄中加載類庫,或者從 JDK 的安裝目錄的 jre\lib\ext 子目錄(擴展目錄)下加載類庫,如果把用戶創建的 JAR 文件放在這個目錄下,也會自動由擴展類加載器加載。擴展類加載器是純 Java 類,是 java.lang.ClassLoader 類的子類。

系統(System)類加載器

        也稱爲應用類加載器,它的父加載器爲擴展類加載器。它從環境變量 classpath 或者系統屬性 java.class.path 所指定的目錄中加載類,它是用戶自定義的類加載器的默認父加載器。系統類加載器是純 Java 類,是 java.lang.ClassLoader 類的子類。
        
用戶自定義的類加載器

        除了以上虛擬機自帶的加載器以外,用戶還可以定製自己的類加載器(User-defined Class Loader)。Java 提供了抽象類 javaJang.ClassLoader,所有用戶自定義的類加載器應該繼承 ClassLoader 類。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Test {
    public static void main(String[] args) throws ClassNotFoundException {
        // String 是由根類加載器加載的,下面打印結果爲null
        Class<?> clazz = Class.forName("java.lang.String");
        System.out.println(clazz.getClassLoader());
 
        // 應用加載器加載的
        Class<?> clazz2 = Class.forName("C");
        System.out.println(clazz2.getClassLoader());
 
 
    }
}
 
class C {
}
打印結果:
null
sun.misc.Launcher$AppClassLoader@42a57993

類加載的父委託機制


        在父親婁託機制中,各個加載器按照父子關係形成樹形結構,除了根類加載器以外,其餘的類加載器都有且只有一個父加載器。
        

        loader2 首先從自己的命名空間中查找 Sample 類是否己經被加載,如果己經加載,就直接返回代表 Sample 類的 Class 對象的引用。如果 Sample 類還沒有被加載,loader2 首先請求 loader1 代爲加載,loader1 再請求系統類加載器代爲加載,系統類加載器再請求擴展類加載器代爲加載,擴展類加載器再請求根類加載器代爲加載。若根類加載器和擴展類加載器都不能加載,則系統類加載器嘗試加載,若能加載成功,則將 Sample 類所對應的 Class 對象的引用返回給 loader1,loader1 再將引用返回給 loader2,從而成功將 Sample 類加載進虛擬機。若系統類加載器不能加載 Sample 類,則 loader1 嘗試加載 Sample 類(上圖所示),若 loader1 也不能成功加載,則 loader2 嘗試加載。若所有的父加載器及 loader2 本身都不能加載,則拋出 ClassNotFoundException 異常。

        需要指出的是,加載器之間的父子關係實際上指的是加載器對象之間的包裝關係,而不是類之間的繼承關係。一對父子加載器可能是同一個加載器類的兩個實例,也可能不是。在子加載器對象中包裝了一個父加載器對象。例如以下 loader1 和 loader2 都是 MyClassLoader 類的實例,並 loader2 包裝了 loader1, loader1 是 loader2 的父加載器。
1
2
3
4
ClassLoader loader1 = new MyClassLoader();
 
// 參數loader1將作爲loader2的父加載器
ClassLoader loader2 = new MyClassLoader(loader1);

        當生成一個自定義的類加載器實例時,如果沒有指定它的父加載器,那麼系統類加載器就將成爲該類加載器的父加載器。

        父親委託機制的優點是能夠提高軟件系統的安全性,因爲在此機制下,用戶自定義的類加載器不可能加載應該由父加載器加載的可靠類,從而防止不可靠甚至惡意的代碼代替由父加載器加載的可靠代碼。例如,java.lang.Object 類總是由根類加載器加載,其他任何用戶自定義的類加載器都不可能加載含有惡意代碼的 java.lang.Object 類。

        定義類加載器:如果某個類加載器能夠加載一個類,那麼該類加載器就稱作定義類加載器;
        初始類加載器:定義類加載器及其所有子加載器都稱作初始類加載器;

命名空間


        每個類加載器都有自己的命名空間,命名空間由該加載器及所有父加載器所加載的類組成。在同一個命名空間中,不會出現類的完整名字(包括類的包名)相同的兩個類;在不同的命名空間中,有可能會出現類的完整名字(包括類的包名)相同的兩個類。

        不會出現完整名字一樣的原因是在同一個命名空間,只會被類加載器加載一次。不同命名空間就會被各自不同命名空間的類加載器分別加載。

運行時包(package


        由同一類加載器加載的屬於相同包的類組成了運行時包。決定兩個類是不是屬於同一個運行時包,不僅要看它們的包名是否相同,還要看定義類加載器是否相同。只有屬於同一運行時包的類才能互相訪問包可見(即默認訪問級別)的類和類成員。這樣的限制能避免用戶自定義的類冒充核心類庫的類,去訪問核心類庫的包可見成員。 假設用戶自己定義了一個類java.lang.Spy,並由用戶自定義的類加載器加載,由於 java.lang.Spy和核心類庫java.lang.*由不同的加載器加載,它們屬於不同的運行時包,所以java.lang.Spy不能訪問核心類庫java.lang包中的包可見成員。


創建用戶自定義的類加載器 


        要創建用戶自己的類加載器,只需要擴展 java.lang.ClassLoader 類,然後覆蓋它的 findClass(String name) 方法即可,該方法根據參數指定的類的名字,返冋對應的 Class 對象的引用。

        自定義類加載器的結構圖
        

        新建三個類,MyClassLoader、Dog、Sample,不能有包名
        新建四個文件夾D:\myapp\otherlib、D:\myapp\serverlib、D:\myapp\clientlib、D:\myapp\syslib
代碼:
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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
public class MyClassLoader extends ClassLoader {
 
    // 類加載器名字
    private String name;
    // 加載類的路徑
    private String path = "d:\\";
    // class文件的擴展名
    private final String fileType = ".class";
 
    public MyClassLoader(String name) {
        super();// 讓系統類加載器成爲該類加載器的父加載器
        this.name = name;
    }
 
    public MyClassLoader(ClassLoader parent, String name) {
        super(parent); // 顯示指定該類加載器的父加載器
        this.name = name;
    }
 
    public String toString() {
        return this.name;
    }
 
    public String getPath() {
        return path;
    }
 
    public void setPath(String path) {
        this.path = path;
    }
 
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] data = this.loadClassData(name);
        return this.defineClass(name, data, 0, data.length);
    }
 
    private byte[] loadClassData(String name) {
        InputStream is = null;
        byte[] data = null;
        ByteArrayOutputStream baos = null;
 
        try {
            this.name = this.name.replace(".""\\");
            is = new FileInputStream(new File(path + name + fileType));
            baos = new ByteArrayOutputStream();
 
            int ch = 0;
            while (-1 != (ch = is.read())) {
                baos.write(ch);
            }
 
            data = baos.toByteArray();
        catch (Exception e) {
            e.printStackTrace();
        finally {
            try {
                baos.close();
                is.close();
            catch (IOException e) {
                e.printStackTrace();
            }
        }
 
        return data;
    }
 
    public static void main(String[] args) throws Exception {
 
        // 父加載器爲系統類加載器
        MyClassLoader loader1 = new MyClassLoader("loader1");
        loader1.setPath("D:\\test\\serverlib\\");
  
        // 指定loader2的父加載器爲loader1
        MyClassLoader loader2 = new MyClassLoader(loader1, "loader2");
        loader2.setPath("D:\\test\\clientlib\\");
 
        // 指定loader3的父加載器爲根加載器
        MyClassLoader loader3 = new MyClassLoader(null"loader3");
        loader3.setPath("D:\\test\\otherlib\\");
 
        test(loader2);
        test(loader3);
    }
 
    public static void test(ClassLoader loader) throws Exception {
        Class clazz = loader.loadClass("Sample");
        Object object = clazz.newInstance();
    }
 
}

1
2
3
4
5
6
public class Dog {
 
    public Dog() {
        System.out.println("Dog is load by : " this.getClass().getClassLoader());
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
public class Sample {
 
    public int v1 = 1;
 
    public Sample() {
 
        System.out.println("Sample is load by : " this.getClass().getClassLoader());
 
        // 主動使用Dog
        new Dog();
    }
}

測試例子:
將生成的.class文件拷貝出來放置如下位置:
syslib放自己定義的加載器MyClassLoader.class
情況1: Sample.class和Dog.class拷貝到serverlib和otherlib下,執行結果
1
2
3
4
5
D:\myapp\syslib>java MyClassLoader
Sample is load by : loader1
Dog is load by : loader1
Sample is load by : loader3
Dog is load by : loader3
Sample,由loader1加載到。

情況2. 把serverlib刪掉,otherlib不刪除,loader2最底層的,找不到,所以提示找不到類文件。

情況3. 都放到syslib下面(otherlib不變),由系統類加載器加載,加載當前./目錄:系統加載器加載classpath
1
2
3
4
5
D:\myapp\syslib>java MyClassLoader
Sample is load by : sun.misc.Launcher$AppClassLoader@659e0bfd
Dog is load by : sun.misc.Launcher$AppClassLoader@659e0bfd
Sample is load by : loader3
Dog is load by : loader3

情況4:刪除syslib下面的Sample和Dog,拷貝到serverlib將serverlib設置爲classpath
1
2
3
4
5
D:\myapp\syslib>java -cp .;d:\myapp\serverlib MyClassLoader
Sample is load by : sun.misc.Launcher$AppClassLoader@659e0bfd
Dog is load by : sun.misc.Launcher$AppClassLoader@659e0bfd
Sample is load by : loader3
Dog is load by : loader3

        這裏需要注意:Sample 被加載兩次是因爲他們是不同的類加載器加載的。在不同的命名空間,如下圖
        
        在 loader1 和 loader3 各自的命名空間中都存在 Sample 類和 Dog 類。

        在 Sample 類中主動使用了 Dog 類,當執行 Sample 類的構造方法中的 new Dog() 語句時,JVM 需要先加載 Dog 類,到底用哪個類加載器加載呢?從情況1的打印結果可以看出,加載 Sample 類的 loader1 還加載 Dog 類,JVM 會用 Sample 類的定義類加載器去加載 Dog 類,加載過程也同樣採用父親委託機制。爲了驗證這一點,可以把 D:\myapp\serverlib 目錄下的 Dog.class 文件刪除,然後在 D:\myapp\syslib 目錄下存放一個Dog.class文件,此時程序的打印結果爲:
1
2
3
4
5
D:\myapp\syslib>java MyClassLoader
Sample is load by : loader1
Dog is load by : sun.misc.Launcher$AppClassLoader@659e0bfd
Sample is load by : loader3
Dog is load by : loader3

        由此可見,當由 loader1 加載的 Sample 類首次主動使用 Dog 類時,Dog 類由系統類加載器加載。如果把 D:\myapp\serverlib 和 D:\myapp\syslib 目錄下的 Dog.class 文件都刪除,然後在 D:\myapp\clientlib 目採下存放一個 Dog.class 文件,此時的目錄結構如下圖,當由 loader1 加載的 Sample 類首次主動使用 Dog 類時,由於 loader1 及它的父加載器都無法加載 Dog 類,因此 test(loader2) 方法會拋出 ClassNotFoundException。
        


不同類加載器的命名空間關係

        同一個命名空間內的類是相互可見的。

        子加載器命名空間包含所有父加載器的命名空間。因此由子加載器加載的類能看見父加載器加載的類。例如系統類加載器加載的類能看見根類加載器加載的類。由父加載器加載的類不能看見子加載器加載的類。

        如果兩個加載器之間沒有直接或間接的父子關係,那麼它們各自加載的類相互不可見。

        修改 MyClassLoader 類的 main 方法
1
2
3
4
5
6
7
8
9
10
11
public static void main(String[] args) throws Exception {
 
    // 父加載器爲系統類加載器
    MyClassLoader loader1 = new MyClassLoader("loader1");
    loader1.setPath("D:\\myapp\\serverlib\\");
     
    Class clazz = loader1.loadClass("Sample");
    Object object = clazz.newInstance(); // 創建對象
    Sample sample = (Sample)object;
    System.out.println(sample.v1);
}
        把 Sample.class 和 Dog.class 僅僅拷貝到 D:\myapp\serverlib 下
1
2
3
4
5
6
7
8
9
10
11
D:\myapp\syslib>java MyClassLoader
Sample is load by : loader1
Dog is load by : loader1
Exception in thread "main" java.lang.NoClassDefFoundError: Sample
        at MyClassLoader.main(MyClassLoader.java:110)
Caused by: java.lang.ClassNotFoundException: Sample
        at java.net.URLClassLoader.findClass(Unknown Source)
        at java.lang.ClassLoader.loadClass(Unknown Source)
        at sun.misc.Launcher$AppClassLoader.loadClass(Unknown Source)
        at java.lang.ClassLoader.loadClass(Unknown Source)
        ... 1 more

        MyclassLoader 類由系統類加載器加載,而 Sample 類由 loader1 類加載,因此 MyClassLoader 看不見  Sample 類。在 MyCIassLoader 類的 main() 方法中使用 Sample 類,會導致 NoClassDefFoundError 錯誤。

        如果把 D:\myapp\serverlib 目錄下的 Sample.class 和 Dog.class 刪除,再把這兩個文件拷貝到 D:\myapp\syslib 目錄下,然後運行 main() 方法,也能正常運行。 此時 MyClassLoader 類和 Sample 類都由系統類加載器加載,由於它們位於同一個命名空間內,因此相互可見。

        當兩個不同命名空間內的類相互不可見時,可採用 Java 反射機制來訪問對方實例的屬性和方法。如果把 MyClassLoader 類的 main() 方法替換爲如下代碼:
1
2
3
4
5
6
7
8
9
10
11
12
public static void main(String[] args) throws Exception {
 
    // 父加載器爲系統類加載器
    MyClassLoader loader1 = new MyClassLoader("loader1");
    loader1.setPath("D:\\myapp\\serverlib\\");
     
    Class clazz = loader1.loadClass("Sample");
    Object object = clazz.newInstance(); // 創建對象
    Field field = clazz.getField("v1");
    int v1 = field.getInt(object);
    System.out.println("v1:" + v1);
}
        運行結果:
1
2
3
4
D:\myapp\syslib>java MyClassLoader
Sample is load by : loader1
Dog is load by : loader1
v1:1

類的卸載


        當 Sample 類被加載、連接和初始化後,它的生命週期就開始了。當代表 Sample 類的 Class 對象不再被引用,即不可觸及時,Class 對象就會結束生命週期,Sample 類在方法區內的數據也會被卸載,從而結束 Sample 類的生命週期。由此可見,一個類何時結束生命週期,取決於代表它的 Class 對象何時結束生命週期。

        由 JVM 自帶的類加載器所加載的類,在虛擬機的生命週期中,始終不會被卸載。前面己經介紹過,JVM 自帶的類加載器包括根類加載器、擴展類加載器和系統類加載器。JVM 本身會始終引用這些類加載器,而這些類加錢器則會始終引用它們所加載的類的 Class 對象,因此這些 Class 對象始終是可觸及的。

        由用戶自定義的類加載器所加載的類是可以被卸載的。

        實驗:把 Sample.class 和 Dog.class 拷貝到 serverlib 下,修改 MyClassLoader 的 main 方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static void main(String[] args) throws Exception {
 
    // 父加載器爲系統類加載器
    MyClassLoader loader1 = new MyClassLoader("loader1");                 //1
    loader1.setPath("D:\\myapp\\serverlib\\");                            //2
     
    Class objClass = loader1.loadClass("Sample");                         //3
    System.out.println("objClass's hashCode is " + objClass.hashCode());  //4
    Object obj = objClass.newInstance(); // 創建對象                       //5
     
    loader1 = null;                                                       //6
    objClass = null;                                                      //7
    obj = null;                                                           //8
     
    loader1 = new MyClassLoader("loader1");                               //9
    loader1.setPath("D:\\myapp\\serverlib\\");
    objClass = loader1.loadClass("Sample");                               //10
    System.out.println("objClass's hashCode is " + objClass.hashCode());  //11
}

        運行結果:
1
2
3
4
5
D:\myapp\syslib>java MyClassLoader
objClass's hashCode is 1311053135
Sample is load by : loader1
Dog is load by : loader1
objClass's hashCode is 865113938

        從以上打印結果可以看出,程序兩次打印 objClass 變量引用的 Class 對象的哈希碼, 得到的數值不同。因此 objClass 變最兩次引用不同的 Class 對象,可見在 JVM 的生命週期中,對 Sample 類先後加載了兩次。

        運行以上程序時,Sample 類由 loader1 加載,在類加載器的內部實現中,用一個 Java 集合來存放所加載類的引用。另一方面,一個 Class 對象總是會引用它的類加載器,調用 Class 對象的 getClassLoader() 方法,就能獲得它的類加載器。由此可見,代表 Sample 類的 Class 實例與 loader1 之間爲雙向關聯關係。

        —個類的實例總是引用代表這個類的 Class 對象,在 Object 類中定義了 getClass() 方法,這個方法返回代表對象所屬類的 Class 對象的引用。此外,所有的 Java 類都有—個靜態屬性 class,它引用代表這個類的 Class 對象。

        當程序執行第5步時,引用變量與對象之間的引用關係如圖
        

        從上圖可以看出,loader1 變量和 obj 變量間接引用代表 Sample 類的 Class 對象, 而 objClass 變量則直接引用它。
 
       當程序執行完第8步時,所有的引用變量都置爲 null,此時 Sample 對象結束生命週期,MyClassLoader 對象結束生命週期,代表 Sample 類的 Class 對象也結束生命周 期,Sample 類在方法區內的二進制數據被卸載。

        當程序執行完第10步時,Sample 類又重新被加載,在 JVM 的堆區會生成一個新的代表 Sample 類的 Class 實例。

在如下幾種情況下,JVM 將結束生命週期

    – 執行了System.exit()方法
    – 程序正常執行結束
    – 程序在執行過程中遇到了異常或錯誤而異常終止 
    – 由於操作系統出現錯誤而導致Java虛擬機進程終止


參考:
        視頻:鏈接:http://pan.baidu.com/s/1cIBS8A 密碼:s5sh
        pdf:鏈接:http://pan.baidu.com/s/1geTbRMz 密碼:hiwj 【深入Java虛擬機視頻教程課件.pdf
        《深入理解Java虛擬機 JVM高級特性與最佳實踐
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章