1. 程式人生 > >類載入器 - ClassLoader詳解

類載入器 - ClassLoader詳解

獲得ClassLoader的途徑

  • 獲得當前類的ClassLoader
    • clazz.getClassLoader()
  • 獲得當前執行緒上下文的ClassLoader
    • Thread.currentThread().getContextClassLoader();
  • 獲得系統的ClassLoader
    • ClassLoader.getSystemClassLoader()
  • 獲得呼叫者的ClassLoader
    • DriverManager.getCallerClassLoader

ClassLoader原始碼解析

概述

類載入器是用於載入類的物件,ClassLoader是一個抽象類。如果我們給定了一個類的二進位制名稱,類載入器應嘗試去定位或生成構成定義類的資料。一種典型的策略是將給定的二進位制名稱轉換為檔名,然後去檔案系統中讀取這個檔名所對應的class檔案。

每個Class物件都會包含一個定義它的ClassLoader的一個引用。

陣列類的Class物件,不是由類載入器去建立的,而是在Java執行期JVM根據需要自動建立的。對於陣列類的類載入器來說,是通過Class.getClassLoader()返回的,與陣列當中元素型別的類載入器是一樣的;如果陣列當中的元素型別是一個原生型別,陣列類是沒有類載入器的【程式碼一】。

應用實現了ClassLoader的子類是為了擴充套件JVM動態載入類的方式。

類載入器典型情況下時可以被安全管理器所使用去標識安全域問題。

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

支援併發類載入的類載入器叫做並行類載入器,要求在初始化期間通過ClassLoader.registerAsParallelCapable 方法註冊自身,ClassLoader類預設被註冊為可以並行,但是如果它的子類也是並行載入的話需要單獨去註冊子類。

在委託模型不是嚴格的層次化的環境下,類載入器需要並行,否則類載入會導致死鎖,因為載入器的鎖在類載入過程中是一直被持有的。

通常情況下,Java虛擬機器以平臺相關的形式從本地的檔案系統中載入類,比如在UNIX系統,虛擬機器從CLASSPATH環境所定義的目錄載入類。
然而,有些類並不是來自於檔案;它們是從其它來源得到的,比如網路,或者是由應用本身構建【動態代理】。定義類(defineClass )方法會將位元組陣列轉換為Class的例項,這個新定義類的例項可以由Class.newInstance建立。

由類載入器建立的物件的方法和構造方法可能引用其它的類,為了確定被引用的類,Java虛擬機器會呼叫最初建立類的類載入器的loadClass方法。

二進位制名稱:以字串引數的形式向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是不同的,這是因為類載入器的名稱空間的原因。

擴充套件:名稱空間

  • 每個類載入器都有自己的名稱空間,名稱空間由該載入器及所有的父載入器所載入的類組成
  • 在同一名稱空間中,不會出現類的完整名字(包括類的包名)相同的兩個類
  • 在不同的名稱空間中,有可能會出現類的完整名字(包括類的包名)相同的兩個類