JVM類載入器與雙親委派模型(二)
阿新 • • 發佈:2018-12-05
(7)URLClassLoader類
前面說到,ClassLoader這個頂級父類只是定義好了雙親委派模型的工作機制;但是ClassLoader是個抽象類,無法直接建立物件,所以需要由繼承它的子類完成建立物件的任務。子類需要自己實現findClass方法,並且在例項化時指定parent屬性的值。如果parent設為null,則意味著它的父“類載入”是啟動類載入器。
第一個需要傳入的引數有包括一個URL陣列和父“類載入器”ClassLoader:parent變數的意義無需多言;而第一個引數 urls用來構造私有常量ucp: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(); }
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都長啥樣?先看下構造方法的註釋:
字面理解,url可以是本地檔案系統的路徑,如果以“/”結尾代表其是一個目錄,否則預設為jar包檔案。其實,這個url也可以是一個網路地址,只要下載下來的資料是個jar包就行。/** * 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. */
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檔案的相對路徑去檔案系統中載入位元組流。 這個方法就不在本文的討論範圍內了。