Java中類加載器classloader淺析

本篇文章來簡析一下 classloader 在 Java 中的應用。java

前言

如今通常一個應用程序開發會包含不少不少的類, Java 程序啓動時並非一次性將全部的類所有加載到內存中進行運行的,而是先加載部分的類到 JVM 中,而後等 JVM 須要用到其餘的類時再加載進去,這樣的好處就是節約內存,提升了效率。c++

在 Java 中類加載器就是 ClassLoader , ClassLoader 的具體做用就是將 class 文件加載到 jvm 虛擬機中去,程序就能夠正確運行了。算法

Class 再認識

咱們日常寫的 Java 文件的格式是 xxx.java 文件格式的,這個格式並非 JVM 執行的格式, JVM 執行的是 .class 格式的文件,這就須要將 .java 的格式文件轉爲 .class 的格式,這就是編譯過程。命令是:安全

javac HelloWorld.java
複製代碼

經過 javac 命令便可在當前目錄下面生成 .class 文件,這個文件就是 JVM 可以執行的文件格式。bash

在這裏插入圖片描述

接着經過 java 命令便可運行這個文件:jvm

java HelloWorld
複製代碼

打印結果以下所示: ide

在這裏插入圖片描述
以上即是一個完整的 Java 文件運行過程,先通過編譯將 .java 文件轉爲 .class 格式以便 JVM可以執行。接下來咱們來詳細分析。

Java 類加載器

ClassLoader 分類

每一個 ClassLoader 對象都是一個 java.lang.ClassLoader 的實例。每一個Class對象都被這些 ClassLoader 對象所加載,經過繼承java.lang.ClassLoader 能夠擴展出自定義 ClassLoader,並使用這些自定義的 ClassLoader 對類進行加載。學習

package java.lang;
public abstract class ClassLoader {
	 public Class loadClass(String name);
	 protected Class defineClass(byte[] b);
	 public URL getResource(String name);
	 public Enumeration getResources(String name);
	 public ClassLoader getParent();
	 Class<?> findClass(String name)
     //...
}
複製代碼

1,loadClass

它接受一個全類名,而後返回一個 Class 類型的實例。測試

2,defineClass

方法接受一組字節,而後將其具體化爲一個 Class 類型實例,它通常從磁盤上加載一個文件,而後將文件的字節傳遞給 JVM ,經過 JVM ( native 方法)對於 Class 的定義,將其具體化,實例化爲一個Class 類型實例。ui

3,getParent

返回其parent ClassLoader

咱們經過實際demo來測試一下。

package demo;
public class Test {
    public static void main(String[] args) {
        ClassLoader classLoader = Test.class.getClassLoader();
        System.out.println(classLoader);
        ClassLoader classLoader1 = classLoader.getParent();
        System.out.println(classLoader1);
        ClassLoader classLoader2 = classLoader1.getParent();
        System.out.println(classLoader2);
    }
}
複製代碼

打印結果以下:

sun.misc.Launcher$AppClassLoader@135fbaa4
sun.misc.Launcher$ExtClassLoader@2503dbd3
null
複製代碼

在 Java 中提供瞭如下三種類加載 ClassLoader:

  1. Bootstrp loader
  2. ExtClassLoader
  3. AppClassLoader

其實上面的 demo 打印出來的結果就驗證了這三種 ClassLoader。

(1): 根類加載器(null)

它是由本地代碼(c/c++)實現的,你根本拿不到他的引用,可是他實際存在,而且加載一些重要的類,它加載(%JAVA_HOME%\jre\lib),如rt.jar(runtime)、i18n.jar等,這些是Java的核心類。

(2): 擴展類加載器(ExtClassLoader)

雖然說能拿到,可是咱們在實踐中不多用到它,它主要加載擴展目錄下的jar包, %JAVA_HOME%\lib\ext

(3): 應用類加載器(AppClassLoader)

它主要加載咱們應用程序中的類,如Test,或者用到的第三方包,如jdbc驅動包等。

這裏的父類加載器與類中繼承概念要區分,它們在class定義上是沒有父子關係的。

類加載器調用順序

一樣的,在 Java 中的 ClassLoader 存在一種調用的順序,咱們就以上面的 Test.java 類進行解析。

當 Test.class 要進行加載時,它將會啓動應用類加載器進行加載Test類,可是這個應用類加載器不會真正去加載他,而是會調用看是否有父加載器,結果有,是擴展類加載器,擴展類加載器也不會直接去加載,它看本身是否有父加載器沒,結果它仍是有的,是根類加載器。

因此這個時候根類加載器就去加載這個類,可在%JAVA_HOME%\jre\lib 下,它找不到 demo.Test 這個類,因此他告訴他的子類加載器,我找不到,你去加載吧,子類擴展類加載器去 %JAVA_HOME%\lib\ext 去找,也找不着,它告訴它的子類加載器 AppClassLoader,我找不到這個類,你去加載吧,結果AppClassLoader 找到了,就加到內存中,並生成 Class 對象。

借用網上的一張圖來表示流程。

在這裏插入圖片描述

這張圖很明顯的展現了 ClassLoader 的執行順序流程。

雙親委託

雙親委託即「類裝載器有載入類的需求時,會先請示其 Parent 使用其搜索路徑幫忙載入,若是 Parent 找不到,那麼才由本身依照本身的搜索路徑搜索類」,上面那張圖就顯示了該流程。

咱們仍是使用上面的代碼進行說明:

package demo;
public class Test {
    public static void main(String[] args) {
    
        ClassLoader classLoader = Test.class.getClassLoader();
        System.out.println(classLoader);
        ClassLoader classLoader1 = classLoader.getParent();
        System.out.println(classLoader1);
        ClassLoader classLoader2 = classLoader1.getParent();
        System.out.println(classLoader2);
    }
}
複製代碼

打印結果以下:

sun.misc.Launcher$AppClassLoader@135fbaa4
sun.misc.Launcher$ExtClassLoader@2503dbd3
null
複製代碼

打印結果能夠看出,Test是由 AppClassLoader 加載器加載的,AppClassLoader 的 Parent 加載器是 ExtClassLoader ,可是ExtClassLoader 的 Parent 爲 null 是怎麼回事呵,朋友們留意的話,前面有提到 Bootstrap Loader 是用 C++ 語言寫的,依 java 的觀點來看,邏輯上並不存在 Bootstrap Loader 的類實體,因此在 java 程序代碼裏試圖打印出其內容時,咱們就會看到輸出爲 null。

爲何要有「委託機制」?能夠從安全方面考慮,若是一我的寫了一個惡意的基礎類(如java.lang.String)並加載到 JVM 將會引發嚴重的後果,但有了全盤負責制,java.lang.String 永遠是由根裝載器來裝載,避免以上狀況發生。

假如咱們本身寫了一個 java.lang.String 的類,咱們是否能夠替換調JDK 自己的類?

答案是否認的。咱們不能實現。爲何呢?我看不少網上解釋是說雙親委託機制解決這個問題,其實不是很是的準確。由於雙親委託機制是能夠打破的,你徹底能夠本身寫一個 classLoader 來加載本身寫的java.lang.String 類,可是你會發現也不會加載成功,具體就是由於針對java.* 開頭的類,jvm 的實現中已經保證了必須由 bootstrp 來加載。

自定義 ClassLoader

既然系統已經存在三種 ClassLoader 爲何還須要咱們本身定義 ClassLoader 呢?由於 Java 中提供的默認 ClassLoader 只加載指定目錄下面的 jar 和 class ,可是若是咱們須要加載其餘地方的 jar 和 class 時則無能爲力了,這個時候就須要咱們本身實現 ClassLoader 了。咱們從上面瞭解到 ClassLoader是一個抽象類,實現自定義的 ClassLoader 須要繼承該類並實現裏面的方法。通常狀況下,咱們重寫父類的 findClass 方法便可。

package java.lang;
public abstract class ClassLoader {
	 public Class loadClass(String name);
	 protected Class defineClass(byte[] b);
	 public URL getResource(String name);
	 public Enumeration getResources(String name);
	 public ClassLoader getParent();
	 Class<?> findClass(String name)
	 //...
}
複製代碼

ClassLoader 方法那麼多爲何只重寫 findClass 方法? 由於 JDK 已經在 loadClass 方法中幫咱們實現了 ClassLoader 搜索類的算法,當在 loadClass 方法中搜索不到類時,loadClass 方法就會調用findClass 方法來搜索類,因此咱們只需重寫該方法便可。如沒有特殊的要求,通常不建議重寫 loadClass 搜索類的算法。

自定義 ClassLoader的示例

假如咱們自定義一個 classloader 咱們能夠編寫一個測試類來講明。在當前目錄下面新建一個 Hello 類。裏面有個方法 sayHello 而後放入到指定目錄下面,如:我當前的目錄爲:

/Users/java/intellidea/JavaTest/src/demo/Hello.java

package demo;
public class Hello {
    public void sayHello() {
        System.out.println("say hello ------> classloader");
    }
}
複製代碼

接着咱們須要自定義一個 ClassLoader 來繼承系統的 ClassLoader。咱們命名爲 HelloClassLoader 類。

package demo;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
public class HelloClassLoader extends ClassLoader {
    private String mLibPath;
    public HelloClassLoader(String path) {
        mLibPath = path;
    }
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        String fileName = getFileName(name);
        File file = new File(mLibPath,fileName);
        try {
            FileInputStream is = new FileInputStream(file);
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            int len = 0;
            try {
                while ((len = is.read()) != -1) {
                    bos.write(len);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
            byte[] data = bos.toByteArray();
            is.close();
            bos.close();
            return defineClass(name,data,0,data.length);
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        return super.findClass(name);
    }
    //獲取要加載 的class文件名
    private String getFileName(String name) {
        // TODO Auto-generated method stub
        int index = name.lastIndexOf('.');
        if(index == -1){
            return name+".class";
        }else{
            return name.substring(index)+".class";
        }
    }
}
複製代碼

在HelloClassLoader 類中咱們經過 findClass() 方法來查找咱們用到的 Hello.class 文件,從而生成了 Class 對象。接着咱們編寫HelloClassLoaderTest 測試類進行測:

package demo;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class HelloClassLoaderTest {
    public static void main(String[] args) throws InvocationTargetException {
        // TODO Auto-generated method stub
        //建立自定義classloader對象。
        HelloClassLoader diskLoader = new HelloClassLoader("/Users/java/intellidea/JavaTest/src/demo");
        try {
            //加載class文件
            Class c = diskLoader.loadClass("demo.Hello");
            if (c != null) {
                try {
                    Object obj = c.newInstance();
                    Method method = c.getDeclaredMethod("sayHello", null);
                    //經過反射調用Hello類的sayHello方法
                    method.invoke(obj, null);
                } catch (InstantiationException | IllegalAccessException
                        | NoSuchMethodException
                        | SecurityException |
                        IllegalArgumentException |
                        InvocationTargetException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
        } catch (ClassNotFoundException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
}
複製代碼

測試類按照預測結果應該打印:

say hello ------> classloader
複製代碼

這行代碼是咱們在 Hello 類中的一個方法 sayHello 裏面所打印的。咱們運行一下上面的代碼,查看下打印結果:

在這裏插入圖片描述

如料想的同樣,打印出了正確結果。以上即是一個簡單的自定義ClassLoader 類的實現過程。

總結

以上即是關於 Java 中的 ClassLoader 裏面的內容,還有一些暫未涉及到,接下來即是研究 Android中 的 ClassLoader 使用內容。

關於做者

專一於 Android 開發多年,喜歡寫 blog 記錄總結學習經驗,blog 同步更新於本人的公衆號,歡迎你們關注,一塊兒交流學習~

在這裏插入圖片描述
相關文章
相關標籤/搜索