深刻理解JVM中的ClassLoader

JVM中的類加載器結構

要理解jvm中的類加載器結構,僅僅查閱文檔是不夠的。這裏給出一個小程序幫助理解jvm虛擬機中的類加載器結構。html

package com.wuyue.demo;

import java.util.Date;
import java.util.List;

/**
 * 測試類
 * @author wuyue
 */
public class JVMClassLoader {

	public static void main(String[] args){
		System.out.println("JVMClassLoader類的加載器的名稱:"+JVMClassLoader.class.getClassLoader().getClass().getName());
		System.out.println("System類的加載器的名稱:"+System.class.getClassLoader());
		System.out.println("List類的加載器的名稱:"+List.class.getClassLoader());
		
		ClassLoader cl = JVMClassLoader.class.getClassLoader();
		while(cl != null){
			System.out.print(cl.getClass().getName()+"->");
			cl = cl.getParent();
		}
		System.out.println(cl);
	}
	
}

複製代碼

而後咱們編譯並運行這段程序查看下這段代碼的運行結果:java

爲何有的類的加載器爲null?

點此查閱jdk文檔c++

咱們能夠查閱jdk中的函數說明,發現有這麼一段話:bootstrap

Returns the class loader for the class. Some implementations may use null to represent the bootstrap class loader. This method will return null in such implementations if this class was loaded by the bootstrap class loader.小程序

講白了,這裏的意思就是有的虛擬機實現會用null 來代替bootstrap這個classloader。api

如何理解打印出來的三種類加載器?

BootStrap->ExtClassLoader->AppClassLoader->開發者自定義類加載器. 可認爲BootStrap爲祖先加載器,開發者自定義類加載器爲底層加載器。 不過多數狀況,咱們並不會自定義類加載器,因此大多數狀況,AppClassLoader就是JVM中的底層類加載器了。數組

注意BootStrap是用c++代碼編寫的,後面2個類加載器則是java類編寫 這就解釋了爲何BootStrap加載器會返回null了,由於這個祖先類加載器在 java里根本找不到嗎bash

類加載的委託機制原則

  1. 由下到上加載,頂層加載不了再交給下層加載,若是回到底層位置加載 還加載不到,那就會報ClassNotFound錯誤了。
  2. 如同一開始咱們的例子同樣,JVMClassLoader 這個類 爲何輸出的類加載器名稱是AppClassLoader呢,緣由就是先找到頂層的Boot類加載器發現找不到這個類,而後繼續找ext類加載器仍是找不到,最後在AppClassLoder中找到這個類。因此這個類的加載器就是AppClassLoader了。
  3. 爲何System和List類的類加載器是Boot類加載器?由於Boot類加載器加載的默認路徑就是/jre/lib 這個目錄下的rt.jar 。ext加載器的默認路徑是 /jre/lib/ext/*.jar.這2個目錄下面固然沒法找到咱們的JVMClassLoader類了 注意這裏的根目錄是你jdk的安裝目錄

如何驗證前面的結論?

不少人學習類加載器只是瀏覽一遍文檔結束,很難有深入的映像,時間一久就忘記,因此下面給出一個例子,能夠加深對類加載器委託機制的印象markdown

這裏咱們能夠看到,我是先將編譯好的class文件 打成一個jar包,而後再將這個打好的jar包放到咱們jdk路徑下的 /jre/lib/ext/ 這個目錄下,前面介紹過這個目錄就是ext類加載器要加載的目錄,而後再次運行咱們一開始編寫好的程序就能夠發現,一樣是JVMClassLoader這個類,一開始咱們的類加載器是appclassloader後面就變成了extclassloader。到這裏應該就對類加載器的委託機制有了深入認識了。oracle

如何評價這種委託機制下的類加載器機制?

簡單來講,一句話歸納jvm中的類加載器機制:

能夠用爸爸的錢就絕對不用本身的錢,若是爸爸沒有錢,再用本身的, 若是本身仍是沒有錢,那麼就classnotfound異常

好處就是要加載一個類首先交給他的上級類加載器處理,若是上級類有,就直接拿來用,這樣若是以前加載過的類就不須要再次重複加載了。簡稱:能啃老用爹的錢,爲啥要用本身的?

看源碼再次加深對類加載器的理解。

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) {
                        //先看有沒有爸爸類加載器若是有就繼續「遞歸」調用loadclass這個方法
                        c = parent.loadClass(name, false);
                    } else {
                        //若是沒有爸爸類加載器了,就說明到頭了。看看
                        //祖先bootstrap類加載器中有沒有
                        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();
                    //若是沒有找到就調用本身的findclass找這個類。
                    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;
        }
複製代碼

因此看到在代碼裏其實就是一個調用parent loadclass的過程,若是parent都找不到就調用本身的findclass方法來找。 和咱們前面的分析是一致的。

有興趣的同窗能夠在jdk目錄中找到rt.jar 這個jar包,查看AppClassLoader等系統自帶的classLoader的源碼,有助於加深理解,這裏就再也不過多敘述了

自定義類加載器。

首先咱們定義一個CustomDate類,這個類只重寫一下toString方法

package com.wuyue.test;

import java.util.Date;

/**
 * 只是重寫了Date的toString方法
 */
public class CustomDate extends Date{

    @Override
    public String toString() {
        return "my cystom date";
    }
}

複製代碼

而後寫一個簡單的classloader,自定義的那種。

package com.wuyue.test;


import java.io.*;

public class MyClassLoader extends ClassLoader{

    String classDir;

    public MyClassLoader() {

    }

    public MyClassLoader(String classDir) {
        this.classDir = classDir;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        String classFile=classDir+"/"+name+".class";
        System.out.println("classFile path=="+classFile);

        try {
            //這個地方咱們只是簡單的讀取文件流的方式來獲取byte數組
            //其實能夠嘗試將class文件加密之後 這裏解密 這樣就能夠保證
            //這種class文件 只有你寫的classloader才能讀取的了。
            //其餘任何classloader都讀取不了 包括系統的。
            byte[] classByte=toByteArray(classFile);
            return defineClass(classByte,0,classByte.length);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }


        return super.findClass(name);
    }

    /**
     * the traditional io way
     *
     * @param filename
     * @return
     * @throws IOException
     */
    public static byte[] toByteArray(String filename) throws IOException, FileNotFoundException {

        File f = new File(filename);
        if (!f.exists()) {
            throw new FileNotFoundException(filename);
        }

        ByteArrayOutputStream bos = new ByteArrayOutputStream((int) f.length());
        BufferedInputStream in = null;
        try {
            in = new BufferedInputStream(new FileInputStream(f));
            int buf_size = 1024;
            byte[] buffer = new byte[buf_size];
            int len = 0;
            while (-1 != (len = in.read(buffer, 0, buf_size))) {
                bos.write(buffer, 0, len);
            }
            return bos.toByteArray();
        } catch (IOException e) {
            e.printStackTrace();
            throw e;
        } finally {
            try {
                in.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
            bos.close();
        }
    }
}

複製代碼

最後寫一個測試咱們自定義classloader的主程序:

package com.wuyue.test;
import java.util.Date;

public class ClassLoaderTest {
     public static void main(String[] args)
     {
         try {

             Class classDate = new MyClassLoader("/Users/wuyue/IdeaProjects/ClassLoaderTest/out/production/ClassLoaderTest/com/wuyue/test").loadClass("com.wuyue.test.CustomDate");
             Class classDate2 = new MyClassLoader("/Users/wuyue/IdeaProjects/ClassLoaderTest/out/production/ClassLoaderTest/com/wuyue/test").loadClass("CustomDate");
             Date date = (Date) classDate.newInstance();
             System.out.println("date ClassLoader:"+date.getClass().getClassLoader().getClass().getName());
             System.out.println(date);

             Date date2 = (Date) classDate2.newInstance();
             System.out.println("date2 ClassLoader:"+date2.getClass().getClassLoader().getClass().getName());
             System.out.println(date2);
         } catch (Exception e1) {
             e1.printStackTrace();
         }

     }
}

複製代碼

而後咱們來看一下程序運行結果:

你們能夠看到classdate和classDate2 這2個類,咱們在用classLoader去加載的時候傳的參數惟一的不一樣就是前者傳入了完整的包名,然後者沒有。這就致使了前者的classLoader依舊是系統自帶的appclassloader 然後者纔是咱們自定義的classloader。 緣由:

雖然對於classDate和classDate2來講,咱們手動指定了她的類加載是咱們自定義的myclassloader,可是根據類加載器的規則,咱們能用父親的loadclass就確定不會用本身的,而咱們系統類加載器,AppClassLoader要想loadclass成功是須要傳入完整的包名的。因此classDate的構造仍是傳入了完整的包名,這就是爲啥classDate的加載器仍是AppClassLoader,可是classDate2並無傳入完整的包名,因此AppClassLoader也是找不到這個CustomDate類的,最後只能交給MyClassLoader這個最底層的,咱們自定義的classloader來load

相關文章
相關標籤/搜索