1. 程式人生 > >類的加載機制(四)

類的加載機制(四)

用戶 str forname 這一 ali initial 樹形 互訪問 重要

這一章我們主要是對雙親委派機制進行詳細講解:

前面我們知道類加載有系統自帶的3種加載器,也有自定義的加載器,那麽這些加載器之間的關系是什麽,已經在加載類的時候,誰去加載呢?這節,我們將進行講解。

一、雙親委派機制

技術分享圖片

JVM的ClassLoader采用的是樹形結構,除了BootstrapClassLoader以外?每個ClassLoader都會有一個parentClassLoader,用戶自定義的ClassLoader默認的parentClassLoader是SystemClassLoader,當然你可以自己指定需要用哪一個ClassLoader的實例,我們來看他的API:

技術分享圖片

默認的無參構造方法使用的是SystemClassLoader,你可以通過傳入一個ClassLoader的實例來指定他的父類加載器。這裏強調一點,很多人認為各個父子類加載器之間是繼承關系,這裏澄清一下,父子類加載器之間是組合關系,子類類加載器會含有一個parentClassLoader的對象,類加載的時候通常會按照樹形結構的原則來進行,也就是說,首先是從parentClassLoader中嘗試進行加載,當parent無法進行加載時,再從當前的類加載器進行加載,以此類推。JVM會保證一個類在同一個ClassLoader中只會被加載一次。
ClassLoader抽象類為我們定義了一系列的關鍵的方法,下來讓我們來看一下
1、loadClass方法

此方法用來加載指定名字的類,ClassLoader會先從已加載的類中尋找,如果沒有,則使用父加載器進行加載,如果加載成功則加載,否則從當前的類加載器中進行加載,如果還沒有找到該類的class文件則會拋出異常ClassNotFoundException

技術分享圖片

如果該類需要鏈接,則通過resolveClass進行鏈接。

2、defineClass,此方法用來將二進制的字節碼轉換為Class對象,這個對類的自定義加載非常重要,當然前文我們已經說了,當類的二進制文件被加載到內存之後,要進行語法分析,語義分析等一系列的驗證,如果不符合JVM規範,則拋出ClassFormateError錯誤,如果生成的類名和字節碼中的不一致,則拋出NoClassDefFoundException,如果加載的class是受保護的、采用不同的標簽名的,或者一java.*開頭的,則拋出SecurityException,如果要加載的class在之前已經被加載過,則直接拋出LinkageError。

技術分享圖片

3、resolveClass,此方法完成Class的鏈接,如果鏈接過則直接返回。當Java開發人員調用Class.forName來獲取一個class對象的時候,JVM會從方法棧上尋找第一個ClassLoader,通常也就是執行Class.forName的ClassLoader,並使用這個ClassLoader來加載此類。JVM為了保護加載、執行的類的安全,不允許ClassLoader直接卸載加載了的類,只有JVM才可以卸載,在SUN的JDK中,只有ClassLoader沒有 被引用的時候,次ClassLoader加載的類才會被卸載!

技術分享圖片

附:JDK中ClassLoader的部分源碼

1、 構造函數

技術分享圖片
protected ClassLoader(ClassLoader parent) {
    SecurityManager security = System.getSecurityManager();
    if (security != null) {
        security.checkCreateClassLoader();
    }
    this.parent = parent;
    initialized = true;
}
    protected ClassLoader() {
    SecurityManager security = System.getSecurityManager();
    if (security != null) {
        security.checkCreateClassLoader();
    }
    this.parent = getSystemClassLoader();
    initialized = true;
    }
技術分享圖片

2loadClass

技術分享圖片
public Class<?> loadClass(String name) throws ClassNotFoundException {
    return loadClass(name, false);
    }
    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;
}
技術分享圖片

類的這種加載機制我們稱之為父委托加載機制,父委托機制的優點就是能夠提高軟件系統的安全性。因為在此機制下,用戶自定義的類加載器不可能加載本應該由父加載器加載的可靠類,從而防止不可靠的甚至惡意的代碼代替由父類加載器加載的可靠代碼。如,java.lang.Object類總是由根類加載器加載的,其他任何用戶自定義的類加載器都不可能加載含有惡意代碼的java.lang.Object類。
被定義的類加載器,而它的父類加載器則被稱為初始類加載器。

我們知道java中很可能出現類名相同的類,但是JVM卻能正常的加載,是因為我們將相同的類名的類放在了不通的包(package)下面,這個也成為命名空間,每個類加載器都有自己的命名空間,命名空間是由該加載器以及所有父加載器所加載的類組成。在同一個命名空間中,不會出現類的完整名字(包名+類名)相同的兩個類;在不同的命名空間中,有可能出現類的完整名字相同的兩個類。

由同一類加載器加載的屬於相同包的類組成了運行時包。決定兩個類是不是屬於同一個運行時包,不僅要看他們的包名稱是否相同,還要看定義類加載器是否相同。只有屬於同一運行時包的類之間才能相互訪問可見(默認訪問級別)的類和成員。假設用戶自定義了一個類java.lang.TestCase並由用於自定義的類加載器加載,由於java.lang.TestCase和核心類庫java.lang.*由不同的類加載器加載,他們屬於不同的運行時包,所以java.lang.TestCase不能訪問核心庫java.lang包中的包可見成員。

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

子類加載器的命名空間包含所有父類加載器的命名空間,因此由子類加載器加載的類能看見父類加載器加載的類,相反,由父類加載器加載的類不能看見子類加載器加載的類。如果兩個加載器之間沒有直接或者間接的父子關系,那麽他們各自加載的類互不可見。

二、自定義類加載器

首先類的雙親委派流程為:

技術分享圖片

首先,我們定義一個待加載的普通Java類:Test.java。放在com.pony.cl包下:

技術分享圖片
package com.pony.cl;

public class Test {
    public void hello() {
        System.out.println("恩,是的,我是由 " + getClass().getClassLoader().getClass()
                + " 加載進來的");
    }
}
技術分享圖片

註意:

如果你是直接在當前項目裏面創建,待Test.java編譯後,請把Test.class文件拷貝走,再將Test.java刪除。因為如果Test.class存放在當前項目中,根據雙親委派模型可知,會通過sun.misc.Launcher$AppClassLoader 類加載器加載。為了讓我們自定義的類加載器加載,我們把Test.class文件放入到其他目錄。

接下來就是自定義我們的類加載器:

技術分享圖片
import java.io.FileInputStream;
import java.lang.reflect.Method;

public class Main {
    static class MyClassLoader extends ClassLoader {
        private String classPath;

        public MyClassLoader(String classPath) {
            this.classPath = classPath;
        }

        private byte[] loadByte(String name) throws Exception {
            name = name.replaceAll("\\.", "/");
            FileInputStream fis = new FileInputStream(classPath + "/" + name
                    + ".class");
            int len = fis.available();
            byte[] data = new byte[len];
            fis.read(data);
            fis.close();
            return data;

        }

        protected Class<?> findClass(String name) throws ClassNotFoundException {
            try {
                byte[] data = loadByte(name);
                return defineClass(name, data, 0, data.length);
            } catch (Exception e) {
                e.printStackTrace();
                throw new ClassNotFoundException();
            }
        }

    };

    public static void main(String args[]) throws Exception {
        MyClassLoader classLoader = new MyClassLoader("D:/test");
        Class clazz = classLoader.loadClass("com.pony.cl.Test");
        Object obj = clazz.newInstance();
        Method helloMethod = clazz.getDeclaredMethod("hello", null);
        helloMethod.invoke(obj, null);
    }
}
技術分享圖片

註意點:

Object obj = clazz.newInstance();
不能寫成:
Test obj = (Test)clazz.newInstance();

如果寫成這樣會報錯,因為當前的這個類是由系統加載器加載,而Test是由自定義加載器加載,那麽系統類加載和自定義類的加載器不屬於同一個運行時包,這個時候是沒有辦法直接轉換的,只能通過反射的方式去訪問,反射是唯一一種可以跨越在不同運行時包的方法。

類的加載機制(四)