Java類載入知識總結

JavaDog發表於2019-02-25

Java類載入器的作用就是在執行時載入類。它通過載入class檔案、網路上的位元組流或其他來源構造Class物件,用於生成物件在程式中執行。

在平時的程式開發中,我們一般不需要操作類載入,因為Java本身的類載入器機制已經幫我們做了很多事情。但後面很多時候,比如說自己開發框架或者排查問題的時候,我們需要理解類載入的機制和如何按自己的需求去自定義類載入器。類載入器知識就像Java開發的一道門,門內外隔離了開發人員對類載入的使用,而瞭解類載入器就掌握了這道門的鑰匙。

什麼是類載入器

類載入器是一個用來載入類檔案的類。Java原始碼通過編譯器編譯後,類載入器載入檔案中的位元組碼來執行程式,位元組碼的來源也可以來自於網路。Java有3中預設的類載入器:Bootstrap類載入器、Extension類載入器和System類載入器(或者叫做Application類載入器)。每個類載入器都設定好從哪裡載入類。

Bootstrap類載入器:JRE/lib/rt.jar中的JDK類檔案
Bootstrap類載入器是所有類載入器的父類(非類繼承關係)。它的大部分由C來寫的,通過程式碼獲取不到,比如呼叫String.class.getClassLoader(),會返回null。

Extension類載入器:JRE/lib/ext或者java.ext.dirs指向的目錄
Extension載入器由sun.misc.Launcher$ExtClassLoader實現 System類載入器:CLASSPATH環境變數,由-classpath-cp選項定義,或者是JAR中的Manifest的classpath屬性定義。 System類載入器由sun.misc.Launcher$AppClassLoader實現。

類載入器的工作原理

Java的類載入性保證了很好的穩定性和擴充性。類載入器的工作原理基於三個機制

委託機制

某載入器在嘗試載入類的時候,都會委託其父類載入器嘗試載入類。比如一個應用要載入CLASSPAH下A.class。載入這個類的請求由System類載入器委託父載入器Extension類載入器,Extension類載入器會委託父載入器Bootstrap類載入器。Bootstrap類載入器先從rt.jar中嘗試載入這個類。這個類在rt.jar中不存在,所以載入工作回到Extension類載入器。Extension類載入器會檢視jre/lib/ext目錄下有沒有這個類,如果存在那麼這個類將被載入,且只載入一次。如果沒找到,載入請求有回到了System類載入器。System類載入器從CLASPATH中檢視該類,如果存在則載入這個類,如果沒找到,則報java.lang.ClassNotFoundException。

可見性機制

子類載入器可以看到父類載入器載入的類,而反之則不行。

單一性機制

父載入器載入過的類不能被子載入器載入第二次,同一個類載入器例項也只能載入一個類一次。

如何載入類

我們可以使用顯示呼叫的方式或者交由JVM隱式載入一個類。在類載入的過程中,一般有三個概念上的類載入器提供使用。
CurrentClassLoader,稱之為當前類載入器
SpecificClassLoader, 稱之為指定的類載入器。值得是一個特定的ClassLoader示例
ThreadContextClassLoader,稱之為執行緒上下文類載入器。每個執行緒都會擁有一個ClassLoader引用,而且可以通過Thread.currentThread().setContextClassLoader(ClassLoader classLoader)進行切換
其中CurrentClassLoader的載入過程是JVM執行時候控制的,非顯示呼叫。

顯示載入

Class.forName和ClassLoader.loadClass都可以用來進行類載入。比如

Class.forName("com.xiaoyi.pandora.vo.A");
Class.forName("com.xiaoyi.pandora.vo.A", true, customClassLoader1);
customClassLoader1.loadClass("com.xiaoyi.pandora.vo.A");
複製程式碼

Class.forName的方式其實是使用了CurrentClassLoader這種方式,本質上還是找到一個類載入器去執行ClassLoader.loadClass動作。

@CallerSensitive
public static Class<?> forName(String className)
            throws ClassNotFoundException {
    Class<?> caller = Reflection.getCallerClass();
    return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
}
複製程式碼

從程式碼中可以看出,利用反射的機制獲取執行方法的類示例caller,從而找到載入caller對應類的類載入器。從而將載入的動作交由這個類載入去執行。
類名,類載入器,類示例這三者的關係是緊密相連的。這裡要提到一個資料結構來儲存Java載入類過程中這三者的關係。SystemDictionary(系統字典)。
image.png
SystemDictionary以類名和類載入器例項作為一個key,Class物件引用為value。Class物件能過找到它的類載入器,類名和類加器例項對應一個唯一的Class物件。
所以,Class.forName的呼叫方式是先從SystemDictionary獲取當前類載入器,然後以ClassLoader.loadClass的方式去載入一個類。

隱式載入

大多數情況下,程式中的類載入都是通過隱式載入的形式。不需要顯示呼叫ClassLoader物件去載入類。 我們看下面一個很普通的場景。為了顯示實際效果,這裡自定義了一個類載入器去顯示類的載入動作。

public class A {
    B b;
    public A() {
        this.b = new B();
    }
    public void show() {
        System.out.println("b's classLoader is " + b.getClass().getClassLoader());
    }
}
複製程式碼

在載入A.class後,如果例項化類A物件,JVM會自動幫我們利用類載入器幫我們載入B類。

String classPath = "/Users/xiaoyi/work";
        CustomClassLoader customClassLoader1 = new CustomClassLoader(classPath, "xiao");
Class aClazz = Class.forName("com.xiaoyi.pandora.vo.A", true, customClassLoader1);
Object a = aClazz.newInstance();
複製程式碼

控制檯的輸出如下

xiao classLoader start load class :com.xiaoyi.pandora.vo.A
xiao classLoader start load class :com.xiaoyi.pandora.vo.B
複製程式碼

在不執行Object a = aClazz.newInstance();這條語句的時候,控制檯不會輸出xiao classLoader start load class :com.xiaoyi.pandora.vo.B。
總結:在載入B類時,JVM會隱式獲取載入A類的類加器去執行載入B類的工作。

類載入過程

java.lang.ClassLoader中Class<?> loadClass(String name, boolean resolve)方法介紹了詳細的類載入過程。類載入中重要的四個方法,loadClass是下面1,2,3方法的入口
1、Class<?> findLoadedClass(String name)
判斷類是否已經被載入過。即從SystemDictionary中根據型別和當前類載入器作為key,檢視是否能找到對應的Class
2、Class<?> findClass(String name)
交給子類載入器的擴充
3、void resolveClass(Class<?> c)
將類名,當前類載入器,類示例物件關聯起來,儲存在SystemDictionary中。Java中規範在一個類被使用之前,必須做關聯(Before the class can be used it must be resolved)
4、Class<?> defineClass(String name, byte[] b, int off, int len)
根據類檔案或者網路上的位元組流,轉化為一個Class的例項。常用於自定義類載入器。
java.lang.ClassLoader中Class<?> loadClass(String name, boolean resolve)的程式碼如下(省略了非核心流程的程式碼)

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
       // 獲取鎖,類的載入過程是執行緒安全的
        synchronized (getClassLoadingLock(name)) {
           // 檢視是否已經被載入過
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                       // 如果存在父載入器,委託父載入器去嘗試載入
                        c = parent.loadClass(name, false);
                    } else {
                       // 委託Bootstrap類載入器去嘗試載入
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                }

                if (c == null) {
                    // 類載入器本身去載入類
                    c = findClass(name);  
                }
            }
            if (resolve) {
                // 關聯類和類載入器的關係
                resolveClass(c);
            }
            return c;
        }
    }
複製程式碼

一些類載入器的使用場景

自定義類載入器

自定義類載入器是使用類載入器一個基本場景。關鍵的方法在前面“類載入過程”已經給出。主要有2點
1、繼承ClassLoader
2、重寫findClass方法,實現獲取類的位元組流轉化為類例項返回
一個簡單的自定義類載入器如下

public class CustomClassLoader extends ClassLoader {
    private String name;
    private String classPath;

    public CustomClassLoader(String classPath, String name) {
        this.name = name;
        this.classPath = classPath;
    }

    @Override
    public Class<?> findClass(String name) throws ClassNotFoundException {
        System.out.println(this.name + " classLoader start load class :"  + name);
        try {
            byte[] classData = getData(name);
            if(classData != null) {
                return defineClass(name, classData, 0, classData.length);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return super.findClass(name);
    }

    private byte[] getData(String className) throws IOException {
        InputStream in = null;
        ByteArrayOutputStream out = null;
        String path = classPath + File.separatorChar + className.replace('.', File.separatorChar) + ".class";
        try {
            in = new FileInputStream(path);
            out = new ByteArrayOutputStream();
            byte[] buffer = new byte[2048];
            int length = 0;

            while ((length = in.read(buffer)) != -1) {
                out.write(buffer, 0, length);
            }
            return out.toByteArray();
        } catch (Exception e) {

        } finally {
            if(in != null) {
                in.close();
            }
            if(out != null) {
                out.close();
            }

        }
        return null;
    }
}
複製程式碼

應用隔離

從SystemDictionary資料結構中,一個類名和類載入器例項,對應一個Class例項。也就是說一個類檔案,可以交由不同的類載入器去生成不同的類例項,這些類例項是可以在JVM中並存的。但是如果在對類載入器的訪問上做好隔離,這些類例項在JVM中是可以實現隔離的。具體可以參考Tomcat容器如何利用WebappClassLoader實現應用上的隔離的技術相關文件或者Pandora的隔離實現原理。

SPI

SPI全稱是Service Provider Interface, 是JDK內建的一種服務提供發現機制。是一種動態替換髮現機制。舉個例子:有個介面想在執行時才發現具體的實現類,那麼你只需在程式執行前新增一個實現即可。常見的SPI有JDBC、JCE、JNDI、JAXP等。
這些SPI的介面是由Java核心庫來提供,而SPI的實現則是作為第二方jar包被包含進類路徑(classpath)中。以JDBC的mysql為例子,呼叫程式碼如下所示

Connection connection = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/mydb", "root", "root");
複製程式碼

com.mysql.jdbc.Driver中的程式碼如下,在類被載入後,向DriverManager中註冊mysql的Driver。

public class Driver extends NonRegisteringDriver implements java.sql.Driver {
    public Driver() throws SQLException {
    }

    static {
        try {
            DriverManager.registerDriver(new Driver());
        } catch (SQLException var1) {
            throw new RuntimeException("Can\'t register driver!");
        }
    }
}
複製程式碼

下面來分析一下其中的類載入過程
1、隱式方式載入DriverManager類,因為DriverManager在JDK的rt.jar中,這是對應的類載入器是Bootstrap類載入器
2、DriverManager類載入後,執行靜態程式碼塊,初始化java.sql.Driver的實現類。按照SPI的規範,ServiceLoader會去/META-INF/services下面查詢java.sql.Driver資源
image.png
3、ServiceLoader獲取ThreadContextClassLoader,構造ServiceLoader例項,並將ThreadContextClassLoader賦給ServiceLoader例項。這時候的類載入器是System類載入器。

public static <S> ServiceLoader<S> load(Class<S> service) {
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        return ServiceLoader.load(service, cl);
}
public static <S> ServiceLoader<S> load(Class<S> service,
                                            ClassLoader loader)
    {
        return new ServiceLoader<>(service, loader);
    }
複製程式碼

4、DriverManager利用ServiceLoader去獲取/META-INF/services下的java.sql.Driver檔案,載入對應的實現類,並初始化具體的Driver類。這時候的類載入器是第3步的System類載入器,它能夠載入classpath下面的類。

ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
try{
    while(driversIterator.hasNext()) {
         driversIterator.next();
     }
} catch(Throwable t) {
     // Do nothing
}
複製程式碼
//driversIterator在呼叫next方法時,載入驅動類
Class<?> c = null;
try {
    c = Class.forName(cn, false, loader);
} catch (ClassNotFoundException x) {
      fail(service, "Provider " + cn + " not found");
}
S p = service.cast(c.newInstance());
providers.put(cn, p);
return p;
複製程式碼

5、Driver類載入例項化後,會執行靜態程式碼將自己註冊到DriverManager中

SPI的實現方式其實是破壞了類載入器的委託機制,在類載入的過程中,Bootstrap類載入在獲取不到具體的SPI Provider實現類的情況下,委託ThreadContextLoader去載入classpath下的實現類。

Java類載入知識總結


相關文章