【深入理解Java虛擬機 】類加載器的命名空間以及類的卸載

類加載器的命名空間

  • 每個類加載器又有一個命名空間,由其以及其父加載器組成

類加載器的命名空間的作用和影響

  • 每個類加載器又有一個命名空間,由其以及其父加載器組成
  • 在每個類加載器自己的命名空間中不能出現相同類名的類 (此處值得是類的全名,包含包名)
  • 在不同的類命名空間中,可能會出現多個相同的類名的類

如下面的代碼示例中, 首先定義一個類加載器 MyClassLoader

  static class MyClassLoader extends ClassLoader {        private String classLoaderName;        private String classPath;        public MyClassLoader(String classPath, String classLoaderName) {        super(); // 未指定則默認使用應用類加載器        this.classLoaderName = classLoaderName;        this.classPath = classPath;      }        public MyClassLoader(ClassLoader parent, String classLoaderName) {        super(parent); // 顯式的指定父類加載器        this.classLoaderName = classLoaderName;      }        @Override      protected Class<?> findClass(String name) {        System.out.println("MyClassLoader.findClass");        byte[] bytes = null;        try {          bytes = loadClassByte(name);          return defineClass(name, bytes, 0, bytes.length);        } catch (Exception e) {            throw new RuntimeException(e);        }      }        private byte[] loadClassByte(String name) throws Exception {        name = name.replace(".", "/");        File file = new File(this.classPath + name + ".class");          try (ByteArrayOutputStream byteStream = new ByteArrayOutputStream();            InputStream fileStream = new FileInputStream(file)) {            int ch;          while ((ch = fileStream.read()) != -1) {            byteStream.write(ch);          }          return byteStream.toByteArray();        }      }    }

下面我們嘗試使用此類加載加載一個磁盤的class文件,代碼如下:

  public static void main(String[] args) throws ClassNotFoundException {      MyClassLoader classLoader = new MyClassLoader("/tmp/", "CustomClassLoader");      Class<?> aClass = classLoader.loadClass("com.zhoutao.classload.ReferenceExample004");      ClassLoader loader = aClass.getClassLoader();      System.out.println(loader);        System.out.println("----------------------");        MyClassLoader classLoader2 = new MyClassLoader("/tmp/", "CustomClassLoader");      Class<?> aClass2 = classLoader2.loadClass("com.zhoutao.classload.ReferenceExample004");      ClassLoader loader2 = aClass2.getClassLoader();      System.out.println(loader);    }

按照之前的理論,由於 ReferenceExample004 類在classLoader 中已經被加載,那麼在 classLoader2 中將不會被加載,我們看一下輸出的結果

MyClassLoader.findClass  com.zhoutao.classload.ReferenceExample010$MyClassLoader@610455d6  ----------------------  MyClassLoader.findClass  com.zhoutao.classload.ReferenceExample010$MyClassLoader@60e53b93

總結

可以看到,類加載器的findClass(String) 方法被執行了兩次,這是因為加載該類的類加載器是兩個不同的對象,在文章的開頭提及:每個類加載器又有一個命名空間,由其以及其父加載器組成 上面的兩個類加載器顯然命名空間不一致,所以findClass() 方法會被執行兩次

類的卸載

在內存中的 Class 類沒有引用的時候就會被 JVM 卸載,因為 JVM 自帶的類加載 啟動類加載器,拓展類加載器以及應用類加載器,在三個類加載的實例一直被 JVM 引用,所以JVM 自帶的類加載器加載的類一直被其加載器引用,所以不會被卸載
但是自定義的類加載器的實例可以被手動的設置為null,這導致類加載不再被引用,其所加載的 class 類在沒有實例其沒有類加載器引用的情況下就會被卸載,卸載的時間發生在系統垃圾回收的時候

類的卸載測試

同類的加載一樣,要觀察到類的卸載,可以通過添加 JVM 參數-XX:+TraceClassUnloading 的方式輸入類的卸載信息。

  public static void main(String[] args) throws ClassNotFoundException, InterruptedException {      MyClassLoader classLoader = new MyClassLoader("/tmp/", "CustomClassLoader");      Class<?> aClass = classLoader.loadClass("com.zhoutao.classload.ReferenceExample004");        classLoader = null;      aClass = null;        // 手動觸發GC 觀察類回收的信息        System.gc();        TimeUnit.SECONDS.sleep(10);    }

通過控制台可以觀察到類的卸載記錄:

MyClassLoader.findClass  [Unloading class com.zhoutao.classload.ReferenceExample004 0x00000007c0061028]

或者通過JVisual 工具觀察到類的卸載過程, 為了便於觀察,適當加長 延時的時間 TimeUnit.SECONDS.sleep(100);, 終端或者CMD終端 輸入 jvisualvm 命令啟動,啟動測試程序,在 jvisualvm 工具中接連到該線程,可以看到下面的信息:

已卸載的總數為: 1

file

歡迎關注微信公眾號獲取更多開發技巧幹貨

關注我的微信公眾號