在JVM中,一個類被加載到虛擬機這個過程包括有3個步驟,即加載、鏈接和初始化。而加載這個過程,就是由類加載器ClassLoader
進行加載的,類加載器天生就負責這個職責。java
Java自己給咱們提供了幾種類型的類加載器,啓動類加載器Bootstrap ClassLoader
、擴展類加載器Extension ClassLoader
、應用類加載器App ClassLoader
,除了上面3種,咱們也能夠定義咱們本身的類加載器,此時咱們只須要繼承ClassLoader
類,重寫其findClass()
方法便可。數組
固然,有時候,你覺得的並非你覺得的,當咱們用自定義類加載器去加載類路徑ClassPath
下某個class
文件時,而後調用該Class
對象的getClassLoader()
方法時,咱們會發現,加載該類的類加載器,並非咱們自定義的加載器,至於爲何?下文將會講到。 下圖是JVM類加載器機制bash
一、啓動類加載器Bootstrap ClassLoader
網絡
啓動類加載器,負責%JRE_HOME/lib/
目錄下的相關類文件,好比rt.jar
、tools.jar
等等,好比說咱們的String
類存放在rt.jar
中,器加載器就是Bootstrap ClassLoader
eclipse
二、擴展類加載器Extension ClassLoader
ide
擴展類加載器,負責%JRE_HOME/lib/ext
目錄下的相關類文件,好比rt.jar
、tools.jar
等等工具
三、應用類加載器App ClassLoader
開發工具
應用類加載器,負責加載應用程序類路徑下的class
文件。ui
說到類路徑的問題,咱們解釋一下,java的類路徑指的是咱們配置的系統環境變量的值:CLASSPATH
,但又不侷限於這個值。小編先打印下咱們本地的classpath
的值: this
.;D:\development\jdk\lib\dt.jar;D:\development\jdk\lib\tools.jar;
你會發現,這根本就不是咱們用的開發工具中的目錄呀,那麼JVM
是怎麼加載到咱們在eclipse
上編寫的類呢?答案是eclipse
已經幫咱們弄好了一切。
下邊咱們舉個例子,看下String
類的類加載器是什麼:
System.out.println(String.class.getClassLoader());
上面這句代碼輸出null
。爲何呢?由於若是一個類是被Bootstrap ClassLoader
或者Extension ClassLoader
加載時,getClassLoader()
規定輸出null
。又由於String
類存在rt.jar
中,將會被Bootstrap ClassLoader
加載,因此輸出null
。
談及類加載器,咱們不得不說類的雙親委派機制,一句話總結雙親委派機制,小編總結成一句話:
若是自定義加載器P有父加載器P1,那麼在加載前就將加載任務委派給其父親P1,若是P1也存在父加載器P2,那麼將加載任務委派給P2,若是最頂層的Bootstrap ClassLoader還加載不到,那麼就再逆着順序加載,直到類被加載到~~
ClassLoader
類特性介紹一、每個Class
實例都包含一個ClassLoader
引用
Class
對象的getClassLoader()
方法獲取其類加載器。固然了,就如上面說的,若是一個類是被Bootstrap ClassLoader
或者Extension ClassLoader
加載時,getClassLoader()
規定輸出null
。這是一個須要咱們注意的點。 二、對於數組類型[]
對象,它們不是由類加載器ClassLoader
去進行加載的,而是Java虛擬機根據須要自動建立的。咱們經過數組類型的Class
對象的getClassLoader()
方法返回的值跟數組裏邊元素所使用的的類加載器同樣,但若是數組元素爲原始類型int
啥的,則getClassLoader()
方法將返回null
// sun.misc.Launcher$AppClassLoader@73d16e93
ClassLoaderTest[] array = new ClassLoaderTest[5];
System.out.println(array.getClass().getClassLoader());
// null
int[] intArray = new int[3];
System.out.println(intArray.getClass().getClassLoader());
複製代碼
三、咱們能夠經過繼承ClassLoader
類,來實現本身的類加載器
/**
* 自定義類加載器
* @Author jiawei huang
* @Since 2019年8月7日
* @Version 1.0
*/
public class MyClassLoader extends ClassLoader {
// 若是咱們從其餘地方進行加載,咱們能夠指定路徑
private String classpath;
public MyClassLoader(String classPath) {
classpath = classPath;
}
public MyClassLoader() {
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
Class clazz = null;
// 獲取該class文件字節碼數組
byte[] classData = getClassData();
if (classData != null) {
// 將class的字節碼數組轉換成Class類的實例
clazz = defineClass(name, classData, 0, classData.length);
}
return clazz;
}
private byte[] getClassData() {
byte[] bytes = null;
File file = new File(classpath);
if (file.exists()) {
// 從文件中讀取class字節數據
FileInputStream in = null;
ByteArrayOutputStream out = null;
try {
in = new FileInputStream(file);
out = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int size = 0;
while ((size = in.read(buffer)) != -1) {
out.write(buffer, 0, size);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
bytes = out.toByteArray();
}
return bytes;
}
}
複製代碼
假設咱們在ex
包路徑下有個Main.java
,看看咱們下面的輸出:
A.java
MyClassLoader loader = new MyClassLoader();
Class<?> clazzClass = loader.loadClass("ex.Main");
// 1
System.out.println(clazzClass.getClassLoader());
複製代碼
上面1
處將輸出sun.misc.Launcher$AppClassLoader@73d16e93
,咱們可能會問,明明我是用本身的類加載器去加載ex.Main
的呀,爲何卻輸出AppClassLoader
這個類加載器呢?其實這就是雙親委派機制,MyClassLoader
把加載任務給到其父加載器App ClassLoader
,恰好ex.Main
又處於類路徑下,因此App ClassLoader
加載以後就直接返回了。
四、一個類整個生命週期只會被加載一次,有且僅有一次
五、支持並行加載的加載器稱爲並行加載器,但前提是,咱們必須在自定義加載器的初始化時,調用ClassLoader.registerAsParallelCapable();
方法註冊本身,怎麼作呢?咱們看看URLClassLoader
的源碼。
static {
// 註冊本身
ClassLoader.registerAsParallelCapable();
}
複製代碼
詳情咱們看下ClassLoader
中loadClass()
方法源碼:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// 這裏有個鎖
synchronized (getClassLoadingLock(name)) {
一、先檢查這個名稱的類是否已經被加載過,若是是,就再也不加載了,這也印證了咱們第4點說的一個類只會被加載一次
Class<?> c = findLoadedClass(name);
// 二、若是該類沒有被加載,那麼就進行加載
if (c == null) {
long t0 = System.nanoTime();
try {
// 三、若是存在父加載器,就進行委派,這就是雙親委派機制的原理
if (parent != null) {
c = parent.loadClass(name, false);
} else {
// 四、去`Bootstrap classloader`加載
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
// 五、若是3,4都沒有加載到,那就執行咱們自定義的classloader,這也是爲何咱們要重寫findClass方法的緣由所在
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
複製代碼
class
文件的加載,獲取byte[]
,而後經過ClassLoader
的defineClass()
將byte[]
轉換成Class
對象,最後經過Class.newInstance()
便可轉成Java對象啦~~~ClassLoader loader = new NetworkClassLoader(host, port);
Object main = loader.loadClass("Main", true).newInstance();
複製代碼
SpringBoot
中,構造器爲何要加上ClassLoader
參數?由於初始化過程當中,load()
方法須要從不一樣的地方去加載類文件。
protected void load(ApplicationContext context, Object[] sources) {
if (logger.isDebugEnabled()) {
logger.debug(
"Loading source " + StringUtils.arrayToCommaDelimitedString(sources));
}
BeanDefinitionLoader loader = createBeanDefinitionLoader(
getBeanDefinitionRegistry(context), sources);
if (this.beanNameGenerator != null) {
loader.setBeanNameGenerator(this.beanNameGenerator);
}
if (this.resourceLoader != null) {
loader.setResourceLoader(this.resourceLoader);
}
if (this.environment != null) {
loader.setEnvironment(this.environment);
}
loader.load();
}
複製代碼