淺談ClassLoader


JAVA啟動后,是經過JVM各級ClassLoader來加載各個類到內存。為了更加了解加載過程,我通過分析和寫了一個簡單的ClassLoader來粗淺的分析它的原理。

JVMClassLoader分三層,分別為Bootstrap ClassLoaderExtension ClassLoaderSystem ClassLoader,他們不是類繼承的父子關系,是邏輯上的上下級關系。

Bootstrap ClassLoader是啟動類加載器,它是用C++編寫的,從%jre%/lib目錄中加載類,或者運行時用-Xbootclasspath指定目錄來加載。

Extension ClassLoader是擴展類加載器,從%jre%/lib/ext目錄加載類,或者運行時用-Djava.ext.dirs制定目錄來加載。

System ClassLoader,系統類加載器,它會從系統環境變量配置的classpath來查找路徑,環境變量里的.表示當前目錄,是通過運行時-classpath-Djava.class.path指定的目錄來加載類。

 

一般自定義的Class Loader可以從java.lang.ClassLoader繼承,不同classloader加載相同的類,他們在內存也不是相等的,即它們不能互相轉換,會直接拋異常。java.lang.ClassLoader的核心加載方法是loadClass方法,如:

    protected synchronized Class<?> loadClass(String name, boolean resolve)

         throws ClassNotFoundException

    {

         // First, check if the class has already been loaded

         Class c = findLoadedClass(name);

         if (c == null) {

             try {

                   if (parent != null) {

                       c = parent.loadClass(name, false);

                   } else {

                       c = findBootstrapClass0(name);

                   }

             } catch (ClassNotFoundException e) {

                 // If still not found, then invoke findClass in order

                 // to find the class.

                 c = findClass(name);

             }

         }

         if (resolve) {

             resolveClass(c);

         }

         return c;

    }

通過上面加載過程,我們能知道JVM默認是雙親委托加載機制,即首先判斷緩存是否有已加載的類,如果緩存沒有,但存在父加載器,則讓父加載器加載,如果不存在父加載器,則讓Bootstrap ClassLoader去加載,如果父類加載失敗,則調用本地的findClass方法去加載。

可以通過下面三條語句,輸入現在加載的各個classloader的加載路徑:

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

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

        System.out.println("java.class.path:" +System.getProperty("java.class.path"));

        ClassLoader cl = Thread.currentThread().getContextClassLoader();//ClassLoader.getSystemClassLoader()

        System.out.println("getContextClassLoader:" +cl.toString());

        System.out.println("getContextClassLoader.parent:" +cl.getParent().toString());

        System.out.println("getContextClassLoader.parent2:" +cl.getParent().getParent());

輸出結果為:

sun.boot.class.path:C:\Program Files\Java\jre7\lib\resources.jar;C:\Program Files\Java\jre7\lib\rt.jar;C:\Program Files\Java\jre7\lib\sunrsasign.jar;C:\Program Files\Java\jre7\lib\jsse.jar;C:\Program Files\Java\jre7\lib\jce.jar;C:\Program Files\Java\jre7\lib\charsets.jar;C:\Program Files\Java\jre7\classes

java.ext.dirs:C:\Program Files\Java\jre7\lib\ext;C:\Windows\Sun\Java\lib\ext

java.class.path:E:\MyProjects\workspace\TestConsole\bin

getContextClassLoader:sun.misc.Launcher$AppClassLoader@19dbc3b

getContextClassLoader.parent:sun.misc.Launcher$ExtClassLoader@b103dd

getContextClassLoader.parent2:null

從上面的運行結果可以看出邏輯上的層級繼承關系。雙親委托機制的作用是防止系統jar包被本地替換,因為查找方法過程都是從最底層開始查找。 因此,一般我們自定義的classloader都需要采用這種機制,我們只需要繼承java.lang.ClassLoader實現findclass即可,如果需要更多控制,自定義的classloader就需要重寫loadClass方法了,比如tomcat的加載過程,這個比較復雜,可以通過其他文檔資料查看相關介紹。

各個ClassLoader加載相同的類后,他們是不互等的,這個當涉及多個ClassLoader,並且有通過當前線程上線文獲取ClassLoader后轉換特別需要注意,可以通過線程的setContextClassLoader設置一個ClassLoader線程上下文,然后再通過Thread.currentThread().getContextClassLoader()獲取當前線程保存的Classloader。但是自定義的類文件,放到Bootstrap ClassLoader加載目錄,是不會被Bootstrap ClassLoader加載的,因為作為啟動類加載器,它不會加載自己不熟悉的jar包的,並且類文件必須打包成jar包放到加載器加載的根目錄,才可能被擴展類加載器所加載。

                                                      

下面我自定義一個簡單的classloader

public class TestClassLoader extends ClassLoader {

    //定義文件所在目錄  

    private static final String DEAFAULTDIR="E:\\MyProjects\\workspace\\TestConsole\\bin\\";

   

    public Class<?> findClass(String name) throws ClassNotFoundException {

        byte[] b = null;  

        try {  

            b = loadClassData(GetClassName(name));  

        } catch (Exception e) {  

            e.printStackTrace();  

        } 

        return defineClass(name, b, 0, b.length);    

    }   

    @Override

    protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {

        if(name.startsWith("java.")){try {

            return super.loadClass(name, false);    

        } catch (ClassNotFoundException e) {

            e.printStackTrace();  

        }

        }

       byte[] b = null;  

       try {  

           b = loadClassData(GetClassName(name));  

       } catch (Exception e) {  

           e.printStackTrace();  

       } 

       return defineClass(name, b, 0, b.length);  

    }  

    private byte[] loadClassData(String filepath) throws Exception {  

        int n =0;  

        BufferedInputStream br = new BufferedInputStream(  

                        new FileInputStream(  

                    new File(filepath)));  

        ByteArrayOutputStream bos= new ByteArrayOutputStream();  

            while((n=br.read())!=-1){  

                bos.write(n);  

            }  

            br.close();  

        return bos.toByteArray();  

    }

    public static String GetClassName(String name){

        return DEAFAULTDIR+name.replace('.','/')+".class";

    }

}

這個自定義的ClassLoader重寫了loadclass方法,但不用默認的雙親委托,比如java.lang包下面的都無法解析,這里我簡單的判斷如果是java.開始的包則用父類去解析,能簡單的滿足雙親委托機制,但是其他相關非系統類加載也沒有用父類加載了。

測試代碼如:

        TestClassLoader liuloader = new TestClassLoader();

        Myrunner runner = new Myrunner();

        runner.setContextClassLoader(liuloader);

        runner.start();

Myrunner是我自定義繼承自Thread的線程,通過設置線程上下文的classloader后,線程內部測試代碼如:

        ClassLoader cl1 = Thread.currentThread().getContextClassLoader();

        System.out.println(cl1);

它將會輸出:

com.liu.ClassLoader.TestClassLoader@347cdb,說明已經為當前線程上下文設置了自定義的Classloader了,如果這個線程內部通過這個classloader加載一個類,再轉換成當前的類,如代碼:

    Class c = cl1.loadClass("com.liu.ClassLoader.TestLoader2");    TestLoader2 tloader = (TestLoader2)c.newInstance();

則為拋java.lang.ClassCastException異常: com.liu.ClassLoader.TestLoader2 cannot be cast to com.liu.ClassLoader.TestLoader2

因為cl1當前是 TestClassLoader加載的,而這個TestLoader2的類還是默認由AppClassLoader加載,因此它們不能隱式轉換,Classloader加載相同的類,內存認為它們是沒有關系的對象。

如果把我自定義的TestClassLoader里的LoadClass方法去掉,則采用了雙親委托機制,這樣我們除了指定的類以外,其他都會優先用父類來加載。這樣可以解決剛才的java.lang.ClassCastException異常問題,為加載的對象建立一個抽象父類,自定義的Classloader負責加載子類,父類統一交給AppClassLoader或父加載器來加載,這樣線程內部可以使用類試:

        Class c = cl1.loadClass("com.liu.ClassLoader.TestLoader2");

        BaseTest tloader = (BaseTest)c.newInstance();

BaseTestTestLoader2的父類,因為BaseTest都是AppClassLoader或父加載器加載的,因此可以達到成功隱式轉換的目的。

對於Tomcat等幾個處理的Classloader都是自定義並重寫了loadclass方法,內部會更復雜處理。

 

作為一個新手,本文是自己對javaclassloader的一個粗略理解,如有問題,請及時指正。
如轉載,請注明來自:http://lawson.cnblogs.com/

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM