是的!又一篇Java類加載介紹!

類加載基礎概念

嘗試用5W1H模型來聊聊Java的類加載。

什麼是類加載? 簡單的說,把字節碼加載到JVM中的過程,我們就稱之爲類加載。輸入是某個類的.class文件的字節流,輸出是JVM所管理的方法區中關於該類的信息。

爲什麼要有類加載? 我的理解是爲了更好的支持動態特性,比如說熱部署,就是利用了JVM可以動態加載字節碼的機制實現的。

什麼時候進行類加載? 總的來說,JVM需要某個類的信息,而又沒有的時候,就會觸發類加載。具體來說分了以下幾個場景:

  1. 遇到new、getstatic、putstatic、invokestatic等指令時,如果類還沒有加載過就會觸發類加載;
  2. 子類進行類加載時,如果父類還沒有加載過,會先觸發父類的加載;
  3. 使用反射進行各種操作時,如果類還沒有加載過,會先進行類加載。
  4. 虛擬機啓動時,會首先加載含有main方法的類。
  5. 其他情況,這裏不是抄書,所以我們先不再枚舉。

誰來負責類加載? 類加載有專門的類加載器來完成,類加載器又有等級森嚴的層級關係,爺爺輩的類加載器叫啓動類加載器,然後是爸爸輩,叫拓展類加載器,最後是應用程序類加載器。這裏涉及到一個類加載過程中各個類加載器是如何分工合作的,會在雙親委派模型中提到。

怎樣進行類加載? 前面提到過類加載就是把類的字節碼塞進虛擬機的過程,那麼具體怎麼做呢?

首先,類加載器需要從某處獲得字節碼的二進制字節流。爲什麼不說字節碼文件?因爲除了從.class文件中獲取,還可以從壓縮包中解壓獲取,從網絡中獲取(比如Applet),甚至是動態生成一個(想想動態代理)都是可以的。這個動作,我們稱爲加載。(TO-DO 這個時候生成Class對象了嗎?

接着,這個對象還不能直接使用,我們需要把針對它做各種校驗,比如字節碼本身是否合規,是否是該版本的虛擬機支持,如果都通過了,就需要給靜態變量開闢一塊內存區域,然後賦零值,這裏的零值指的是,當內存中沒有數據時,變量的值,比如對於int型來說零值是0,對於boolean型來說,零值是false。最後,如果這個類中存在符號引用,還需要把符號引用解析爲具體的內存地址。以上所有的動作,我們合併起來,稱之爲鏈接

最後,終於到了給靜態字段賦值的時候了,無論是直接賦值還是通過靜態塊來完成,編譯器都會把這些賦值語句收集到一起,並且按程序書寫的順序,然後放在一個叫clinit的方法中,依次執行。這個動作,我們稱爲初始化

類加載進階

以上是針對類加載機制的一個簡單介紹,下面我們進行一些更加高階的講解。

一種優雅的單例實現

實現單例有多種不同的寫法,也個有優缺點,其中“餓漢式”的寫法最爲簡潔,但缺點是一旦觸發了類加載就會同步進行實例化。觸發的機制前面的基礎篇中已經提到過,比如調用Resource中存在的任意一個static字段或者方法,就會觸發Resource類的類加載。

而下面的寫法通過引入靜態內部類完美的解決了這個問題。結合剛纔提到的類加載機制,說說這是爲什麼?

public class Resource {

    private Resource() {}

    public static Resource getInstance() {
        return Holder.resource;
    }

    private static class Holder {
        public static final Resource resource = new Resource();
    }
}

關於雙親委派模型

剛纔介紹了幾個不同的類加載器,那麼他們之間是怎樣合作的?我們結合ClassLoader類中的loadClass方法來看:

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

  synchronized (getClassLoadingLock(name)) {

    // 首先,檢查該類是否已經被加載過
    Class<?> c = findLoadedClass(name);
    if (c == null) {

      // 針對未加載過的類,先嚐試讓父類加載器進行加載
      try {
        // 啓動類加載器是通過C++實現的,只能表示爲null
        // 因此這裏有2個邏輯分支
        if (parent != null) {
          c = parent.loadClass(name, false);
        } else {
          // 返回一個啓動類加載器加載的類,如果沒有則返回null
          c = findBootstrapClassOrNull(name);
        }
      } catch (ClassNotFoundException e) {
        
      }

      // 如果父類加載器無法加載,再嘗試自己加載
      if (c == null) {
        c = findClass(name);
      }
    }

    // 其他實現細節...

    return c;
  }
}

通過自定義類加載器實現熱部署

熱部署指的是不需要重啓應用就可以動態的替換掉其中的一些功能,類加載器給我們提供了這樣一種實現的思路。

首先,我們說在Java的世界裏,通過一個類的全限定名 + 類加載器,可以唯一的定位一個類,也就是說,哪怕是同一個.class文件,通過不同的類加載器進入JVM,它們之間也是互相隔離的。

基於上述事實,當我們希望只是升級某個類的功能時,就可以通過這樣的機制來實現:爲該類實例化一個新的類加載器,並重新加載該類,最後替換掉之前舊的版本。

根據這樣的思路,我們可以定義一個MyTest類,擁有一個showVersion()方法,在第一個版本中會打印1.0,在第二個版本中會打印2.0,代表功能進行了升級。

public class MyTest {
  public void showVersion() {
    System.out.println("1.0版本");
  }
}

接着,需要自定義一個類加載器,重寫部分方法,簡單來說,它會根據類的全限定名,在/tmp目錄下找對應的字節碼文件,針對特定的類,如MyTest,不經過雙親委派模型,直接加載進內存中。

public class MyClassLoader extends ClassLoader {

  // 指定那些類可以通過自定義類加載器的方式加載
  private Set<String> classNamesLoadMyself = new HashSet<>();

  public MyClassLoader(String ... classNames) {
    for (String className : classNames) {
      classNamesLoadMyself.add(className);
    }
  }

  @Override
  protected Class<?> findClass(String name) {
    // 根據路徑和類名找到對應的文件並轉化爲相應的字節流
    byte[] bytes = FileUtil.getClassByte("/tmp", name);
    return defineClass(name, bytes, 0, bytes.length);
  }

  @Override
  public Class<?> loadClass(String name) throws ClassNotFoundException {
    // 如果是指定了要自定義類加載的類,則繞開雙親委派模型
    if (classNamesLoadMyself.contains(name)) {
      return findClass(name);
    }
    return super.loadClass(name);
  }
}

最後,我們對這個自定義的類加載器做一個測試。

public class MyClassLoaderClient {

  public static void main(String[] args) throws Exception {
    for (int i = 0; i < 10; i++) {
        // 實例化一個類加載器
        MyClassLoader myClassLoader = new MyClassLoader("MyTest");

        // 注意這裏不能直接強制類型轉化爲MyTest
        Object myTest = myClassLoader.loadClass(className).newInstance();
        myTest.getClass().getMethod("showVersion").invoke(myTest);

        // 休眠1秒
        TimeUnit.SECONDS.sleep(1);
    }
  }
}

在這個測試類中有一行註釋,不能將實例化的MyTest做強制類型轉換,請問這是爲什麼呢?

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章