理解jvm的ClassLoader分析-基礎篇

什麼是ClassLoader

我們都知道,java源碼編譯後,生成的是一個個.class文件,這些類必須要被裝載到jvm才能被運行。ClassLoader就是用來完成這些類的裝載工作。

ClasssLoader加載機制

jvm類加載器分三個層次:

1.引導類加載器(bootstrap class loader):它用來加載 Java 的核心庫,是用原生代碼來實現的,並不繼承自 java.lang.ClassLoader,是Java類加載層次中最頂層的類加載器,負責加載JDK中的核心類庫,如:rt.jar、resources.jar、charsets.jar等,下面通過程序輸出bootstrap加載器加載的核心類。

//打印bootstrap classloader加載了哪些文件
public static void main(String[] args) {
        URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs();
        for (int i = 0; i < urls.length; i++) {
            System.out.println(urls[i].toExternalForm());
        }
    }

運行結果:

file:/home/wzz/bin/jdk1.8.0_101/jre/lib/resources.jar
file:/home/wzz/bin/jdk1.8.0_101/jre/lib/rt.jar
file:/home/wzz/bin/jdk1.8.0_101/jre/lib/sunrsasign.jar
file:/home/wzz/bin/jdk1.8.0_101/jre/lib/jsse.jar
file:/home/wzz/bin/jdk1.8.0_101/jre/lib/jce.jar
file:/home/wzz/bin/jdk1.8.0_101/jre/lib/charsets.jar
file:/home/wzz/bin/jdk1.8.0_101/jre/lib/jfr.jar
file:/home/wzz/bin/jdk1.8.0_101/jre/classes

2.擴展類加載器(extensions class loader):負責加載Java的擴展類庫,默認加載JAVA_HOME/jre/lib/ext/目下的所有jar。該加載器是由sun.misc.LauncherExtClassLoader3.systemclassloaderJavaCLASSPATHJavasun.misc.Launcher AppClassLoader實現。一般來說,Java 應用的類都是由它來完成加載的。可以通過 ClassLoader.getSystemClassLoader()來獲取它。

雙親委託模型

  • 前面說了,java中有三個類加載器,問題就來了,碰到一個類需要加載時,它們之間是如何協調工作的,即java是如何區分一個類該由哪個類加載器來完成呢。 在這裏java採用了委託模型機制,這個機制簡單來講,就是“類裝載器有載入類的需求時,會先請示其Parent使用其搜索路徑幫忙載入,如果Parent 找不到,那麼才由自己依照自己的搜索路徑搜索類”。
    加載器層次圖
  • Java 虛擬機是如何判定兩個 Java 類是相同的的呢?Java 虛擬機不僅要看類的全名是否相同,還要看加載此類的類加載器是否一樣。只有兩者都相同的情況,才認爲兩個類是相同的。即便是同樣的字節代碼,被不同的類加載器加載之後所得到的類,也是不同的。比如一個 Java 類 com.example.Sample,編譯之後生成了字節代碼文件 Sample.class。兩個不同的類加載器 ClassLoaderA和 ClassLoaderB分別讀取了這個 Sample.class文件,並定義出兩個 java.lang.Class類的實例來表示這個類。這兩個實例是不相同的。對於 Java 虛擬機來說,它們是不同的類。試圖對這兩個類的對象進行相互賦值,會拋出運行時異常 ClassCastException。這樣的好處是不同層次的類加載器具有不同優先級,比如所有Java對象的超級父類java.lang.Object,位於rt.jar,無論哪個類加載器加載該類,最終都是由啓動類加載器進行加載,保證安全。

自定義ClassLoader

雖然在絕大多數情況下,系統默認提供的類加載器實現已經可以滿足需求。但是在某些情況下,您還是需要爲應用開發出自己的類加載器。比如您的應用通過網絡來傳輸 Java 類的字節代碼,爲了保證安全性,這些字節代碼經過了加密處理。這個時候您就需要自己的類加載器來從某個網絡地址上讀取加密後的字節代碼,接着進行解密和驗證,最後定義出要在 Java 虛擬機中運行的類來。下面將通過具體的實例來說明類加載器的開發。

package cn.wuzhizhan.study.classloader;

import java.io.IOException;
import java.io.InputStream;

/**
 * 作者: wzz
 * 日期: 16-10-15
 */
public class ClassLoaderDemo {
    public static void main(String[] args) throws Exception {

        ClassLoader clazzLoader = new ClassLoader() {
            @Override
            public Class<?> loadClass(String name) throws ClassNotFoundException {
                try {
                    String clazzName = name.substring(name.lastIndexOf(".") + 1) + ".class";

                    InputStream is = getClass().getResourceAsStream(clazzName);
                    if (is == null) {
                        return super.loadClass(name);
                    }
                    byte[] b = new byte[is.available()];
                    is.read(b);
                    return defineClass(name, b, 0, b.length);
                } catch (IOException e) {
                    throw new ClassNotFoundException(name);
                }
            }
        };

        String currentClass = "cn.wuzhizhan.study.classloader.ClassLoaderDemo";
        Class<?> clazz = clazzLoader.loadClass(currentClass);
        Object obj = clazz.newInstance();

        System.out.println(obj.getClass());
        System.out.println(obj instanceof cn.wuzhizhan.study.classloader.ClassLoaderDemo);
    }
}

運行結果:

class cn.wuzhizhan.study.classloader.ClassLoaderDemo
false

雖然兩個類名字相同,但是由於加載器不同,所以,它們在jvm看來,不是同一個類。這也就是有些情況,明明是看到依賴有這個類,但運行時就是提示找不到類的原因。

線程上下文類加載器

線程上下文類加載器(context class loader)是從 JDK 1.2 開始引入的。類 java.lang.Thread中的方法 getContextClassLoader()和 setContextClassLoader(ClassLoader cl)用來獲取和設置線程的上下文類加載器。如果沒有通過 setContextClassLoader(ClassLoader cl)方法進行設置的話,線程將繼承其父線程的上下文類加載器。Java 應用運行的初始線程的上下文類加載器是系統類加載器。在線程中運行的代碼可以通過此類加載器來加載類和資源。
前面提到的類加載器的代理模式並不能解決 Java 應用開發中會遇到的類加載器的全部問題。Java 提供了很多服務提供者接口(Service Provider Interface,SPI),允許第三方爲這些接口提供實現。常見的 SPI 有 JDBC、JCE、JNDI、JAXP 和 JBI 等。這些 SPI 的接口由 Java 核心庫來提供,如 JAXP 的 SPI 接口定義包含在 javax.xml.parsers包中。這些 SPI 的實現代碼很可能是作爲 Java 應用所依賴的 jar 包被包含進來,可以通過類路徑(CLASSPATH)來找到,如實現了 JAXP SPI 的 Apache Xerces所包含的 jar 包。SPI 接口中的代碼經常需要加載具體的實現類。如 JAXP 中的 javax.xml.parsers.DocumentBuilderFactory類中的 newInstance()方法用來生成一個新的 DocumentBuilderFactory的實例。這裏的實例的真正的類是繼承自 javax.xml.parsers.DocumentBuilderFactory,由 SPI 的實現所提供的。如在 Apache Xerces 中,實現的類是 org.apache.xerces.jaxp.DocumentBuilderFactoryImpl。而問題在於,SPI 的接口是 Java 核心庫的一部分,是由引導類加載器來加載的;SPI 實現的 Java 類一般是由系統類加載器來加載的。引導類加載器是無法找到 SPI 的實現類的,因爲它只加載 Java 的核心庫。它也不能代理給系統類加載器,因爲它是系統類加載器的祖先類加載器。也就是說,類加載器的代理模式無法解決這個問題。
線程上下文類加載器正好解決了這個問題。如果不做任何的設置,Java 應用的線程的上下文類加載器默認就是系統上下文類加載器。在 SPI 接口的代碼中使用線程上下文類加載器,就可以成功的加載到 SPI 實現的類。線程上下文類加載器在很多 SPI 的實現中都會用到。

Class.forName

Class.forName是一個靜態方法,同樣可以用來加載類。該方法有兩種形式:Class.forName(String name, boolean initialize, ClassLoader loader)和 Class.forName(String className)。第一種形式的參數 name表示的是類的全名;initialize表示是否初始化類;loader表示加載時使用的類加載器。第二種形式則相當於設置了參數 initialize的值爲 true,loader的值爲當前類的類加載器。Class.forName的一個很常見的用法是在加載數據庫驅動的時候。如 Class.forName(“org.apache.derby.jdbc.EmbeddedDriver”).newInstance()用來加載 Apache Derby 數據庫的驅動。

回顧

本文主要介紹了jvm類加載器的三個層次、雙親委託代理、以及實現自定義加載器。
參考:
http://blog.csdn.net/xyang81/article/details/7292380
http://blog.jobbole.com/96145/
https://www.ibm.com/developerworks/cn/java/j-lo-classloader/
http://gityuan.com/2016/01/24/java-classloader/

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章