1. 程式人生 > >JVM類載入器與雙親委派模型(二)

JVM類載入器與雙親委派模型(二)

(7)URLClassLoader類

前面說到,ClassLoader這個頂級父類只是定義好了雙親委派模型的工作機制;但是ClassLoader是個抽象類,無法直接建立物件,所以需要由繼承它的子類完成建立物件的任務。子類需要自己實現findClass方法,並且在例項化時指定parent屬性的值。如果parent設為null,則意味著它的父“類載入”是啟動類載入器。

繼承體系如下:URLClassLoader extends SecureClassLoader extends ClassLoader 其中SecureClassLoader只是做了一些安全方面的限制,而關鍵的業務程式碼並沒有實現,把這些實現任務交給了子類URLClassLoader。 URLClassLoader不是一個抽象類,所以可以直接拿來建立物件,然後呼叫loadClass方法載入類。那麼需要分析它的構造方法,一共有5個,其中三個宣告為public:
public URLClassLoader(URL[] urls, ClassLoader parent) {
    super(parent);
    // this is to make the stack depth consistent with 1.1
    SecurityManager security = System.getSecurityManager();
    if (security != null) {
        security.checkCreateClassLoader();
    }
    ucp = new URLClassPath(urls);
    this.acc = AccessController.getContext();
}
第一個需要傳入的引數有包括一個URL陣列和父“類載入器”ClassLoader:parent變數的意義無需多言;而第一個引數 urls用來構造私有常量ucp:
private final URLClassPath ucp;
URLClassLoader還有另外一個私有常量acc:
private final AccessControlContext acc;
它跟許可權控制有關,也會頻繁出現在URLClassLoader的構造方法中,暫時不用理會。 於是,這個構造方法中,除了指定父“類載入器”外,最重要的程式碼就是這行了:
ucp = new URLClassPath(urls);
根據傳入的URL陣列構造一個URLClassPath物件,這個物件用來根據class檔案的路徑生成一個Resource物件,其中包含了檔案的二進位制資料:
Resource res = ucp.getResource(path, false);
這個URLClassPath物件才是整個URLClassLoader物件進行類載入的核心,下文我們將會分析到。那麼傳進來的這些urls都長啥樣?先看下構造方法的註釋:
/**
 * Constructs a new URLClassLoader for the given URLs. The URLs will be
 * searched in the order specified for classes and resources after first
 * searching in the specified parent class loader. Any URL that ends with
 * a '/' is assumed to refer to a directory. Otherwise, the URL is assumed
 * to refer to a JAR file which will be downloaded and opened as needed.
 */
字面理解,url可以是本地檔案系統的路徑,如果以“/”結尾代表其是一個目錄,否則預設為jar包檔案。其實,這個url也可以是一個網路地址,只要下載下來的資料是個jar包就行。

public URLClassLoader(URL[] urls, ClassLoader parent, URLStreamHandlerFactory factory) {
    super(parent);
    // this is to make the stack depth consistent with 1.1
    SecurityManager security = System.getSecurityManager();
    if (security != null) {
        security.checkCreateClassLoader();
    }
    ucp = new URLClassPath(urls, factory);
    acc = AccessController.getContext();
}
對比前一個,這個構造方法只是生成URLClassPath物件的方式不一樣,使用了傳入的URLStreamHandlerFactory物件。
public URLClassLoader(URL[] urls) {
    super();
    // this is to make the stack depth consistent with 1.1
    SecurityManager security = System.getSecurityManager();
    if (security != null) {
        security.checkCreateClassLoader();
    }
    ucp = new URLClassPath(urls);
    this.acc = AccessController.getContext();
}
這個構造方法最省事,只需要傳入一個URL陣列,但是其實現是最複雜的,複雜在父“類載入器”的構造上。這個空參的super()方法,最終呼叫了ClassLoader的如下構造方法:
protected ClassLoader() {
    this(checkCreateClassLoader(), getSystemClassLoader());
}
看下它的註釋:
/**
 * Creates a new class loader using the <tt>ClassLoader</tt> returned by
 * the method {@link #getSystemClassLoader()
 * <tt>getSystemClassLoader()</tt>} as the parent class loader.
 */
也就是說,當建立URLClassLoader物件時如果不指定parent值,那麼parent的值最終由ClassLoadder.getSystemClassLoader方法決定,其程式碼如下:
public static ClassLoader getSystemClassLoader() {
    initSystemClassLoader();
    if (scl == null) {
        return null;
    }
    SecurityManager sm = System.getSecurityManager();
    if (sm != null) {
        checkClassLoaderPermission(scl, Reflection.getCallerClass());
    }
    return scl;
}
顯然,返回值scl會在initSystemClassLoader方法中完成初始化:
private static synchronized void initSystemClassLoader() {
    if (!sclSet) {
        if (scl != null)
            throw new IllegalStateException("recursive invocation");
        sun.misc.Launcher l = sun.misc.Launcher.getLauncher();
        if (l != null) {
            Throwable oops = null;
            scl = l.getClassLoader();
            try {
                scl = AccessController.doPrivileged(
                    new SystemClassLoaderAction(scl));
            } catch (PrivilegedActionException pae) {
                oops = pae.getCause();
                if (oops instanceof InvocationTargetException) {
                    oops = oops.getCause();
                }
            }
            if (oops != null) {
                if (oops instanceof Error) {
                    throw (Error) oops;
                } else {
                    // wrap the exception
                    throw new Error(oops);
                }
            }
        }
        sclSet = true;
    }
}
其核心邏輯是:1、獲取一個Laucher物件;2、呼叫Laucher物件的getClassLoader方法,用其返回值初始化scl變數;3、使用SystemClassLoaderAction再次為scl賦值。先看一下 Laucher.getClassLoader()方法:
public ClassLoader getClassLoader() {
    return this.loader;
}
而loader變數的初始化在Laucher的構造方法中:
public Launcher() {
    Launcher.ExtClassLoader var1;
    try {
        var1 = Launcher.ExtClassLoader.getExtClassLoader();
    } catch (IOException var10) {
        throw new InternalError("Could not create extension class loader");
    }

    try {
        this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
    } catch (IOException var9) {
        throw new InternalError("Could not create application class loader");
    }
}
可以看到,在Launcher的構造方法中,先後建立了兩個內部類的物件Launcher.ExtClassLoader和Launcher.AppClassLoader,並且後者的parent變數就設定為前者;然後,將Launcher.AppClassLoader物件賦值給this.loader。也就是說,第二步scl的值就是一個Launcher.AppClassLoader物件。我們看下這個內部類:
static class AppClassLoader extends URLClassLoader {}
是URLClassLoader的子類,其getClassLoader方法如下:
public static ClassLoader getAppClassLoader(final ClassLoader var0) throws IOException {
    final String var1 = System.getProperty("java.class.path");
    final File[] var2 = var1 == null ? new File[0] : Launcher.getClassPath(var1);
    return (ClassLoader)AccessController.doPrivileged(new PrivilegedAction<Launcher.AppClassLoader>() {
        public Launcher.AppClassLoader run() {
            URL[] var1x = var1 == null ? new URL[0] : Launcher.pathToURLs(var2);
            return new Launcher.AppClassLoader(var1x, var0);
        }
    });
}
首先,獲取系統屬性java.class.path的值,這個值其實就是經常設定的環境變數ClassPath的值;然後,根據根據這個系統屬性的值生成一個File陣列,陣列包含了ClassPath指定的各個類的class檔案;最後,根據File陣列生成URL陣列,然後把這個陣列和傳入的parent ClassLoader一起傳入給構造方法生成一個Launcher.AppClassLoader物件。 接著看下Launcher.AppClassLoader的構造方法: 
AppClassLoader(URL[] var1, ClassLoader var2) {
    super(var1, var2, Launcher.factory);
}
直接呼叫了其父類URLClassLoader的構造方法,這個構造方法在前文已經分析過。

第三步,呼叫了SystemClassLoaderAction類的run方法為scl重新賦值,來看下這個類:
class SystemClassLoaderAction
    implements PrivilegedExceptionAction<ClassLoader> {
    private ClassLoader parent;

    SystemClassLoaderAction(ClassLoader parent) {
        this.parent = parent;
    }

    public ClassLoader run() throws Exception {
        String cls = System.getProperty("java.system.class.loader");
        if (cls == null) {
            return parent;
        }

        Constructor ctor = Class.forName(cls, true, parent)
            .getDeclaredConstructor(new Class[] { ClassLoader.class });
        ClassLoader sys = (ClassLoader) ctor.newInstance(
            new Object[] { parent });
        Thread.currentThread().setContextClassLoader(sys);
        return sys;
    }
}
這個類和ClassLoader在同一個類檔案中,但是沒有生命為public,我沒想明白為何這麼設計(內部類不行嗎?)。 這個類的作用通過下面這行程式碼就已經自解釋了: 
String cls = System.getProperty("java.system.class.loader”);
又是一個環境變數,或者說jvm引數,當指定了 java.system.class.loader系統變數時,那麼會把指定的這個類作為System Class Loader,這段程式碼就是為了生成一個該類的例項(利用了反射的方式),並且它的的父“類載入器”就是上一步生成的 Launcher.AppClassLoader物件。如果沒有指定這個系統變數的值,那麼就返回 Launcher.AppClassLoader
繞了這麼大一個圈子,原來在呼叫 public URLClassLoader(URL[] urls)生成物件時,這個物件的父“類載入器”要麼是環境變數 java.system.class.loader指定的類,要麼是 URLClassLoader的子類 Launcher.AppClassLoader。
說完構造方法,也就是如何建立物件,接著該分析這個URLClassLoader類是如何去載入其他類的,也就是findClass方法是如何實現的。URLClassLoader中定義的findClass方法如下:
protected Class<?> findClass(final String name)  throws ClassNotFoundException {
    try {
        return AccessController.doPrivileged(
            new PrivilegedExceptionAction<Class>() {
                public Class run() throws ClassNotFoundException {
                    String path = name.replace('.', '/').concat(".class");
                    Resource res = ucp.getResource(path, false);
                    if (res != null) {
                        try {
                            return defineClass(name, res);
                        } catch (IOException e) {
                            throw new ClassNotFoundException(name, e);
                        }
                    } else {
                        throw new ClassNotFoundException(name);
                    }
                }
            }, acc);
    } catch (java.security.PrivilegedActionException pae) {
        throw (ClassNotFoundException) pae.getException();
    }
}
拋開安全檢查等程式碼之後,核心程式碼如下:
String path = name.replace('.', '/').concat(".class");
Resource res = ucp.getResource(path, false);
if (res != null) {
    try {
        return defineClass(name, res);
    } catch (IOException e) {
        throw new ClassNotFoundException(name, e);
    }
} else {
    throw new ClassNotFoundException(name);
} 
第一行:
String path = name.replace('.', '/').concat(".class”);
name是傳入的引數,這個引數顯然是loadClass方法傳進來的,而loadClass的這個引數是呼叫時傳入的,一般形式如下:
ClassLoader.loadClass(“xx.xx.Xxx”);
當loadClass沒有找到時,就會把這個類傳遞給findClass讓它去找。而這行程式碼顯然是在把一個類名轉換為其對應的class檔案的相對路徑名。 第二行:
Resource res = ucp.getResource(path, false);
根據方法名,可以推測是在根據path去磁碟載入這個檔案,在記憶體中生存一個Resource物件。構造方法中初始化的ucp變數終於派上用場了:
private final URLClassPath ucp;
其型別是URLClassPath,這個變數的初始化是在構造方法中,這個後面再分析。getResource的實現也暫時略過。 如果這個res不為空,那麼進入下一行關鍵程式碼:
return defineClass(name, res);
呼叫了defineClass方法,這是個私有方法,如下:
private Class defineClass(String name, Resource res) throws IOException {
    long t0 = System.nanoTime();
    int i = name.lastIndexOf('.');
    URL url = res.getCodeSourceURL();
    if (i != -1) {
        String pkgname = name.substring(0, i);
        // Check if package already loaded.
        Manifest man = res.getManifest();
        if (getAndVerifyPackage(pkgname, man, url) == null) {
            try {
                if (man != null) {
                    definePackage(pkgname, man, url);
                } else {
                    definePackage(pkgname, null, null, null, null, null, null, null);
                }
            } catch (IllegalArgumentException iae) {
                // parallel-capable class loaders: re-verify in case of a
                // race condition
                if (getAndVerifyPackage(pkgname, man, url) == null) {
                    // Should never happen
                    throw new AssertionError("Cannot find package " +
                                             pkgname);
                }
            }
        }
    }
    // Now read the class bytes and define the class
    java.nio.ByteBuffer bb = res.getByteBuffer();
    if (bb != null) {
        // Use (direct) ByteBuffer:
        CodeSigner[] signers = res.getCodeSigners();
        CodeSource cs = new CodeSource(url, signers);
        sun.misc.PerfCounter.getReadClassBytesTime().addElapsedTimeFrom(t0);
        return defineClass(name, bb, cs);
    } else {
        byte[] b = res.getBytes();
        // must read certificates AFTER reading bytes.
        CodeSigner[] signers = res.getCodeSigners();
        CodeSource cs = new CodeSource(url, signers);
        sun.misc.PerfCounter.getReadClassBytesTime().addElapsedTimeFrom(t0);
        return defineClass(name, b, 0, b.length, cs);
    }
}
方法很長,其引數是類名和對應的檔案資源(Resource物件),返回值型別是Class,這意味著,這個方法將會根據clas檔案流生成一個Class物件。這個方法在邏輯上分為兩個部分,第一部分用來解析、驗證包名,具體細節就不再分析,我們假設包名通過了驗證。然後進入到第二部分:
java.nio.ByteBuffer bb = res.getByteBuffer();
    if (bb != null) {
        // Use (direct) ByteBuffer:
        CodeSigner[] signers = res.getCodeSigners();
        CodeSource cs = new CodeSource(url, signers);
        sun.misc.PerfCounter.getReadClassBytesTime().addElapsedTimeFrom(t0);
        return defineClass(name, bb, cs);
    } else {
        byte[] b = res.getBytes();
        // must read certificates AFTER reading bytes.
        CodeSigner[] signers = res.getCodeSigners();
        CodeSource cs = new CodeSource(url, signers);
        sun.misc.PerfCounter.getReadClassBytesTime().addElapsedTimeFrom(t0);
        return defineClass(name, b, 0, b.length, cs);
    }
If-else分支中的邏輯是類似的:根據url和CodeSigner構造一個CodeSource物件,最終還都是呼叫了父類中的defineClass方法,傳入的引數包括類名name,剛剛生成的CodeSource物件cs,以及位元組流。來看下父類中的defineClass方法:
protected final Class<?> defineClass(String name, java.nio.ByteBuffer b, CodeSource cs) {
    return defineClass(name, b, getProtectionDomain(cs));
}
被宣告為final說明不可覆蓋。邏輯很簡單,就是繼續呼叫父類的defineClass方法,只是把剛剛的CodeSource物件傳遞給getProtectionDomain方法得到一個ProtectionDomain物件。繼續跟蹤父類也就是ClassLoader類的defineClass方法:
protected final Class<?> defineClass(String name, java.nio.ByteBuffer b, ProtectionDomain protectionDomain)
    throws ClassFormatError {...}
對於這個方法,只需要看註釋就足夠了,具體邏輯有點複雜就不分析了:
Converts a {@link java.nio.ByteBuffer <tt>ByteBuffer</tt>} into an instance of class <tt>Class</tt>
說白了,就是把一段位元組流轉化為一個Class物件。那麼URLClassLoader載入類的機制已經分析完畢,最核心的一行程式碼其實就是:
Resource res = ucp.getResource(path, false);
利用了URLClassPath類的getResource方法根據class檔案的相對路徑去檔案系統中載入位元組流。 這個方法就不在本文的討論範圍內了。