Java虛擬機類加載機制中的ClassLoader類加載器詳解以及如何自定義ClassLoader類加載器

1、Java虛擬機的類加載機制概述

虛擬機把描述類的數據從Class文件加載到內存,並對數據進行校驗、轉換解析和初始化,最終形成可以被虛擬機直接使用的Java類型,這就是Java虛擬機的類加載機制

類從被加載虛擬機內存中開始,到卸載出內存中爲止,它的整個生命週期包括:加載、驗證、準備、解析、初始化、使用和卸載7個階段。

加載是類加載過程中的一個階段,不要混淆這兩個看起很相似的名詞,在加載階段虛擬機會採用類加載器來加載Class文件。

2、Java虛擬機中的類加載器

站在Java虛擬機的角度來講,只存在兩種不同的類加載器:

  • 一種是啓動類加載器(Bootstrap ClassLoader),這個類加載器是使用C++語言實現,是虛擬機自身的一部分。
  • 另外一種就是其他的類加載器,這些加載器都是由Java語言實現,獨立於虛擬機外部,並且全部都繼承於抽象類java.lang.ClassLoader。

站在Java開發人員角度來講, 類加載器其實還可以分得更細緻一些,絕大部分的Java程序都會使用到下面3種系統提供的類加載器。

  • 啓動類加載器(Bootstrap
    ClassLoader),前面介紹過,這個類加載器負責將存放在<JAVA_HOME>\lib目錄中的類庫加載到虛擬機內存中
  • 擴展類加載器(Extension ClassLoader),這個類加載器負責加載<JAVA_HOME>\lib\ext目錄中的類庫。
  • 應用程序類加載器(Application
    ClassLoader),這個類加載器是ClassLoader中的getSystemLoader()方法的返回值,所以也稱爲系統類加載器。它負責加載ClassPath上所指定的類庫。

2.1、查看類加載器加載的路徑

通過下面代碼可以看到類加載器加載的路徑

2.1.1、查看啓動類加載器

System.out.println(System.getProperty("sun.boot.class.path"));

結果:

D:\Program Files\Java\jdk\jre\lib\resources.jar;
D:\Program Files\Java\jdk\jre\lib\rt.jar;
D:\Program Files\Java\jdk\jre\lib\sunrsasign.jar;
D:\Program Files\Java\jdk\jre\lib\jsse.jar;
D:\Program Files\Java\jdk\jre\lib\jce.jar;
D:\Program Files\Java\jdk\jre\lib\charsets.jar;
D:\Program Files\Java\jdk\jre\lib\jfr.jar;
D:\Program Files\Java\jdk\jre\classes

2.1.2、查看擴展類加載器

System.out.println(System.getProperty("java.ext.dirs"));

結果:

D:\Program Files\Java\jdk\jre\lib\ext;
C:\Windows\Sun\Java\lib\ext

3、類加載器之間的關係

接下來探討它們的加載順序,先創建一個 ClassLoaderTest 的Java文件。
先看看這個ClassLoaderTest類是被誰加載的:

public class ClassLoaderTest {
    public static void main(String[] args) {
    
        System.out.println(ClassLoaderTest.class.getClassLoader());
        
    }
}

結果:

sun.misc.Launcher$AppClassLoader@42a57993

也就是說明 ClassLoaderTest.class文件是由AppClassLoader加載的。

由於這個 ClassLoaderTest 類是我們自己編寫的,那麼int.class或者是String.class的加載是由誰完成的呢?
我們可以在代碼中嘗試:

public class ClassLoaderTest {
    public static void main(String[] args) {
    
        System.out.println(String.class.getClassLoader());
        System.out.println(int.class.getClassLoader());
        
    }
}

結果:

null
null

結果打印的是空null,意思是int.class和String.class這類基礎類沒有類加載器加載?

當然不是!
int.class和String.class這類基礎類是由Bootstrap ClassLoader加載的,最直觀的解釋就是String類是在java.lang包裏,這個包在rt.jar裏面,而這個jar是由Bootstrap ClassLoader加載的。要徹底明白這些,我們首先得知道一個前提。

3.1、每個類加載器都有一個父加載器

每個類加載器都有一個父加載器,比如加載 ClassLoaderTest.class是由AppClassLoader完成,那麼AppClassLoader也有一個父加載器,怎麼樣獲取呢?很簡單,通過getParent方法。就像這樣:

public class ClassLoaderTest {
    public static void main(String[] args) {
    
        ClassLoader c =  ClassLoaderTest.class.getClassLoader();
        System.out.println(c.getParent());
        
    }
}

結果:

sun.misc.Launcher$ExtClassLoader@28d93b30

這個說明,AppClassLoader的父加載器是ExtClassLoader。那麼ExtClassLoader的父加載器又是誰呢?

public class ClassLoaderTest {
    public static void main(String[] args) {
    
        ClassLoader c =  ClassLoaderTest.class.getClassLoader();
        System.out.println(c.getParent().getParent());
        
    }
}

結果:

null

又是一個空,這表明ExtClassLoader沒有父加載器。那麼,爲什麼標題又是每一個加載器都有一個父加載器呢?這不矛盾嗎?爲了解釋這一點,我們還需要看下面的一個基礎前提。

3.2、父加載器不是父類

我們我們可以看一下ExtClassLoader和AppClassLoader的源碼。(源碼是精簡過的,只展示了關鍵部分)

...
static class AppClassLoader extends URLClassLoader{...}
...
static class ExtClassLoader extends URLClassLoader{...}
...

可以看見ExtClassLoader和AppClassLoader同樣繼承自URLClassLoader,但上面一小節代碼中,爲什麼調用AppClassLoader的getParent()代碼會得到ExtClassLoader的實例呢?

可以看sun.misc.Launcher.class的部分關鍵源碼:

public Launcher() {
 ...
	Launcher.ExtClassLoader var1;
	try {
		// 創建ExtClassLoader 實例。沒有傳參數,注意這個細節。下面解釋
		var1 = Launcher.ExtClassLoader.getExtClassLoader();
	} catch (IOException var10) {
		throw new InternalError("Could not create extension class loader", var10);
	}

	try {
		// 創建AppClassLoader實例。將ExtClassLoader 實例作爲參數,注意這個細節。下面解釋
		this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
	} catch (IOException var9) {
		throw new InternalError("Could not create application class loader", var9);
	}
...

AppClassLoader.class的部分關鍵源碼:

 static class AppClassLoader extends URLClassLoader {
        final URLClassPath ucp = SharedSecrets.getJavaNetAccess().getURLClassPath(this);

        public static ClassLoader getAppClassLoader(final ClassLoader var0) throws IOException {
            final String var1 = System.getProperty("java.class.path");
            final File[] var2 = var1 == null?new File[0]:Launcher.getClassPath(var1);
            return (ClassLoader)AccessController.doPrivileged(new PrivilegedAction() {
                public Launcher.AppClassLoader run() {
                    URL[] var1x = var1 == null?new URL[0]:Launcher.pathToURLs(var2);
                    // 創建AppClassLoader時,傳進來的參數var0,繼續往AppClassLoader方法傳。
                    return new Launcher.AppClassLoader(var1x, var0);
                }
            });
        }
        // 傳進來的 參數 ClassLoader var2。繼續傳往父類構造器,也就是URLClassLoader類的構造器
        AppClassLoader(URL[] var1, ClassLoader var2) {
            super(var1, var2, Launcher.factory);
            this.ucp.initLookupCache(this);
        }
        ...

ExtClassLoader.class的部分關鍵源碼:

  static class ExtClassLoader extends URLClassLoader {
        private static volatile Launcher.ExtClassLoader instance;
        // 在創建ExtClassLoader 是調用了父類構造器,也就是URLClassLoader類的構造器,第二個參數直接寫了爲null
        // 這是爲什麼呢?下面 URLClassLoader 源碼中揭曉
        public ExtClassLoader(File[] var1) throws IOException {
            super(getExtURLs(var1), (ClassLoader)null, Launcher.factory);
            SharedSecrets.getJavaNetAccess().getURLClassPath(this).initLookupCache(this);
        }	
        ...

java.net.URLClassLoader的部分關鍵源碼:

// 注意看這個構造器的第二個參數,傳進來的是 parent!
 public URLClassLoader(URL[] urls, ClassLoader parent,
                          URLStreamHandlerFactory factory) {
        super(parent);
        // this is to make the stack depth consistent with 1.1
        SecurityManager security = System.getSecurityManager();
        if (security != null) {
            security.checkCreateClassLoader();
        }
        acc = AccessController.getContext();
        ucp = new URLClassPath(urls, factory, acc);
    }	
    ...

需要注意的是sun.misc.Launcher.class的部分關鍵源碼中的這幾句話:

Launcher.ExtClassLoader var1;

var1 = Launcher.ExtClassLoader.getExtClassLoader();

this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);

結合AppClassLoader.class的部分關鍵源碼和java.net.URLClassLoader的部分關鍵源碼得知:

Launcher.AppClassLoader.getAppClassLoader(var1)這個方法中的var1參數最終會傳到URLClassLoder的構造方法中去,這個參數就表示了parent。

代碼已經說明了問題,AppClassLoader的parent是一個ExtClassLoader實例。

ExtClassLoader並沒有直接找到對parent的賦值。它調用了它的父類也就是URLClassLoder的構造方法並傳遞了3個參數。和AppClassLoader的方法一樣,在創建ExtClassLoader 時,並沒有傳任何參數,而在調用了它的父類也就是URLClassLoder的構造方法並傳遞了3個參數時,第二個參數顯示的寫明瞭爲(ClassLoader)null。看下方代碼

源碼兩個關鍵部分:
ExtClassLoader.class的源碼:

public ExtClassLoader(File[] var1) throws IOException {
            super(getExtURLs(var1), (ClassLoader)null, Launcher.factory);
            SharedSecrets.getJavaNetAccess().getURLClassPath(this).initLookupCache(this);
        }
       ...

java.net.URLClassLoader的源碼:

public URLClassLoader(URL[] urls, ClassLoader parent, URLStreamHandlerFactory factory) {
        super(parent);
           ...

答案已經很明瞭了,ExtClassLoader的parent爲null。

上面張貼這麼多代碼也是爲了說明AppClassLoader的parent是ExtClassLoader,ExtClassLoader的parent是null。這符合我們之前編寫的測試代碼。

3.3、再講Bootstrap ClassLoader

Bootstrap ClassLoader是由C/C++編寫的,它本身是虛擬機的一部分,所以它並不是一個JAVA類,也就是無法在java代碼中獲取它的引用,JVM啓動時通過Bootstrap類加載器加載rt.jar等核心jar包中的class文件,之前的int.class,String.class都是由它加載。然後呢,我們前面已經分析了,JVM初始化sun.misc.Launcher並創建Extension ClassLoader和AppClassLoader實例。並將ExtClassLoader設置爲AppClassLoader的父加載器。

4、雙親委派模式

雙親委派模式的工作過程是:如果一個類加載器收到了類加載的請求,它首先不會自己去嘗試加載這個類,而是把這個請求委派給父類加載器去完成,每一個層次的類加載器都是如此,因此所有的加載請求最終都會傳送到頂層的啓動類加載器中,只有當父加載器反饋自己無法完成這個加載請求時(它搜索的範圍沒有找到所需的類),子加載器纔會嘗試自己取加載。

類加載器雙親委派模型:
在這裏插入圖片描述

上面已經詳細介紹了加載過程,但具體爲什麼是這樣加載,我們還需要了解幾個個重要的方法loadClass()、findLoadedClass()、findClass()、defineClass()。

4.1、重要方法

4.1.1、loadClass()

通過指定的全限定類名加載class,它通過重載的loadClass(String,boolean)方法,它的源碼是這樣的:

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 {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // 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;
        }
    }

源碼描述的加載步驟是:

  1. 執行 findLoadedClass(name) 方法,檢查這個class有沒有被加載過,如果加載過了,直接返回,如果沒有就進行加載。
  2. 判斷父加載器是否爲null,如果不是,就交給父加載去嘗試加載。如果爲null就交給Bootstrap ClassLoader去嘗試加載。

填坑:這也解釋了ExtClassLoader的parent爲null,但仍然說Bootstrap ClassLoader是它的父加載器的原因。

另外代碼解釋了雙親委託的加載機制。

4.1.2、findClass()

根據名稱或位置加載.class字節碼

4.1.3、defineClass()

這個方法在編寫自定義classloader的時候非常重要,它能將class二進制內容轉換成Class對象,如果不符合要求的會拋出各種異常。

5、自定義ClassLoader

如果在某種情況下,我們需要動態加載一些東西,比如從D盤某個文件夾加載一個class文件,或者從網絡上下載class主內容然後再進行加載,這樣可以嗎?

如果要這樣做的話,需要我們自定義一個ClassLoader。

步驟描述

  1. 編寫一個類繼承自ClassLoader抽象類。
  2. 複寫它的findClass()方法。
  3. 在findClass()方法中調用defineClass()。

注意:一個ClassLoader創建時如果沒有指定parent,那麼它的parent默認就是AppClassLoader。

5.1、普通Java類

我們寫編寫一個測試用的類文件,Test.java

public class Test {
	
	public void say(){
		System.out.println("Hello World");
	}

}

然後將它編譯過年class文件Test.class放到F:\這個路徑下。

5.2、自定義的ClassLoader

我們編寫自定義的ClassLoader的代碼。

public class CustomClassLoader extends ClassLoader {

    private String mLibPath;

    public CustomClassLoader(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) {
            e.printStackTrace();
        }

        return super.findClass(name);
    }

    //獲取要加載 的class文件名
    private String getFileName(String name) {
        int index = name.lastIndexOf('.');
        if(index == -1){
            return name+".class";
        }else{
            return name.substring(index+1)+".class";
        }
    }
}

5.3、編寫測試類

現在編寫測試代碼。如果調用Test對象的say方法,它會輸出"Say Hello"這條字符串。但現在是我們把Test.class放置在應用工程所有的目錄之外,我們需要加載它,然後執行它的方法。具體效果如何呢?

public class ClassLoaderTest {

    public static void main(String[] args) {

        //創建自定義classloader對象。
        CustomClassLoader diskLoader = new CustomClassLoader("F:\\");
        try {
            //加載class文件
            Class c = diskLoader.loadClass("com.learn.classLoader.Test");

            if(c != null){
                try {
                    Object obj = c.newInstance();
                    Method method = c.getDeclaredMethod("say");
                    //通過反射調用Test類的say方法
                    method.invoke(obj);
                } catch (InstantiationException | IllegalAccessException
                        | NoSuchMethodException
                        | SecurityException |
                        IllegalArgumentException |
                        InvocationTargetException e) {
                    e.printStackTrace();
                }
            }
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

結果:

Say Hello

可以看到,Test類的say方法正確執行,也就是我們自定義寫的CustomClassLoader 編寫成功。

6、另外一個類加載器:線程上下文類加載器

6.1、概念

線程上下文類加載器(Thread Context ClassLoader),這個類加載器可以通過java.lang.Thread類的setContextClassLoader()方法進行設置,如果創建線程時還未設置,它將會從父線程中繼承一個。Java 應用運行的初始線程的上下文類加載器是應用程序類加載器(AppClassLoader)。

6.2、繞過雙親委派模式

有了這個類加載器,父類加載器就可以請求子類加載器去完成類加載的動作,這種行爲實際上就是打破了雙親委派模型的層次結構來逆向使用類加載器
最典型的例子就是:JNDI、JDBC。

7、總結

  • Java虛擬機類加載機制
  • Java的幾種類加載器
  • 各個加載器掃描的範圍
  • 各個加載器之間的關係
  • 如何自定義一個ClassLoader類加載器

8、拓展之Class解密類加載器

常見的用法是將Class文件按照某種加密手段進行加密,然後按照規則編寫自定義的ClassLoader進行解密,這樣我們就可以在程序中加載特定了類,並且這個類只能被我們自定義的加載器進行加載,提高了程序的安全性。
詳情請參照該文章:Java實現自定義classLoader類加載器動態解密class文件


技 術 無 他, 唯 有 熟 爾。
知 其 然, 也 知 其 所 以 然。
踏 實 一 些, 不 要 着 急, 你 想 要 的 歲 月 都 會 給 你。


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