0 類加載器介紹
Java類加載器是Java運(yùn)行時(shí)環(huán)境(Java Runtime Environment)的一部分,它負(fù)責(zé)動(dòng)態(tài)加載Java類到Java虛擬機(jī)的內(nèi)存空間中。類通常是按需加載,即第一次使用該類時(shí)才加載。由于有了類加載器,Java運(yùn)行時(shí)系統(tǒng)不需要知道文件與文件系統(tǒng)。
Java中的類加載器是java.lang.ClassLoader
,它是一個(gè)抽象類。給定一個(gè)類名,ClassLoader就負(fù)責(zé)把這個(gè)類從特定的文件系統(tǒng)中加載到虛擬機(jī)中。
Class類有一個(gè)方法getClassLoader()
,每一個(gè)類的Class對(duì)象都可以調(diào)用這個(gè)方法來獲取把這個(gè)類加載到虛擬機(jī)中的ClassLoader。
對(duì)于數(shù)組來說,它們不是由ClassLoader來創(chuàng)建,而是由Java運(yùn)行時(shí)創(chuàng)建,數(shù)組的ClassLoader就是加載該數(shù)組元素類的ClassLoader。如果元素類型是基本類型,那么數(shù)組就沒有ClassLoader。
ClassLoader采用的是代理模式來加載類,每一個(gè)ClassLoader實(shí)例都有一個(gè)父ClassLoader(并不是繼承關(guān)系),當(dāng)一個(gè)類加載器需要加載一個(gè)類的時(shí)候,它會(huì)首先傳遞這個(gè)類的信息到parent 類加載器,請(qǐng)求parent來加載,然后依次傳遞,直到該類被成功加載或者失敗。如果失敗了,那么就由最開始的那個(gè)類加載器來進(jìn)行加載。在Java虛擬機(jī)中有一個(gè)內(nèi)置的類加載器是bootstrap class loader
,它是沒有parent的,但是可以作為所有ClassLoader實(shí)例的parent。這種加載方式也叫作雙親委派機(jī)制或者父委托機(jī)制。
通常來講,類加載器都是加載本地的Class文件,但是它也可以加載其它來源的文件,比如從網(wǎng)絡(luò)下載下來的。可以通過繼承java.lang.ClassLoader
類的方式實(shí)現(xiàn)自己的類加載器,以滿足一些特殊的需求而不需要完全了解Java虛擬機(jī)的類加載的細(xì)節(jié)。ClassLoader的一個(gè)方法defineClass
可以把一個(gè)字節(jié)數(shù)組轉(zhuǎn)為Class實(shí)例。然后可以根據(jù)Class.newInstance()
方法來創(chuàng)建一個(gè)對(duì)象。被ClassLoader創(chuàng)建的類的方法或者構(gòu)造方法可能還會(huì)引用其它的類,為了確定引用的類,虛擬機(jī)會(huì)調(diào)用最開始加載引用類的ClassLoader的loadClass
方法。
protected final Class<?> defineClass(String name, byte[] b, int off, int len)
例如,想要自定義一個(gè)NetworkClassLoader,來加載從網(wǎng)絡(luò)傳來的Class類:
ClassLoader loader = new NetworkClassLoader(host, port);
Object main = loader.loadClass("Main", true).newInstance();
. . .
NetworkClassLoader必須重寫findClass
方法,,然后定義一個(gè)方法來返回Class類的字節(jié)數(shù)組。當(dāng)下載完畢,需要調(diào)用defineClass方法,示例如下:
class NetworkClassLoader extends ClassLoader {
String host;
int port;
public Class findClass(String name) {
byte[] b = loadClassData(name);
return defineClass(name, b, 0, b.length);
}
private byte[] loadClassData(String name) {
// load the class data from the connection
. . .
}
}
1 JVM中的ClassLoader
JVM中有3個(gè)默認(rèn)的類加載器:
- 引導(dǎo)(Bootstrap)類加載器。用C/C++寫的,在Java代碼中無法獲取到。主要是加載存儲(chǔ)在
<JAVA_HOME>/jre/lib
目錄下的核心Java庫,對(duì)應(yīng)的加載路徑是sun.boot.class.path
。 - 擴(kuò)展(Extensions)類加載器.用來加載
<JAVA_HOME>/jre/lib/e。t
目錄下或者對(duì)應(yīng)的加載路徑java.ext.dirs
中指明的Java擴(kuò)展庫。Java 虛擬機(jī)的實(shí)現(xiàn)會(huì)提供一個(gè)擴(kuò)展庫目錄。該類加載器在此目錄里面查找并加載 Java 類。該類由sun.misc.Launcher$ExtClassLoader
實(shí)現(xiàn)。 - Apps類加載器(也稱系統(tǒng)類加載器)。根據(jù) Java應(yīng)用程序的類路徑(java.class.path或CLASSPATH環(huán)境變量)來加載 Java 類。一般來說,Java 應(yīng)用的類都是由它來完成加載的。可以通過
ClassLoader.getSystemClassLoader()
來獲取它。該類由sun.misc.Launcher$AppClassLoader
實(shí)現(xiàn),它的parent類加載器是ExtClassLoader。
下面通過一個(gè)示例來看一下:
package classLoader;
public class ClassLoaderTest {
public static void main(String[] args) {
ClassLoaderTest clt=new ClassLoaderTest();
ClassLoader cl=clt.getClass().getClassLoader();
System.out.println(cl);
System.out.println(cl.getParent());
System.out.println(cl.getParent().getParent());
System.out.println(ClassLoader.getSystemClassLoader());
System.out.println(System.getProperty("sun.boot.class.path"));
System.out.println(System.getProperty("java.ext.dirs"));
System.out.println(System.getProperty("java.class.path"));
}
}
打印結(jié)果:
sun.misc.Launcher$AppClassLoader@2a139a55
sun.misc.Launcher$ExtClassLoader@7852e922
null
sun.misc.Launcher$AppClassLoader@2a139a55
/Library/Java/JavaVirtualMachines/jdk1.8.0_121.jdk/Contents/Home/jre/lib/resources.jar:
/Library/Java/JavaVirtualMachines/jdk1.8.0_121.jdk/Contents/Home/jre/lib/rt.jar:
/Library/Java/JavaVirtualMachines/jdk1.8.0_121.jdk/Contents/Home/jre/lib/sunrsasign.jar:
/Library/Java/JavaVirtualMachines/jdk1.8.0_121.jdk/Contents/Home/jre/lib/jsse.jar:
/Library/Java/JavaVirtualMachines/jdk1.8.0_121.jdk/Contents/Home/jre/lib/jce.jar:
/Library/Java/JavaVirtualMachines/jdk1.8.0_121.jdk/Contents/Home/jre/lib/charsets.jar:
/Library/Java/JavaVirtualMachines/jdk1.8.0_121.jdk/Contents/Home/jre/lib/jfr.jar:
/Library/Java/JavaVirtualMachines/jdk1.8.0_121.jdk/Contents/Home/jre/classes
/Users/jason/Library/Java/Extensions:/Library/Java/JavaVirtualMachines/jdk1.8.0_121.jdk/Contents/Home/jre/lib/ext:
/Library/Java/Extensions:/Network/Library/Java/Extensions:/System/Library/Java/Extensions:/usr/lib/java
/works/EclipseWorkSpace/classLoader/bin
java.lang.ClassLoader
繼承關(guān)系如下:
其中AppClassLoader和ExtClassLoader都是在Laucher中的內(nèi)部類。而這個(gè)Laucher是JVM的入口。
注意: 并不是說子加載器繼承自父加載器
2 ClassLoader源碼分析
前面已經(jīng)講了,類的加載使用的是雙親委派機(jī)制。那我們啟動(dòng)一個(gè)Java應(yīng)用程序,它的類加載順序是從AppClassLoader委托ExtClassLoader,如果ExtClassLoader也找不到就會(huì)去委托Bootstrap類加載器加載。如果父加載器沒有找到的話,再從子加載器中加載,加載到的類會(huì)被緩存起來,如果最終都沒有找到這個(gè)類,就會(huì)報(bào)一個(gè)異常ClassNotFoundException
。
我們先看一下ClassLoader的構(gòu)造方法,它有3個(gè)構(gòu)造方法,但是其中有一個(gè)私有的:
//最終調(diào)用的還是這個(gè)私有的方法
private ClassLoader(Void unused, ClassLoader parent) {}
//有參構(gòu)造,傳遞parent類加載器
protected ClassLoader(ClassLoader parent) {
this(checkCreateClassLoader(), parent);
}
// 無參構(gòu)造,默認(rèn)采用getSystemClassLoader()方法獲取的ClassLoader作為parent類加載器
protected ClassLoader() {
this(checkCreateClassLoader(), getSystemClassLoader());
}
下面我們來看getSystemClassLoader()這個(gè)方法:
public static ClassLoader getSystemClassLoader() {
// 初始化系統(tǒng)類加載器
initSystemClassLoader();
if (scl == null) {
return null;
}
// 做一些安全方面的校驗(yàn)
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
checkClassLoaderPermission(scl, Reflection.getCallerClass());
}
return scl;
}
initSystemClassLoader()方法如下:
private static synchronized void initSystemClassLoader() {
if (!sclSet) {
if (scl != null)
throw new IllegalStateException("recursive invocation");
//獲取Launcher對(duì)象
sun.misc.Launcher l = sun.misc.Launcher.getLauncher();
if (l != null) {
Throwable oops = null;
//調(diào)用Launcher對(duì)象的getClassLoader()方法,這個(gè)獲取的就是AppClassLoader,詳細(xì)內(nèi)容可以看下面對(duì)Launcher的分析
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;
}
}
下面我們就ClassLoader加載一個(gè)類的過程來進(jìn)行一下分析。ClassLoader加載一個(gè)類,調(diào)用的方法是loadClass()方法
:
//通常外界是調(diào)用ClassLoader的這個(gè)loadClass方法
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
//ClassLoader的默認(rèn)加載方式,如果需要自定義ClassLoader最好不要重寫這個(gè)方法。
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// 同步代碼塊
synchronized (getClassLoadingLock(name)) {
// findLoadedClass是一個(gè)native方法,如果已經(jīng)加載過的類是會(huì)被緩存起來的,直接從緩存獲取即可
Class<?> c = findLoadedClass(name);
if (c == null) {
// c為null,說明沒有緩存,就需要初次加載
long t0 = System.nanoTime();
try {
if (parent != null) {
// 如果parent不為null就委托parent去加載
c = parent.loadClass(name, false);
} else {
// 如果parent為null就委托bootstrap class loader去加載
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 如果非空的父加載器找不到類會(huì)拋出異常,在這里try-catch住了
}
if (c == null) {
// 如果父加載器和bootstrap加載器都沒有找到,就會(huì)調(diào)用ClassLoader實(shí)例自身的findClass()方法。
// 其方法體是拋出一個(gè)ClassNotFoundException異常,
// 所以繼承ClassLoader的子類加載器需要重寫這個(gè)findClass()方法
long t1 = System.nanoTime();
c = findClass(name);
...
}
}
if (resolve) {
// 使Classloader鏈接指定的類,如果這個(gè)類已經(jīng)被鏈接過了,那么這個(gè)方法只做一個(gè)簡單的返回。
// 否則,這個(gè)類將被按照 Java?規(guī)范中的Execution描述進(jìn)行鏈接
resolveClass(c);
}
return c;
}
}
需要注意的是第一個(gè)參數(shù),name表示的是二進(jìn)制名稱(Binary name),例如:
"java.lang.String"
"javax.swing.JSpinner$DefaultEditor"
"java.security.KeyStore$Builder$FileBuilder$1"
"java.net.URLClassLoader$3$1"
需要指出:類加載過程是同步的
簡單總結(jié)一下類加載器的工作過程:
- 如果當(dāng)前加載的類已經(jīng)加載過,直接從緩存獲取。
- 之前沒有加載過,如果該ClassLoader對(duì)象的parent不為null就委托父加載器加載,父加載器會(huì)重新開始走第1步。如果parent為null,那么就采用根加載器bootstrap class loader進(jìn)行加載。
- 如果之前還是沒有成功加載類,那么就會(huì)調(diào)用當(dāng)前ClassLoader的findClass()方法去加載。
類加載器采用雙親委派機(jī)制的好處:
- 加載的類會(huì)被緩存起來,下次加載就快了。
- 安全,比如我們自定義一個(gè)與系統(tǒng)String包名類型一致的類,然后想要把這個(gè)String類加載進(jìn)來干點(diǎn)壞事的話實(shí)際上是做不到的,由于父委托機(jī)制,真正的String類會(huì)被bootstrap class loader 加載(String類是存放在bootstrap class loader 負(fù)責(zé)加載的區(qū)域),就不會(huì)再調(diào)用我們這個(gè)假的String類。實(shí)際上,如果你自定義了一個(gè)類加載器并且重寫了loadClass的邏輯,最終還是不能加載假的String類,因?yàn)镃lassLoader有一個(gè)preDefineClass方法,該方法會(huì)檢測類的包名,如果是'java'開頭就會(huì)拋出一個(gè)SecurityException異常。
那么AppClassLoader和ExtClassLoader是什么時(shí)候初始化的呢?下面我們?cè)偃タ匆幌翷auncher的部分源碼:
# sun.misc.Launcher
//構(gòu)造方法
public Launcher() {
ClassLoader extcl;
try {
// 創(chuàng)建ExtClassLoader對(duì)象
extcl = ExtClassLoader.getExtClassLoader();
} catch (IOException e) {
throw new InternalError(
"Could not create extension class loader", e);
}
//創(chuàng)建AppClassLoader對(duì)象loader,這個(gè)loader就是上面講到的,Launcher的getClassLoader()方法返回的對(duì)象。
//AppClassLoader.getAppClassLoader()方法的參數(shù)為extcl,實(shí)際上就是把ExtClassLoader對(duì)象當(dāng)作其父加載器
try {
loader = AppClassLoader.getAppClassLoader(extcl);
} catch (IOException e) {
throw new InternalError(
"Could not create application class loader", e);
}
// 設(shè)置上下文的類加載器,也就是AppClassLoader對(duì)象。
Thread.currentThread().setContextClassLoader(loader);
// Finally, install a security manager if requested
String s = System.getProperty("java.security.manager");
if (s != null) {
SecurityManager sm = null;
if ("".equals(s) || "default".equals(s)) {
sm = new java.lang.SecurityManager();
} else {
try {
sm = (SecurityManager)loader.loadClass(s).newInstance();
} catch (IllegalAccessException e) {
} catch (InstantiationException e) {
} catch (ClassNotFoundException e) {
} catch (ClassCastException e) {
}
}
if (sm != null) {
System.setSecurityManager(sm);
} else {
throw new InternalError(
"Could not create SecurityManager: " + s);
}
}
}
關(guān)于ExtClassLoader和AppClassLoader的源碼我們這里就不做多余的介紹了,感興趣的可以去看一下Launcher這個(gè)類,ExtClassLoader和AppClassLoader都是其靜態(tài)內(nèi)部類。
3 自定義類加載器
我們完全可以通過自定義類加載器來加載我們想要加載的類,這個(gè)類可能來源于網(wǎng)絡(luò),也可能來源于文件系統(tǒng)。
從前面的分析我們知道,加載一個(gè)類的過程調(diào)用的是ClassLoader的loadClass()方法。自定義類加載器通常不要重寫loadClass()方法的邏輯。在這個(gè)方法內(nèi)部,如果所有的父加載器都沒有成功加載,就會(huì)調(diào)用ClassLoader對(duì)象自身的findClass()方法,自定義類加載器可以實(shí)現(xiàn)這個(gè)findClass()方法即可。
還有一個(gè)關(guān)鍵的方法就是調(diào)用ClassLoader對(duì)象的defineClass()方法,這樣就可以創(chuàng)建一個(gè)Class對(duì)象了。
public class CustomClassLoader extends ClassLoader {
private String dirPath;
public CustomClassLoader(String dirPath) {
this.dirPath = dirPath;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
Class clazz = null;
//注意這個(gè)name一定要是二進(jìn)制名稱,如'java.lang.String'
//根據(jù)類的二進(jìn)制名稱,獲得該class文件的字節(jié)碼數(shù)組
byte[] classData = getClassDataBytes(name);
if (classData == null) {
throw new ClassNotFoundException();
}
//調(diào)用define()方法將class的字節(jié)碼數(shù)組轉(zhuǎn)換成Class類的實(shí)例
clazz = defineClass(name, classData, 0, classData.length);
return clazz;
}
private byte[] getClassDataBytes(String name) {
FileInputStream is = null;
try {
String path = classNameToPath(name);
is = new FileInputStream(path);
byte[] buff = new byte[1024];
int len;
ByteArrayOutputStream baos = new ByteArrayOutputStream();
while ((len = is.read(buff)) != -1) {
baos.write(buff, 0, len);
}
return baos.toByteArray();
} catch (Exception e) {
e.printStackTrace();
} finally {
if (is != null) {
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return null;
}
private String classNameToPath(String name) {
return dirPath + "/" + name.replace(".", "/") + ".class";
}
}
自定義的類加載器已經(jīng)寫好了,下面我們來演示一下如何加載一個(gè)類,首先我們編寫一個(gè)java類:
package com.sososeen09;
class Test {
public Test() {
}
public void print() {
System.out.println("this is Test Class");
}
}
根據(jù)javac
命令把java文件編譯為對(duì)應(yīng)的class文件。這個(gè)Test.class文件的二進(jìn)制名稱就是com.sososeen09.Test
。
public class ClassLoaderTest {
public static void main(String[] args) {
String srcPath = "/test/bin";
CustomClassLoader customClassLoader = new CustomClassLoader(srcPath);
String classname = "com.sososeen09.Test";
try {
Class clazz = customClassLoader.loadClass(classname);
System.out.println("loaded class: " + clazz);
System.out.println("class loader: " + clazz.getClassLoader());
System.out.println("class loader parent: " + clazz.getClassLoader().getParent());
Constructor constructor = clazz.getConstructor();
constructor.setAccessible(true);
Object o = constructor.newInstance();
Method print = clazz.getDeclaredMethod("print");
print.setAccessible(true);
print.invoke(o);
} catch (Exception e) {
e.printStackTrace();
}
運(yùn)行一下可以查看打印結(jié)果:
loaded class: class com.sososeen09.Test
class loader: com.sososeen09.javamodule.classloaders.CustomClassLoader@14ae5a5
class loader parent: sun.misc.Launcher$AppClassLoader@18b4aac2
this is Test Class
可以看到我們自定義的類加載器已經(jīng)成功的把一個(gè)文件系統(tǒng)中的class加載了。
需要注意:我們是把二進(jìn)制文件前面的包名轉(zhuǎn)為路徑了,所以我們傳遞的srcPath是"/test/bin"
,那么實(shí)際上class文件存放路徑應(yīng)該是"/test/bin/com/sososeen09/"
。
4 擴(kuò)展知識(shí)點(diǎn)
在Java中,類的加載時(shí)按需加載,也就是需要的時(shí)候才會(huì)把class文件加載到內(nèi)存中。可以分為隱式加載和顯示加載。
- 隱式加載:由當(dāng)new一個(gè)Java對(duì)象,或者調(diào)用類的靜態(tài)方法或者使用靜態(tài)成員變量的時(shí)候,會(huì)加載當(dāng)前的Class。
- 顯示加載,顯示的調(diào)用Class.forName()方法,或者調(diào)用ClassLoader的loadClass()方法。
參考文章