做者 某人Valar
如需轉載請保留原文連接html本文涉及到的Java源碼均爲
Java8
版本java部分圖片來自百度,若有侵權請聯繫刪除c++
目錄:緩存
前言:咱們剛剛接觸Java時,在IDE(集成開發環境) 或者文本編輯器中所寫的都是.java文件,在編譯後會生成.class文件,又稱字節碼文件。安全
javac HelloWorld.java ---> HelloWorld.class 複製代碼
對於.class文件來講,須要被加載到虛擬機中才能使用,這個加載的過程就成爲類加載。若是想要知道類加載的方式,就須要知道類加載器和雙親委託機制的概念。也就是咱們本篇所要介紹的內容。bash
Java中的類加載器能夠分爲兩種:app
而系統類加載器又有3個:編輯器
Bootstrap ClassLoader用來加載JVM(Java虛擬機)
運行時所須要的系統類,其使用c++
實現。ide
從如下路徑來加載類:oop
%JAVA_HOME%/jre/lib
目錄,如rt.jar、resources.jar、charsets.jar等Java虛擬機的啓動就是經過 Bootstrap ClassLoader建立一個初始類來完成的。 能夠經過以下代碼來得出Bootstrap ClassLoader所加載的目錄:
public class ClassLoaderTest {
public static void main(String[]args) {
System.out.println(System.getProperty("sun.boot.class.path"));
}
}
複製代碼
打印結果爲:
C:\Program Files\Java\jdk1.8.0_102\jre\lib\resources.jar;
C:\Program Files\Java\jdk1.8.0_102\jre\lib\rt.jar;
C:\Program Files\Java\jdk1.8.0_102\jre\lib\sunrsasign.jar;
C:\Program Files\Java\jdk1.8.0_102\jre\lib\jsse.jar;
C:\Program Files\Java\jdk1.8.0_102\jre\lib\jce.jar;
C:\Program Files\Java\jdk1.8.0_102\jre\lib\charsets.jar;
C:\Program Files\Java\jdk1.8.0_102\jre\lib\jfr.jar;
C:\Program Files\Java\jdk1.8.0_102\jre\classes
複製代碼
能夠發現幾乎都是$JAVA_HOME/jre/lib
目錄中的jar包,包括rt.jar、resources.jar和charsets.jar等等。
Extensions ClassLoader(擴展類加載器)具體是由ExtClassLoader
類實現的,ExtClassLoader
類位於sun.misc.Launcher
類中,是其的一個靜態內部類。對於Launcher
類,能夠先當作是Java虛擬機的一個入口。
ExtClassLoader
的部分代碼以下:
Extensions ClassLoader負責將JAVA_HOME/jre/lib/ext
或者由系統變量-Djava.ext.dir
指定位置中的類庫加載到內存中。
經過如下代碼能夠獲得Extensions ClassLoader加載目錄:
System.out.println(System.getProperty("java.ext.dirs"));
複製代碼
打印結果爲:
C:\Program Files\Java\jdk1.8.0_102\jre\lib\ext;
C:\Windows\Sun\Java\lib\ext
複製代碼
也稱爲SystemAppClass(系統類加載器),具體是由AppClassLoader
類實現的,AppClassLoader
類也位於sun.misc.Launcher
類中。
部分代碼以下:
經過如下代碼能夠獲得App ClassLoader加載目錄:
System.out.println(System.getProperty("java.class.path"));
複製代碼
打印結果爲:
C:\workspace\Demo\bin
複製代碼
這個路徑其實就是當前Java工程目錄bin,裏面存放的是編譯生成的class文件。
在Java中,除了上述的3種系統提供的類加載器,還能夠自定義一個類加載器。
爲了能夠從指定的目錄下加載jar包或者class文件,咱們能夠用繼承java.lang.ClassLoader類的方式來實現一個本身的類加載器。
在自定義類加載器時,咱們通常複寫findClass
方法,並在findClass
方法中調用defineClass
方法。
接下來會先介紹下ClassLoader類相關的具體內容,以後看一個自定義類加載器demo。
從上面關於ExtClassLoader、AppClassLoader源碼圖中咱們能夠看到,他們都繼承自URLClassLoader,那這個URLClassLoader是什麼,其背後又有什麼呢?
先來一張很重要的繼承關係圖:
關係:
還有2個結論:
咱們準備一個簡單的demo 自建的一個Test.java文件。
public class Test{}
複製代碼
public class Main {
public static void main(String[] args) {
ClassLoader cl = Test.class.getClassLoader();
System.out.println("ClassLoader is:"+cl.toString());
}
}
複製代碼
這樣就能夠獲取到Test.class文件的類加載器,而後打印出來。結果是:
sun.misc.Launcher$AppClassLoader@75b83e92
複製代碼
也就是說明Test.class文件是由AppClassLoader加載的。
那AppClassLoader是誰加載的呢? 其實AppClassLoader也有一個父加載器,咱們能夠經過如下代碼獲取
public class Test {
public static void main(String[] args) {
ClassLoader loader = Test.class.getClassLoader();
while (loader != null) {
System.out.println(loader);
loader = loader.getParent();
}
}
}
複製代碼
上述代碼結果以下:
sun.misc.Launcher$AppClassLoader@7565783b
sun.misc.Launcher$ExtClassLoader@1b586d23
複製代碼
至於爲什麼沒有打印出ExtClassLoader的父加載器Bootstrap ClassLoader,這是由於Bootstrap ClassLoader是由C++編寫的,並非一個Java類,所以咱們沒法在Java代碼中獲取它的引用。
上一節咱們看到了ClassLoader的getParent
方法,getParent
獲取到的其實就是其父加載器。這一節將經過源碼,來介紹ClassLoader中的一些重要方法。
ClassLoader類
---------
public final ClassLoader getParent() {
if (parent == null) return null;
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
checkClassLoaderPermission(parent, Reflection.getCallerClass());
}
return parent;
}
複製代碼
咱們能夠看到,其返回值有兩種可能,爲空
或者是parent
變量。
從源碼中還能夠發現其是一個final修飾的方法,咱們知道被final修飾的說明這個方法提供的功能已經知足當前要求,是不能夠重寫的, 因此其各個子類所調用的
getParent()
方法最終都會由ClassLoader來處理。
parent變量又是什麼呢?咱們在查看源碼時能夠發現parent的賦值是在構造方法中。
ClassLoader類
---------
private ClassLoader(Void unused, ClassLoader parent) {
this.parent = parent;
... //省略了無關代碼
}
複製代碼
而此構造方法又是私有的,不能被外部調用,因此其調用者仍是在內部。因而接着查找到了另外兩個構造方法。
ClassLoader類
---------
protected ClassLoader() {
this(checkCreateClassLoader(), getSystemClassLoader());
}
protected ClassLoader(ClassLoader parent) {
this(checkCreateClassLoader(), parent);
}
複製代碼
因此:
getSystemClassLoader()
方法的返回值。接着看上面代碼中的getSystemClassLoader的源碼:
ClassLoader類
---------
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()
方法中發現了對scl變量的賦值。
ClassLoader類
---------
private static synchronized void initSystemClassLoader() {
if (!sclSet) {
if (scl != null)
throw new IllegalStateException("recursive invocation");
sun.misc.Launcher l = sun.misc.Launcher.getLauncher(); //1
if (l != null) {
Throwable oops = null;
scl = l.getClassLoader();
...//省略代碼
}
sclSet = true;
}
}
複製代碼
重點來了,註釋1處其獲取到的是Launcher
類的對象,而後調用了Launcher
類的getClassLoader()
方法。
Launcher類
---------
public ClassLoader getClassLoader() {
return this.loader;
}
複製代碼
那這個this.loader是什麼呢?在Launcher
類中發現,其賦值操做在Launcher
的構造方法中,其值正是Launcher
類中的AppClassLoader:
Launcher類
---------
public Launcher() {
Launcher.ExtClassLoader var1;
try {
var1 = Launcher.ExtClassLoader.getExtClassLoader();
} catch (IOException var10) {
throw new InternalError("Could not create extension class loader", var10);
}
try {
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
} catch (IOException var9) {
throw new InternalError("Could not create application class loader", var9);
}
...
}
複製代碼
到這裏謎團所有解開了:
在建立ClassLoder時,
getSystemClassLoader()
方法的返回值(也就是Launcher
類中的AppClassLoader)做爲其parent。能將class二進制內容轉換成Class對象,若是不符合要求的會拋出異常,例如ClassFormatError
、NoClassDefFoundError
。
在自定義ClassLoader時,咱們一般會先將特定的文件讀取成byte[]對象,再使用此方法,將其轉爲class對象。
ClassLoader類
---------
/** * String name:表示預期的二進制文件名稱,不知道的話,能夠填null。 * byte[] b:此class文件的二進制數據 * int off:class二進制數據開始的位置 * int len:class二進制數據的總長度 */
protected final Class<?> defineClass(String name, byte[] b, int off, int len)
throws ClassFormatError
{
return defineClass(name, b, off, len, null);
}
protected final Class<?> defineClass(String name, byte[] b, int off, int len, ProtectionDomain protectionDomain)
throws ClassFormatError
{
protectionDomain = preDefineClass(name, protectionDomain);
String source = defineClassSourceLocation(protectionDomain);
Class<?> c = defineClass1(name, b, off, len, protectionDomain, source);
postDefineClass(c, protectionDomain);
return c;
}
複製代碼
findClass()
方法通常被loadClass()
方法調用去加載指定名稱類。
ClassLoader類
---------
/** * String name:class文件的名稱 */
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
複製代碼
經過源碼看到ClassLoader類中並無具體的邏輯,而是等待着其子類去實現,經過上面的分析咱們知道兩個系統類加載器ExtClassLoader
和AppClassLoader
都繼承自URLClassLoader
,那就來看一下URLClassLoader
中的具體代碼。
URLClassLoader類
---------
protected Class<?> findClass(final String name) throws ClassNotFoundException
{
final Class<?> result;
try {
result = 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);
}
...
return result;
}
private Class<?> defineClass(String name, Resource res) throws IOException {
...
URL url = res.getCodeSourceURL();
...
java.nio.ByteBuffer bb = res.getByteBuffer();
if (bb != null) {
...
return defineClass(name, bb, cs);
} else {
byte[] b = res.getBytes();
...
return defineClass(name, b, 0, b.length, cs);
}
}
複製代碼
能夠看到其對傳入的name進行處理後,就調用了defineClass(name, res)
;在這個方法裏主要是經過res資源和url,加載出相應格式的文件,最終仍是經過ClassLoader的defineClass
方法加載出具體的類。
上節說到findClass()
通常是在loadClass()
中調用,那loadClass()
是什麼呢? 其實loadClass()
就是雙親委託機制的具體實現,因此在咱們先介紹下雙親委託機制後,再來分析loadClass()
。
先簡單介紹下雙親委託機制: 類加載器查找Class(也就是在loadClass時)所採用的是雙親委託模式,所謂雙親委託模式就是
Bootstrap ClassLoader
。Bootstrap ClassLoader
能夠從%JAVA_HOME%/jre/lib
目錄或者-Xbootclasspath指定目錄查找到,就直接返回該對象,不然就讓ExtClassLoader
去查找。ExtClassLoader
就會從JAVA_HOME/jre/lib/ext
或者-Djava.ext.dir
指定位置中查找,找不到時就交給AppClassLoader
,AppClassLoader
就從當前工程的bin目錄下查找CustomClassLoader
查找,具體查找的結果,就要看咱們怎麼實現自定義ClassLoader的findClass
方法了。接下來咱們看看雙親委託機制在源碼中是如何體現的。 先看loadClass的源碼:
ClassLoader類
---------
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
//首先,根據name檢查類是否已經加載,若已加載,會直接返回
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
//若當前類加載器有父加載器,則調用其父加載器的loadClass()
c = parent.loadClass(name, false);
} else {
//若當前類加載器的parent爲空,則調用findBootstrapClassOrNull()
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
}
if (c == null) {
// 1.若是到這裏c依然爲空的話,表示一直到最頂層的父加載器也沒有找到已加載的c,那就會調用findClass進行查找
// 2.在findClass的過程當中,若是指定目錄下沒有,就會拋出異常ClassNotFoundException
// 3.拋出異常後,此層調用結束,接着其子加載器繼續進行findClass操做
long t1 = System.nanoTime();
c = findClass(name);
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
複製代碼
findBootstrapClassOrNull()方法:能夠看到其對name進行校驗後,最終調用了一個native
方法findBootstrapClass()
。在findBootstrapClass()
方法中最終會用Bootstrap Classloader來查找類。
ClassLoader類
---------
private Class<?> findBootstrapClassOrNull(String name)
{
if (!checkName(name)) return null;
return findBootstrapClass(name);
}
private native Class<?> findBootstrapClass(String name);
複製代碼
不會。 在Java中,咱們用包名+類名做爲一個類的標識。 但在JVM中,一個類用其包名+類名和一個ClassLoader的實例做爲惟一標識,不一樣類加載器加載的類將被置於不一樣的命名空間.
經過一個demo來看,
public class Main {
public static void main(String[] args) {
ClassLoaderTest myClassLoader = new ClassLoaderTest("F:\\");
ClassLoaderTest myClassLoader2 = new ClassLoaderTest("F:\\");
try {
Class c = myClassLoader.loadClass("com.example.Hello");
Class c2 = myClassLoader.loadClass("com.example.Hello");
Class c3 = myClassLoader2.loadClass("com.example.Hello");
System.out.println(c.equals(c2)); //true
System.out.println(c.equals(c3)); //flase
}
}
複製代碼
輸出結果:
true
false
複製代碼
只有兩個類名一致而且被同一個類加載器加載的類,Java虛擬機纔會認爲它們是同一個類。
上面demo中用到的自定義ClassLoader:
自定義的類加載器
注意點:
1.覆寫findClass方法
2.讓其能夠根據name從咱們指定的path中加載文件,也就是將文件正確轉爲byte[]格式
3.使用defineClass方法將byte[]數據轉爲Class對象
-------------
public class ClassLoaderTest extends ClassLoader{
private String path;
public ClassLoaderTest(String path) {
this.path = path;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
Class clazz = null;
byte[] classData = classToBytes(name);
if (classData == null) {
throw new ClassNotFoundException();
} else {
clazz= defineClass(name, classData, 0, classData.length);
}
return clazz;
}
private byte[] classToBytes(String name) {
String fileName = getFileName(name);
File file = new File(path,fileName);
InputStream in=null;
ByteArrayOutputStream out=null;
try {
in = new FileInputStream(file);
out = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int length=0;
while ((length = in.read(buffer)) != -1) {
out.write(buffer, 0, length);
}
return out.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
if(in!=null) {
in.close();
}
} catch (IOException e) {
e.printStackTrace();
}
try{
if(out!=null) {
out.close();
}
}catch (IOException e){
e.printStackTrace();
}
}
return null;
}
private String getFileName(String name) {
int index = name.lastIndexOf('.');
if(index == -1){
return name+".class";
}else{
return name.substring(index+1)+".class";
}
}
}
複製代碼
到此Java的類加載器以及雙親委託機制都講了個大概,若是文中有錯誤的地方、或者有其餘關於類加載器比較重要的內容又沒有介紹到的,歡迎在評論區裏留言,一塊兒交流學習。
下一篇會說道Java new一個對象的過程,其中會涉及到類的加載、驗證,以及對象建立過程當中的堆內存分配等內容。