1. 程式人生 > 實用技巧 >類載入器ClassLoader

類載入器ClassLoader

上篇文章說到,Class類可以通過一個類的全限定名去載入類,那麼底層是如何去載入的呢?這就是我們今天要聊的類載入器ClassLoader,其可以通過一個類的全限定名來獲取描述此類的二進位制位元組流,也即是將編譯過後的Class檔案載入到記憶體中。

需要注意的是,即使是同一個類,類載入器不一樣,就必定不相等。

例如自定義了一個類載入器跟JVM預設載入器進行比對

/**
 *自定義類載入器
 */
class MyClassLoader extends ClassLoader {
    //類載入需要用到包名
    String packageName;

    public MyClassLoader(String packageName) throws ClassNotFoundException {
        
this.packageName = packageName; } @Override public Class<?> findClass(String name) throws ClassNotFoundException { String filename = name + ".class"; name = packageName+"."+name.substring(name.lastIndexOf("/")+1); InputStream is = null; try {
is = new FileInputStream(new File(filename)); } catch (FileNotFoundException e) { e.printStackTrace(); } if (is == null) { return super.findClass(name); } try { byte[] bytes = new byte[is.available()]; is.read(bytes);
//根據class對應的二進位制檔案,呼叫defineClass return defineClass(name,bytes,0,bytes.length); } catch (IOException e) { e.printStackTrace(); return null; } } }

//引數是包名,類載入需要用到包名 
MyClassLoader myClassLoader = new MyClassLoader("com.liusy.lang");
//引數是java檔案編譯過後的全路徑
Object obj = myClassLoader.loadClass("H:/Code/IDEACODE/java_source_code/out/production/java_source_code/com/liusy/lang/ClassSource");
System.out.println(obj);System.out.println(obj instanceof ClassSource);

上述程式碼執行結果如下

Java的3種類載入器

1、Bootstrap ClassLoader,頂級載入器。

啟動類載入器,載入$JAVA_HOME$/jre/lib下的核心類庫,也是所有載入器的頂級父類,由c++所寫。也可以用JVM引數-Xbootclasspath指定其載入的目錄。

//檢視其載入的jar包資訊
Launcher.getBootstrapClassPath().getURLs()

2、Extension ClassLoader,擴充套件類載入器

負責載入$JAVA_HOME$/jre/lib/ext目錄中的jar檔案,是Application ClassLoader的父類。

//檢視其載入的jar包資訊
URL[] urLs = ((URLClassLoader) ClassLoader.getSystemClassLoader().getParent()).getURLs();

3、Application ClassLoader,應用程式類載入器

系統預設載入器,負責載入使用者類所在路徑的類資訊。可以由ClassLoader.getSystemClassLoader()直接獲取。

下圖是獲取系統類載入器以及獲取其父類,可以看到,AppClassLoader的父類就是ExtClassLoader,而ExtClassLoader的父類是null,這是因為頂級載入器BootstrapClassLoader是用C++所寫,java無法獲取其資訊。

JVM的雙親委派模型(保證父類載入器會先載入類)

工作流程:如果一個類載入器收到了類載入的請求,首先不會自己去嘗試載入此類,而是把請求委派給父類載入器去完成,每一個層次的類載入器都是如此,因此所有的載入請求都會傳送到頂層的啟動類載入器中,只有當父載入器反饋無法完成此類的載入請求時,子載入器才會嘗試自己去載入。

就是預設載入器不會一開始就去載入,一直往上拋,拋到最頂層,如果到最頂層了,此時又不能載入類,就會往下拋,直到載入完為止。

具體如下圖

這個雙親委派特性體現在ClassLoader類的loadClass方法中

//name:類的全限定名
 //resolve:是否連結到指定的類 
 protected Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException{
    synchronized (getClassLoadingLock(name)) {
        //檢視是否已被載入,如果是,則直接返回
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                //parent是父載入器,也就是一直往上拋
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
                    //沒有父載入器,直接找頂級載入器
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
            }

            if (c == null) {
                long t1 = System.nanoTime();
                //父類載入器無法載入的時候
                //用自定義載入器去載入
                c = findClass(name);
            }
        }
        if (resolve) {
            //連結到指定的類
            resolveClass(c);
        }
        return c;
    }
}

上面的loadClass方法開頭有一個加鎖的程式碼,加鎖的物件的getClassLoadingLock是這個方法返回的。

protected Object getClassLoadingLock(String className) {
    Object lock = this;
    //parallelLockMap是一個Map物件
    //parallelLockMap為空,則直接返回當前ClassLoader
    if (parallelLockMap != null) {
        Object newLock = new Object();
        //className作為key,如果存在,則直接返回舊值,如果不存在,
        //則將newLock作為value存入,此時lock就是newLock
        lock = parallelLockMap.putIfAbsent(className, newLock);
        if (lock == null) {
            lock = newLock;
        }
    }
    return lock;
}

而parallelLockMap 是什麼東西呢?如下,是一個ConcurrentHashMap物件,文件顯示是該類不為null的時候當前載入器就具有並行的功能。

private final ConcurrentHashMap<String, Object> parallelLockMap;
private ClassLoader(Void unused, ClassLoader parent) {
    this.parent = parent;
    //ParallelLoaders是靜態內部類
    if (ParallelLoaders.isRegistered(this.getClass())) {
        parallelLockMap = new ConcurrentHashMap<>();
        package2certs = new ConcurrentHashMap<>();
        domains = Collections.synchronizedSet(new HashSet<ProtectionDomain>());
        assertionLock = new Object();
    } else {
        // no finer-grained lock; lock on the classloader instance
        parallelLockMap = null;
        package2certs = new Hashtable<>();
        domains = new HashSet<>();
        assertionLock = this;
    }}
private static class ParallelLoaders { private ParallelLoaders() {} //擁有並行能力的set集合 private static final Set<Class<? extends ClassLoader>> loaderTypes = Collections.newSetFromMap( new WeakHashMap<Class<? extends ClassLoader>, Boolean>()); static { synchronized (loaderTypes) { loaderTypes.add(ClassLoader.class); } } //往set集合上新增擁有並行能力的ClassLoader static boolean register(Class<? extends ClassLoader> c) { synchronized (loaderTypes) { if (loaderTypes.contains(c.getSuperclass())) { loaderTypes.add(c); return true; } else { return false; } } } //判斷某個ClassLoader是否擁有並行能力 static boolean isRegistered(Class<? extends ClassLoader> c) { synchronized (loaderTypes) { return loaderTypes.contains(c); } } }

另外,自定義類載入器官方推薦是重寫findClass()方法,這樣可以確保是符合雙親委派模型的。

=======================================================

我是Liusy,一個喜歡健身的程式設計師。

歡迎關注微信公眾號【Liusy01】,一起交流Java技術及健身,獲取更多幹貨。