深入理解JVM中的ClassLoader

DK_BurNIng發表於2017-12-07

JVM中的類載入器結構

要理解jvm中的類載入器結構,僅僅查閱文件是不夠的。這裡給出一個小程式幫助理解jvm虛擬機器中的類載入器結構。

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);
	}
	
}

複製程式碼

然後我們編譯並執行這段程式檢視下這段程式碼的執行結果:

深入理解JVM中的ClassLoader

為什麼有的類的載入器為null?

點此查閱jdk文件

我們可以查閱jdk中的函式說明,發現有這麼一段話:

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。

如何理解列印出來的三種類載入器?

BootStrap->ExtClassLoader->AppClassLoader->開發者自定義類載入器. 可認為BootStrap為祖先載入器,開發者自定義類載入器為底層載入器。 不過多數情況,我們並不會自定義類載入器,所以大多數情況,AppClassLoader就是JVM中的底層類載入器了。

注意BootStrap是用c++程式碼編寫的,後面2個類載入器則是java類編寫 這就解釋了為什麼BootStrap載入器會返回null了,因為這個祖先類載入器在 java里根本找不到嗎

類載入的委託機制原則

  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的安裝目錄

如何驗證前面的結論?

很多人學習類載入器只是瀏覽一遍文件結束,很難有深刻的映像,時間一久就忘記,所以下面給出一個例子,可以加深對類載入器委託機制的印象

深入理解JVM中的ClassLoader

深入理解JVM中的ClassLoader

這裡我們可以看到,我是先將編譯好的class檔案 打成一個jar包,然後再將這個打好的jar包放到我們jdk路徑下的 /jre/lib/ext/ 這個目錄下,前面介紹過這個目錄就是ext類載入器要載入的目錄,然後再次執行我們一開始編寫好的程式就可以發現,同樣是JVMClassLoader這個類,一開始我們的類載入器是appclassloader後面就變成了extclassloader。到這裡應該就對類載入器的委託機制有了深刻認識了。

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

簡單來說,一句話概括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();
         }

     }
}

複製程式碼

然後我們來看一下程式執行結果:

深入理解JVM中的ClassLoader

大家可以看到classdate和classDate2 這2個類,我們在用classLoader去載入的時候傳的引數唯一的不同就是前者傳入了完整的包名,而後者沒有。這就導致了前者的classLoader依舊是系統自帶的appclassloader 而後者才是我們自定義的classloader。 原因:

雖然對於classDate和classDate2來說,我們手動指定了她的類載入是我們自定義的myclassloader,但是根據類載入器的規則,我們能用父親的loadclass就肯定不會用自己的,而我們系統類載入器,AppClassLoader要想loadclass成功是需要傳入完整的包名的。所以classDate的構造還是傳入了完整的包名,這就是為啥classDate的載入器還是AppClassLoader,但是classDate2並沒有傳入完整的包名,所以AppClassLoader也是找不到這個CustomDate類的,最後只能交給MyClassLoader這個最底層的,我們自定義的classloader來load

相關文章