類加載器 - ClassLoader詳解

得到ClassLoader的途徑

  • 得到當前類的ClassLoader
    • clazz.getClassLoader()
  • 得到當前線程上下文的ClassLoader
    • Thread.currentThread().getContextClassLoader();
  • 得到系統的ClassLoader
    • ClassLoader.getSystemClassLoader()
  • 得到調用者的ClassLoader
    • DriverManager.getCallerClassLoader

ClassLoader源碼解析

概述

類加載器是用於加載類的對象,ClassLoader是一個抽象類。若是咱們給定了一個類的二進制名稱,類加載器應嘗試去定位或生成構成定義類的數據。一種典型的策略是將給定的二進制名稱轉換爲文件名,而後去文件系統中讀取這個文件名所對應的class文件。java

每一個Class對象都會包含一個定義它的ClassLoader的一個引用。數組

數組類的Class對象,不是由類加載器去建立的,而是在Java運行期JVM根據須要自動建立的。對於數組類的類加載器來講,是經過Class.getClassLoader()返回的,與數組當中元素類型的類加載器是同樣的;若是數組當中的元素類型是一個原生類型,數組類是沒有類加載器的【代碼一】。安全

應用實現了ClassLoader的子類是爲了擴展JVM動態加載類的方式。網絡

類加載器典型狀況下時能夠被安全管理器所使用去標識安全域問題。併發

ClassLoader類使用了委託模型來尋找類和資源,ClassLoader的每個實例都會有一個與之關聯的父ClassLoader,當ClassLoader被要求尋找一個類或者資源的時候,ClassLoader實例在自身嘗試尋找類或者資源以前會委託它的父類加載器去完成。虛擬機內建的類加載器,稱之爲啓動類加載器,是沒有父加載器的,可是能夠做爲一個類加載器的父類加載器【雙親委託機制】。jvm

支持併發類加載的類加載器叫作並行類加載器,要求在初始化期間經過ClassLoader.registerAsParallelCapable 方法註冊自身,ClassLoader類默認被註冊爲能夠並行,可是若是它的子類也是並行加載的話須要單獨去註冊子類。ide

在委託模型不是嚴格的層次化的環境下,類加載器須要並行,不然類加載會致使死鎖,由於加載器的鎖在類加載過程當中是一直被持有的。post

一般狀況下,Java虛擬機以平臺相關的形式從本地的文件系統中加載類,好比在UNIX系統,虛擬機從CLASSPATH環境所定義的目錄加載類。
然而,有些類並非來自於文件;它們是從其它來源獲得的,好比網絡,或者是由應用自己構建【動態代理】。定義類(defineClass )方法會將字節數組轉換爲Class的實例,這個新定義類的實例能夠由Class.newInstance建立。測試

由類加載器建立的對象的方法和構造方法可能引用其它的類,爲了肯定被引用的類,Java虛擬機會調用最初建立類的類加載器的loadClass方法。ui

二進制名稱:以字符串參數的形式向CalssLoader提供的任意一個類名,必須是一個二進制的名稱,包含如下四種狀況

  • "java.lang.String" 正常類
  • "javax.swing.JSpinner$DefaultEditor" 內部類
  • "java.security.KeyStore\(Builder\)FileBuilder$1" KeyStore的內部類Builder的內部類FileBuilder的第一個匿名內部類
  • "java.net.URLClassLoader$3$1" URLClassLoader類的第三個匿名內部類的第一個匿名內部類

代碼一:

public class Test12 {
    public static void main(String[] args) {
        String[] strings = new String[6];
        System.out.println(strings.getClass().getClassLoader());
        // 運行結果:null

        Test12[] test12s = new Test12[1];
        System.out.println(test12s.getClass().getClassLoader());
        // 運行結果:sun.misc.Launcher$AppClassLoader@18b4aac2

        int[] ints = new int[2];
        System.out.println(ints.getClass().getClassLoader());
        // 運行結果:null
    }
}

loadClass方法

loadClass的源碼以下, loadClass方法加載擁有指定的二進制名稱的Class,默認按照以下順序尋找類:

  • 調用findLoadedClass(String)檢查這個類是否被加載
  • 調用父類加載器的loadClass方法,若是父類加載器爲null,就會調用啓動類加載器
  • 調用findClass(String)方法尋找

使用上述步驟若是類被找到且resolve爲true,就會去調用resolveClass(Class)方法

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

findClass方法

findClass的源碼以下,findClass尋找擁有指定二進制名稱的類,JVM鼓勵咱們重寫此方法,須要自定義加載器遵循雙親委託機制,該方法會在檢查完父類加載器以後被loadClass方法調用,默認返回ClassNotFoundException異常。

protected Class<?> findClass(String name) throws ClassNotFoundException {
    throw new ClassNotFoundException(name);
}

defineClass方法

defineClass的源碼以下,defineClass方法將一個字節數組轉換爲Class的實例。

protected final Class<?> defineClass(String name, byte[] b, int off, int len,
                                     ProtectionDomain protectionDomain)
    throws ClassFormatError
{
    protectionDomain = preDefineClass(name, protectionDomain);
    String source = defineClassSourceLocation(protectionDomain);
    Class<?> c = defineClass1(name, b, off, len, protectionDomain, source);
    postDefineClass(c, protectionDomain);
    return c;
}

自定義類加載器

/**
 * 繼承了ClassLoader,這是一個自定義的類加載器
 * @author 夜的那種黑丶
 */
public class ClassLoaderTest extends ClassLoader {
    public static void main(String[] args) throws Exception {
        ClassLoaderTest loader = new ClassLoaderTest("loader");
       Class<?> clazz = loader.loadClass("classloader.Test01");
        Object object = clazz.newInstance();
        System.out.println(object);
        System.out.println(object.getClass().getClassLoader());
    }
    //------------------------------以上爲測試代碼---------------------------------

    /**
     * 類加載器名稱,標識做用
     */
    private String classLoaderName;

    /**
     * 從磁盤讀物字節碼文件的擴展名
     */
    private String fileExtension = ".class";

    /**
     * 建立一個類加載器對象,將系統類加載器當作該類加載器的父加載器
     * @param classLoaderName 類加載器名稱
     */
    private ClassLoaderTest(String classLoaderName) {
        // 將系統類加載器當作該類加載器的父加載器
        super();
        this.classLoaderName = classLoaderName;
    }

    /**
     * 建立一個類加載器對象,顯示指定該類加載器的父加載器
     * 前提是須要有一個類加載器做爲父加載器
     * @param parent 父加載器
     * @param classLoaderName 類加載器名稱
     */
    private ClassLoaderTest(ClassLoader parent, String classLoaderName) {
        // 顯示指定該類加載器的父加載器
        super(parent);
        this.classLoaderName = classLoaderName;
    }

    /**
     * 尋找擁有指定二進制名稱的類,重寫ClassLoader類的同名方法,須要自定義加載器遵循雙親委託機制
     * 該方法會在檢查完父類加載器以後被loadClass方法調用
     * 默認返回ClassNotFoundException異常
     * @param className 類名
     * @return Class的實例
     * @throws ClassNotFoundException 若是類不能被找到,拋出此異常
     */
    @Override
    protected Class<?> findClass(String className) throws ClassNotFoundException {
        byte[] data = this.loadClassData(className);
        /*
         * 經過defineClass方法將字節數組轉換爲Class
         * defineClass:將一個字節數組轉換爲Class的實例,在使用這個Class以前必需要被解析
         */
        return this.defineClass(className, data, 0 , data.length);
    }

    /**
     * io操做,根據類名找到對應文件,返回class文件的二進制信息
     * @param className 類名
     * @return class文件的二進制信息
     * @throws ClassNotFoundException 若是類不能被找到,拋出此異常
     */
    private byte[] loadClassData(String className) throws ClassNotFoundException {
        InputStream inputStream = null;
        byte[] data;
        ByteArrayOutputStream byteArrayOutputStream = null;

        try {
            this.classLoaderName = this.classLoaderName.replace(".", "/");
            inputStream = new FileInputStream(new File(className + this.fileExtension));
            byteArrayOutputStream = new ByteArrayOutputStream();

            int ch;
            while (-1 != (ch = inputStream.read())) {
                byteArrayOutputStream.write(ch);
            }

            data = byteArrayOutputStream.toByteArray();
        } catch (Exception e) {
            throw new ClassNotFoundException();
        } finally {
            try {
                if (inputStream != null) {
                    inputStream.close();
                }
                if (byteArrayOutputStream != null) {
                    byteArrayOutputStream.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return data;
    }
}

以上是一段自定義類加載器的代碼,咱們執行這段代碼

classloader.Test01@7f31245a
sun.misc.Launcher$AppClassLoader@18b4aac2

能夠看見,這段代碼中進行類加載的類加載器仍是系統類加載器(AppClassLoader)。這是由於jvm的雙親委託機制形成的,private ClassLoaderTest(String classLoaderName)將系統類加載器當作咱們自定義類加載器的父加載器,jvm的雙親委託機制使自定義類加載器委託系統類加載器完成加載。

改造如下代碼,添加一個path屬性用來指定類加載位置:

public class ClassLoaderTest extends ClassLoader {
    public static void main(String[] args) throws Exception {
        ClassLoaderTest loader = new ClassLoaderTest("loader");
        loader.setPath("/home/fanxuan/Study/java/jvmStudy/out/production/jvmStudy/");
        Class<?> clazz = loader.loadClass("classloader.Test01");
        System.out.println("class:" + clazz);

        Object object = clazz.newInstance();
        System.out.println(object);
        System.out.println(object.getClass().getClassLoader());
    }
    //------------------------------以上爲測試代碼---------------------------------

    /**
     * 從指定路徑加載
     */
    private String path;

    ......
    
    /**
     * io操做,根據類名找到對應文件,返回class文件的二進制信息
     * @param className 類名
     * @return class文件的二進制信息
     * @throws ClassNotFoundException 若是類不能被找到,拋出此異常
     */
    private byte[] loadClassData(String className) throws ClassNotFoundException {
        InputStream inputStream = null;
        byte[] data;
        ByteArrayOutputStream byteArrayOutputStream = null;

        className = className.replace(".", "/");

        try {
            this.classLoaderName = this.classLoaderName.replace(".", "/");
            inputStream = new FileInputStream(new File(this.path + className + this.fileExtension));
            byteArrayOutputStream = new ByteArrayOutputStream();

            int ch;
            while (-1 != (ch = inputStream.read())) {
                byteArrayOutputStream.write(ch);
            }

            data = byteArrayOutputStream.toByteArray();
        } catch (Exception e) {
            throw new ClassNotFoundException();
        } finally {
            try {
                if (inputStream != null) {
                    inputStream.close();
                }
                if (byteArrayOutputStream != null) {
                    byteArrayOutputStream.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return data;
    }

    public void setPath(String path) {
        this.path = path;
    }
}

運行一下

class:class classloader.Test01
classloader.Test01@7f31245a
sun.misc.Launcher$AppClassLoader@18b4aac2

修改一下測試代碼,並刪除工程下的Test01.class文件

public static void main(String[] args) throws Exception {
    ClassLoaderTest loader = new ClassLoaderTest("loader");
   loader.setPath("/home/fanxuan/桌面/");
    Class<?> clazz = loader.loadClass("classloader.Test01");
    System.out.println("class:" + clazz);

    Object object = clazz.newInstance();
    System.out.println(object);
    System.out.println(object.getClass().getClassLoader());
}

運行一下

class:class classloader.Test01
classloader.Test01@135fbaa4
classloader.ClassLoaderTest@7f31245a

分析

改造後的兩塊代碼,第一塊代碼中加載類的是系統類加載器AppClassLoader,第二塊代碼中加載類的是自定義類加載器ClassLoaderTest。是由於ClassLoaderTest會委託他的父加載器AppClassLoader加載class,第一塊代碼的path直接是工程下,AppClassLoader能夠加載到,而第二塊代碼的path在桌面目錄下,因此AppClassLoader沒法加載到,而後ClassLoaderTest自身嘗試加載併成功加載到。若是第二塊代碼工程目錄下的Test01.class文件沒有被刪除,那麼依然是AppClassLoader加載。

再來測試一塊代碼

public static void main(String[] args) throws Exception {
    ClassLoaderTest loader = new ClassLoaderTest("loader");
    loader.setPath("/home/fanxuan/Study/java/jvmStudy/out/production/jvmStudy/");
    Class<?> clazz = loader.loadClass("classloader.Test01");
    System.out.println("class:" + clazz.hashCode());

    Object object = clazz.newInstance();
    System.out.println(object.getClass().getClassLoader());

    ClassLoaderTest loader2 = new ClassLoaderTest("loader");
    loader2.setPath("/home/fanxuan/Study/java/jvmStudy/out/production/jvmStudy/");
    Class<?> clazz2 = loader2.loadClass("classloader.Test01");
    System.out.println("class:" + clazz2.hashCode());

    Object object2 = clazz2.newInstance();
    System.out.println(object2.getClass().getClassLoader());
}

結果顯而易見,類由系統類加載器加載,而且clazz和clazz2是相同的。

class:2133927002
sun.misc.Launcher$AppClassLoader@18b4aac2
class:2133927002
sun.misc.Launcher$AppClassLoader@18b4aac2

在改造一下

public static void main(String[] args) throws Exception {
    ClassLoaderTest loader = new ClassLoaderTest("loader");
    loader.setPath("/home/fanxuan/桌面/");
    Class<?> clazz = loader.loadClass("classloader.Test01");
    System.out.println("class:" + clazz.hashCode());

    Object object = clazz.newInstance();
    System.out.println(object.getClass().getClassLoader());

    ClassLoaderTest loader2 = new ClassLoaderTest("loader2");
    loader2.setPath("/home/fanxuan/桌面/");
    Class<?> clazz2 = loader2.loadClass("classloader.Test01");
    System.out.println("class:" + clazz2.hashCode());

    Object object2 = clazz2.newInstance();
    System.out.println(object2.getClass().getClassLoader());
}

運行結果

class:325040804
classloader.ClassLoaderTest@7f31245a
class:621009875
classloader.ClassLoaderTest@45ee12a7

ClassLoaderTest是顯而易見,可是clazz和clazz2是不一樣的,這是由於類加載器的命名空間的緣由。

咱們能夠經過設置父類加載器來讓loader和loader2處於同一命名空間

public static void main(String[] args) throws Exception {
    ClassLoaderTest loader = new ClassLoaderTest("loader");
    loader.setPath("/home/fanxuan/桌面/");
    Class<?> clazz = loader.loadClass("classloader.Test01");
    System.out.println("class:" + clazz.hashCode());

    Object object = clazz.newInstance();
    System.out.println(object.getClass().getClassLoader());

    ClassLoaderTest loader2 = new ClassLoaderTest(loader, "loader2");
    loader2.setPath("/home/fanxuan/桌面/");
    Class<?> clazz2 = loader2.loadClass("classloader.Test01");
    System.out.println("class:" + clazz2.hashCode());

    Object object2 = clazz2.newInstance();
    System.out.println(object2.getClass().getClassLoader());
}

運行結果

class:325040804
classloader.ClassLoaderTest@7f31245a
class:325040804
classloader.ClassLoaderTest@7f31245a

擴展:命名空間

  • 每一個類加載器都有本身的命名空間,命名空間由該加載器及全部的父加載器所加載的類組成
  • 在同一命名空間中,不會出現類的完整名字(包括類的包名)相同的兩個類
  • 在不一樣的命名空間中,有可能會出現類的完整名字(包括類的包名)相同的兩個類
相關文章
相關標籤/搜索