深刻分析Java類加載器原理

本文分析了雙親委派模型的實現原理,並經過代碼示例說明了何時須要實現本身的類加載器以及如何實現本身的類加載器。java

本文基於JDK8。web

0 ClassLoader的做用

ClassLoader用於將class文件加載到JVM中。另一個做用是確認每一個類應該由哪一個類加載器加載。bash

第二個做用也用於判斷JVM運行時的兩個類是否相等,影響的判斷方法有equals()、isAssignableFrom()、isInstance()以及instanceof關鍵字,這一點在後文中會舉例說明。微信

0.1 什麼時候出發類加載動做?

類加載的觸發能夠分爲隱式加載和顯示加載。網絡

隱式加載框架

隱式加載包括如下幾種狀況:dom

  • 遇到new、getstatic、putstatic、invokestatic這4條字節碼指令時
  • 對類進行反射調用時
  • 當初始化一個類時,若是其父類尚未初始化,優先加載其父類並初始化
  • 虛擬機啓動時,需指定一個包含main函數的主類,優先加載並初始化這個主類

顯示加載函數

顯示加載包含如下幾種狀況:post

  • 經過ClassLoader的loadClass方法
  • 經過Class.forName
  • 經過ClassLoader的findClass方法

0.2 被加載的類存放在哪裏

JDK8以前會加載到內存中的方法區。 從JDK8到如今爲止,會加載到元數據區。ui

1 都有哪些ClassLoader

整個JVM平臺提供三類ClassLoader。

1.1 Bootstrap ClassLoader

加載JVM自身工做須要的類,它由JVM本身實現。它會加載$JAVA_HOME/jre/lib下的文件

1.2 ExtClassLoader

它是JVM的一部分,由sun.misc.LauncherExtClassLoader實現,他會加載JAVA_HOME/jre/lib/ext目錄中的文件(或由System.getProperty("java.ext.dirs")所指定的文件)。

1.3 AppClassLoader

應用類加載器,咱們工做中接觸最多的也是這個類加載器,它由sun.misc.Launcher$AppClassLoader實現。它加載由System.getProperty("java.class.path")指定目錄下的文件,也就是咱們一般說的classpath路徑。

2 雙親委派模型

2.1 雙親委派模型原理

從JDK1.2以後,類加載器引入了雙親委派模型,其模型圖以下:

ClassLoaderParentMod

其中,兩個用戶自定義類加載器的父加載器是AppClassLoader,AppClassLoader的父加載器是ExtClassLoader,ExtClassLoader是沒有父類加載器的,在代碼中,ExtClassLoader的父類加載器爲null。BootstrapClassLoader也並無子類,由於他徹底由JVM實現。

雙親委派模型的原理是:當一個類加載器接收到類加載請求時,首先會請求其父類加載器加載,每一層都是如此,當父類加載器沒法找到這個類時(根據類的全限定名稱),子類加載器纔會嘗試本身去加載。

爲了說明這個繼承關係,我這裏實現了一個本身的類加載器,名爲TestClassLoader,在類加載器中,用parent字段來表示當前加載器的父類加載器,其定義以下:

public abstract class ClassLoader {
...
    // The parent class loader for delegation
    // Note: VM hardcoded the offset of this field, thus all new fields
    // must be added *after* it.
    private final ClassLoader parent;
...
}
複製代碼

而後經過debug來看一下這個結構,以下圖

ClassLoader委派模型關係圖

這裏的第一個紅框是我本身定義的類加載器,對應上圖的最下層部分;第二個框是自定義類加載器的父類加載器,能夠看到是AppClassLoader;第三個框是AppClassLoader的父類加載器,是ExtClassLaoder;第四個框是ExtClassLoader的父類加載器,是null。

OK,這裏先有個直觀的印象,後面的實現原理中會詳細介紹。

2.2 此模型解決的問題

爲何要使用雙親委派模型呢?它能夠解決什麼問題呢?

雙親委派模型是JDK1.2以後引入的。根據雙親委派模型原理,能夠試想,沒有雙親委派模型時,若是用戶本身寫了一個全限定名爲java.lang.Object的類,並用本身的類加載器去加載,同時BootstrapClassLoader加載了rt.jar包中的JDK自己的java.lang.Object,這樣內存中就存在兩份Object類了,此時就會出現不少問題,例如根據全限定名沒法定位到具體的類。

有了雙親委派模型後,全部的類加載操做都會優先委派給父類加載器,這樣一來,即便用戶自定義了一個java.lang.Object,但因爲BootstrapClassLoader已經檢測到本身加載了這個類,用戶自定義的類加載器就不會再重複加載了。

因此,雙親委派模型可以保證類在內存中的惟一性。

2.3 雙親委派模型實現原理

下面從源碼的角度看一下雙親委派模型的實現。

JVM在加載一個class時會先調用classloader的loadClassInternal方法,該方法源碼以下

// This method is invoked by the virtual machine to load a class.
private Class<?> loadClassInternal(String name)
    throws ClassNotFoundException
{
    // For backward compatibility, explicitly lock on 'this' when
    // the current class loader is not parallel capable.
    if (parallelLockMap == null) {
        synchronized (this) {
             return loadClass(name);
        }
    } else {
        return loadClass(name);
    }
}
複製代碼

該方法裏面作的事兒就是調用了loadClass方法,loadClass方法的實現以下

protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        // First, check if the class has already been loaded
        // 先查看這個類是否已經被本身加載了
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                // 若是有父類加載器,先委派給父類加載器來加載
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
                    // 若是父類加載器爲null,說明ExtClassLoader也沒有找到目標類,則調用BootstrapClassLoader來查找
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // ClassNotFoundException thrown if class not found
                // from the non-null parent class loader
            }
            // 若是都沒有找到,調用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;
    }
}
複製代碼

源碼中已經給出了幾個關鍵步驟的說明。

代碼中調用BootstrapClassLoader的地方實際是調用的native方法。

因而可知,雙親委派模型實現的核心就是這個loadClass方法。

3 實現本身的類加載器

3.1 爲何要實現本身的類加載器

回答這個問題首先要思考類加載器有什麼做用(粗體標出)。

3.1.1 類加載器的做用

類加載器有啥做用呢?咱們再回到上面的源碼。

從上文咱們知道JVM經過loadClass方法來查找類,因此,他的第一個做用也是最重要的:在指定的路徑下查找class文件(各個類加載器的掃描路徑在上文已經給出)。

而後,當父類加載器都說沒有加載過目標類時,他會嘗試本身加載目標類,這就調用了findClass方法,能夠看一下findClass方法的定義:

protected Class<?> findClass(String name) throws ClassNotFoundException {
    throw new ClassNotFoundException(name);
}
複製代碼

能夠發現他要求返回一個Class對象實例,這裏我經過一個實現類sun.rmi.rmic.iiop.ClassPathLoader來講明一下findClass都幹了什麼。

protected Class findClass(String var1) throws ClassNotFoundException {
    // 從指定路徑加載指定名稱的class的字節流
    byte[] var2 = this.loadClassData(var1);
    // 經過ClassLoader的defineClass來建立class對象實例
    return this.defineClass(var1, var2, 0, var2.length);
}
複製代碼

他作的事情在註釋中已經給出,能夠看到,最終是經過defineClass方法來實例化class對象的。

另外能夠發現,class文件字節的獲取和處理咱們是能夠控制的。因此,第二個做用:咱們能夠在字節流解析這一步作一些自定義的處理。 例如,加解密。

接下來,看似還有個defineClass可讓咱們來作點兒什麼,ClassLoader的實現以下:

protected final Class<?> defineClass(String name, byte[] b, int off, int len)
    throws ClassFormatError
{
    return defineClass(name, b, off, len, null);
}
複製代碼

被final掉了,沒辦法覆寫,因此這裏看似不能作什麼事兒了。

小結一下:

  • 經過loadClass在指定的路徑下查找文件。
  • 經過findClass方法解析class字節流,並實例化class對象。
3.1.2 何時須要本身實現類加載器

當JDK提供的類加載器實現沒法知足咱們的需求時,才須要本身實現類加載器。

現有應用場景:OSGi、代碼熱部署等領域。

另外,根據上述類加載器的做用,可能有如下幾個場景須要本身實現類加載器

  • 當須要在自定義的目錄中查找class文件時(或網絡獲取)
  • class被類加載器加載前的加解密(代碼加密領域)

3.2 如何實現本身的類加載器

接下來,實現一個在自定義class類路徑中查找並加載class的自定義類加載器。

package com.lordx.sprintbootdemo.classloader;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;

/**
 * 自定義ClassLoader
 * 功能:可自定義class文件的掃描路徑
 * @author zhiminxu 
 */
// 繼承ClassLoader,獲取基礎功能
public class TestClassLoader extends ClassLoader {

    // 自定義的class掃描路徑
    private String classPath;

    public TestClassLoader(String classPath) {
        this.classPath = classPath;
    }

    // 覆寫ClassLoader的findClass方法
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        // getDate方法會根據自定義的路徑掃描class,並返回class的字節
        byte[] classData = getDate(name);
        if (classData == null) {
            throw new ClassNotFoundException();
        } else {
            // 生成class實例
            return defineClass(name, classData, 0, classData.length);
        }
    }


    private byte[] getDate(String name) {
        // 拼接目標class文件路徑
        String path = classPath + File.separatorChar + name.replace('.', File.separatorChar) + ".class";
        try {
            InputStream is = new FileInputStream(path);
            ByteArrayOutputStream stream = new ByteArrayOutputStream();
            byte[] buffer = new byte[2048];
            int num = 0;
            while ((num = is.read(buffer)) != -1) {
                stream.write(buffer, 0 ,num);
            }
            return stream.toByteArray();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
}
複製代碼

使用自定義的類加載器

package com.lordx.sprintbootdemo.classloader;

public class MyClassLoader {
    public static void main(String[] args) throws ClassNotFoundException {
        // 自定義class類路徑
        String classPath = "/Users/zhiminxu/developer/classloader";
        // 自定義的類加載器實現:TestClassLoader
        TestClassLoader testClassLoader = new TestClassLoader(classPath);
        // 經過自定義類加載器加載
        Class<?> object = testClassLoader.loadClass("ClassLoaderTest");
        // 這裏的打印應該是咱們自定義的類加載器:TestClassLoader
        System.out.println(object.getClassLoader());
    }
}
複製代碼

實驗中的ClassLoaderTest類就是一個簡單的定義了兩個field的Class。以下圖所示

classloadertest

最終的打印結果以下

Result

PS: 實驗類(ClassLoaderTest)最好不要放在IDE的工程目錄內,由於IDE在run的時候會先將工程中的全部類都加載到內存,這樣一來這個類就不是自定義類加載器加載的了,而是AppClassLoader加載的。

3.3 類加載器對「相等」判斷的影響

3.3.1 對Object.equals()的影響

仍是上面那個自定義類加載器

修改MyClassLoader代碼

package com.lordx.sprintbootdemo.classloader;

public class MyClassLoader {
    public static void main(String[] args) throws ClassNotFoundException {
        // 自定義class類路徑
        String classPath = "/Users/zhiminxu/developer/classloader";
        // 自定義的類加載器實現:TestClassLoader
        TestClassLoader testClassLoader = new TestClassLoader(classPath);
        // 經過自定義類加載器加載
        Class<?> object1 = testClassLoader.loadClass("ClassLoaderTest");
        Class<?> object2 = testClassLoader.loadClass("ClassLoaderTest");
        // object1和object2使用同一個類加載器加載時
        System.out.println(object1.equals(object2));

        // 新定義一個類加載器
        TestClassLoader testClassLoader2 = new TestClassLoader(classPath);
        Class<?> object3 = testClassLoader2.loadClass("ClassLoaderTest");
        // object1和object3使用不一樣類加載器加載時
        System.out.println(object1.equals(object3));
    }
}

複製代碼

打印結果:

true

false

equals方法默認比較的是內存地址,因而可知,不一樣的類加載器實例都有本身的內存空間,即便類的全限定名相同,但類加載器不一樣也是不行的。

因此,內存中兩個類equals爲true的條件是用同一個類加載器實例加載的全限定名相同的兩個類實例。

3.3.2 對instanceof的影響

修改TestClassLoader,增長main方法來實驗,修改後的TestClassLoader以下:

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;

/**
 * 自定義ClassLoader
 * 功能:可自定義class文件的掃描路徑
 * @author zhiminxu
 */
// 繼承ClassLoader,獲取基礎功能
public class TestClassLoader extends ClassLoader {

    public static void main(String[] args) throws ClassNotFoundException {
        TestClassLoader testClassLoader = new TestClassLoader("/Users/zhiminxu/developer/classloader");
        Object obj = testClassLoader.loadClass("ClassLoaderTest");
        // obj是testClassLoader加載的,ClassLoaderTest是AppClassLoader加載的,因此這裏打印:false
        System.out.println(obj instanceof ClassLoaderTest);
    }

    // 自定義的class掃描路徑
    private String classPath;

    public TestClassLoader(String classPath) {
        this.classPath = classPath;
    }

    // 覆寫ClassLoader的findClass方法
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        // getDate方法會根據自定義的路徑掃描class,並返回class的字節
        byte[] classData = getDate(name);
        if (classData == null) {
            throw new ClassNotFoundException();
        } else {
            // 生成class實例
            return defineClass(name, classData, 0, classData.length);
        }
    }


    private byte[] getDate(String name) {
        // 拼接目標class文件路徑
        String path = classPath + File.separatorChar + name.replace('.', File.separatorChar) + ".class";
        try {
            InputStream is = new FileInputStream(path);
            ByteArrayOutputStream stream = new ByteArrayOutputStream();
            byte[] buffer = new byte[2048];
            int num = 0;
            while ((num = is.read(buffer)) != -1) {
                stream.write(buffer, 0 ,num);
            }
            return stream.toByteArray();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
}
複製代碼

打印結果:

false

其餘的還會影響Class的isAssignableFrom()方法和isInstance()方法,緣由與上面的相同。

3.3.3 另外的說明

不要嘗試自定義java.lang包,並嘗試用加載器去加載他們。像下面這樣

selfjavalang

這麼幹的話,會直接拋出一個異常

error

這個異常是在調用defineClass的校驗過程拋出的,源碼以下

// Note:  Checking logic in java.lang.invoke.MemberName.checkForTypeAlias
// relies on the fact that spoofing is impossible if a class has a name
// of the form "java.*"
if ((name != null) && name.startsWith("java.")) {
    throw new SecurityException
        ("Prohibited package name: " +
         name.substring(0, name.lastIndexOf('.')));
}
複製代碼

4 跟ClassLoader相關的幾個異常

想知道以下幾個異常在什麼狀況下拋出,其實只須要在ClassLoader中找到哪裏會拋出他,而後在看下相關邏輯便可。

4.1 ClassNotFoundException

這個異常,相信你們常常遇到。

那麼,到底啥緣由致使拋出這個異常呢?

看一下ClassLoader的源碼,在JVM調用loadClassInternal的方法中,就會拋出這個異常。

其聲明以下:

// This method is invoked by the virtual machine to load a class.
private Class<?> loadClassInternal(String name)
    throws ClassNotFoundException
{
    // For backward compatibility, explicitly lock on 'this' when
    // the current class loader is not parallel capable.
    if (parallelLockMap == null) {
        synchronized (this) {
             return loadClass(name);
        }
    } else {
        return loadClass(name);
    }
}
複製代碼

這裏面loadClass方法會拋出這個異常,再來看loadClass方法

public Class<?> loadClass(String name) throws ClassNotFoundException {
    return loadClass(name, false);
}
複製代碼

調用了重寫的loadClass方法

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        // First, check if the class has already been loaded
        // 查看findLoadedClass的聲明,沒有拋出這個異常
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                if (parent != null) {
                    // 這裏會拋,但一樣是調loadClass方法,無需關注
                    c = parent.loadClass(name, false);
                } else {
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // catch住沒有拋,由於要在下面嘗試本身獲取class
                // ClassNotFoundException thrown if class not found
                // from the non-null parent class loader
            }

            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;
    }
}
複製代碼

再來看findClass方法

/**
 * Finds the class with the specified <a href="#name">binary name</a>.
 * This method should be overridden by class loader implementations that
 * follow the delegation model for loading classes, and will be invoked by
 * the {@link #loadClass <tt>loadClass</tt>} method after checking the
 * parent class loader for the requested class.  The default implementation
 * throws a <tt>ClassNotFoundException</tt>.
 *
 * @param  name
 *         The <a href="#name">binary name</a> of the class
 *
 * @return  The resulting <tt>Class</tt> object
 *
 * @throws  ClassNotFoundException
 *          If the class could not be found
 *
 * @since  1.2
 */
protected Class<?> findClass(String name) throws ClassNotFoundException {
    throw new ClassNotFoundException(name);
}
複製代碼

果真這裏拋出的,註釋中拋出這個異常的緣由是說:當這個class沒法被找到時拋出。

這也就是爲何在上面自定義的類加載器中,覆寫findClass方法時,若是沒有找到class要拋出這個異常的緣由。

至此,這個異常拋出的緣由就明確了:在雙親委派模型的全部相關類加載器中,目標類在每一個類加載器的掃描路徑中都不存在時,會拋出這個異常。

4.2 NoClassDefFoundError

這也是個常常會碰到的異常,並且不熟悉的同窗可能常常搞不清楚何時拋出ClassNotFoundException,何時拋出NoClassDefFoundError。

咱們仍是到ClassLoader中搜一下這個異常,能夠發如今defineClass方法中可能拋出這個異常,defineClass方法源碼以下:

/**
 * ... 忽略註釋和參數以及返回值的說明,直接看異常聲明
 * 
 * @throws  ClassFormatError
 *          If the data did not contain a valid class
 *
 * 在這裏。因爲NoClassDefFoundError是Error下的,因此不用顯示throws
 * @throws  NoClassDefFoundError
 *          If <tt>name</tt> is not equal to the <a href="#name">binary
 *          name</a> of the class specified by <tt>b</tt>
 *
 * @throws  IndexOutOfBoundsException
 *          If either <tt>off</tt> or <tt>len</tt> is negative, or if
 *          <tt>off+len</tt> is greater than <tt>b.length</tt>.
 *
 * @throws  SecurityException
 *          If an attempt is made to add this class to a package that
 *          contains classes that were signed by a different set of
 *          certificates than this class, or if <tt>name</tt> begins with
 *          "<tt>java.</tt>".
 */
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;
}
複製代碼

繼續追蹤裏面的方法,能夠發現是preDefineClass方法拋出的,這個方法源碼以下:

/* Determine protection domain, and check that:
    - not define java.* class,
    - signer of this class matches signers for the rest of the classes in
      package.
*/
private ProtectionDomain preDefineClass(String name,
                                        ProtectionDomain pd)
{
    // 這裏顯示拋出,可發現是在目標類名校驗不經過時拋出的
    if (!checkName(name))
        throw new NoClassDefFoundError("IllegalName: " + name);

    // Note:  Checking logic in java.lang.invoke.MemberName.checkForTypeAlias
    // relies on the fact that spoofing is impossible if a class has a name
    // of the form "java.*"
    if ((name != null) && name.startsWith("java.")) {
        throw new SecurityException
            ("Prohibited package name: " +
             name.substring(0, name.lastIndexOf('.')));
    }
    if (pd == null) {
        pd = defaultDomain;
    }

    if (name != null) checkCerts(name, pd.getCodeSource());

    return pd;
}
複製代碼

這個校驗的源碼以下

// true if the name is null or has the potential to be a valid binary name
private boolean checkName(String name) {
    if ((name == null) || (name.length() == 0))
        return true;
    if ((name.indexOf('/') != -1)
        || (!VM.allowArraySyntax() && (name.charAt(0) == '[')))
        return false;
    return true;
}
複製代碼

因此,這個異常拋出的緣由是:類名校驗未經過。

但這個異常其實不止在ClassLoader中拋出,其餘地方,例如框架中,web容器中都有可能拋出,還要具體問題具體分析。

5 總結

本文先介紹了ClassLoader的做用:主要用於從指定路徑查找class並加載到內存,另外是判斷兩個類是否相等。

後面介紹了雙親委派模型,以及其實現原理,JDK中主要是在ClassLoader的loadClass方法中實現雙親委派模型的。

而後代碼示例說明了如何實現本身的類加載器,以及類加載器的使用場景。

最後說明了在工做中常遇到的兩個與類加載器相關的異常。


歡迎關注個人微信公衆號

公衆號
相關文章
相關標籤/搜索