天天看點

java類的熱加載

這兩篇文章分别介紹了類的生命周期、ClassLoader類。今天,我們繼續介紹類的熱加載。

一、類的解除安裝

1、引用關系:

當某個類被類加載器加載到記憶體後,就會生成一個相應的Class對象。他們的關系如下:

1)類加載器和Class對象:

  • 在類加載器的内部實作中,用一個Java集合來存放所加載類的Class執行個體的引用。
  • 另一方面,一個Class對象總是會引用它的類加載器。調用Class對象的getClassLoader()方法,就能獲得它的類加載器。

由此可見,Class執行個體和加載它的加載器之間為雙向關聯關系。

2)類、類的執行個體對象、類的Class對象:

  • 一個類的執行個體總是引用代表這個類的Class對象,在Object類中定義了getClass()方法,這個方法傳回代表對象所屬類的Class對象的引用。
  • 此外,所有的Java類都有一個靜态屬性class,它引用代表這個類的Class對象。
java類的熱加載

2、解除安裝類:

由Java虛拟機自帶的類加載器(bootstrap、ext、app)所加載的類,在虛拟機的生命周期中,始終不會被解除安裝。因為,Java虛拟機本身會始終引用這些類加載器,而這些類加載器則會始終引用它們所加載的類的Class對象,是以這些Class對象始終是可觸及的。

由使用者自定義的類加載器加載的類是可以被解除安裝的。先看一下如何使用自定義加載器加載一個類,并建立類的執行個體。

java類的熱加載

代碼也許不準确,但是過程很清晰,整個過程涉及三個變量,而且他們之間的引用關系很明确,即執行個體對象引用着類對應的Class對象,Class對象引用着類加載器,調用類的getClassLoader()方法可以獲得該類的加載器。

1)類何時被解除安裝?

解除安裝類,其實就是解除安裝類對應的Class執行個體,以及方法區中關于該類對應的資料結構。是以要想解除安裝類必須:

  • 先解除安裝該類的加載器,這很好了解,因為類引用着類加載器;
  • 然後将類所建立的執行個體都解除安裝掉;

隻有當這個類的加載器和類的執行個體都結束了生命周期,這個類的生命周期才結束。即

java類的熱加載

有一種情況,就是類加載器被解除安裝掉了,但是類還沒有被解除安裝掉,這個時候如果有需要用這個類,就可以重新使用不需要加載,方法區的二進制資料結構也是原來的。如果類已經被解除安裝了,就需要重新加載這個類,當然方法區中的資料結構以及類資訊都是重新加載的。

總結:

  • 被bootstrap、Ext、App加載的類運作期間不會被解除安裝;
  • 被自定義類加載器加載的類運作期間可以被解除安裝,但是幾率也很小,因為涉及到垃圾回收等;

2)NoClassDefFoundError suddenly:

該錯誤線上上環境遇到過一次。

二、熱加載

1、自定義類加載器:

通過前面的分析可知,實作自定義類加載器需要繼承ClassLoader或者URLClassLoader,繼承ClassLoader則需要自己重寫findClass()方法并編寫加載邏輯,繼承URLClassLoader則可以省去編寫findClass()方法以及class檔案加載轉換成位元組碼流的代碼。那麼編寫自定義類加載器的意義何在呢?

  • 當class檔案不在ClassPath路徑下,預設系統類加載器無法找到該class檔案,在這種情況下我們需要實作一個自定義的ClassLoader來加載特定路徑下的class檔案生成class對象。
  • 當一個class檔案是通過網絡傳輸并且可能會進行相應的加密操作時,需要先對class檔案進行相應的解密後再加載到JVM記憶體中,這種情況下也需要編寫自定義的ClassLoader并實作相應的邏輯。
  • 當需要實作熱部署功能時(一個class檔案通過不同的類加載器産生不同class對象進而實作熱部署功能),需要實作自定義ClassLoader的邏輯。

接下來我們看幾個例子。

1.1)繼承ClassLoader實作自定義File類加載器:

package cn.nuc.edu.LogTest.load;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;

public class FileClassLoader extends ClassLoader{
    private String rootPath;
    public FileClassLoader(String rootPath) {
        super();
        this.rootPath = rootPath;
    }
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] classData = null;
        InputStream ins = null;
        ByteArrayOutputStream baos = null;
        //1、讀取類檔案的位元組碼
        try {
            ins = new FileInputStream(classNameToPath(name));
            baos = new ByteArrayOutputStream();
            int bufferSize = 4096;
            byte[] buffer = new byte[bufferSize];
            int bytesNumRead = 0;
            while ((bytesNumRead = ins.read(buffer)) != -1) {
                baos.write(buffer, 0, bytesNumRead);
            }
            classData = baos.toByteArray();
        } catch (IOException e) {
            throw new ClassNotFoundException();
        } finally {
            try {
                if (baos != null) baos.close();
                if (ins != null) ins.close();
            } catch (Exception e) {
                
            }
        }
        //2、生成class對象
        if (classData == null) {
            throw new ClassNotFoundException();
        } else {
            return defineClass(name, classData, 0, classData.length);
        }
    }
    private String classNameToPath(String className) {
            return rootPath + File.separatorChar
                    + className.replace('.', File.separatorChar) + ".class";
    }
     
     public static void main(String[] args) throws Exception {
        //建立自定義檔案類加載器
        FileClassLoader loader = new FileClassLoader("E:");
        Class<?> class1 = loader.loadClass("cn.nuc.edu.LogTest.load.Demo");
        System.out.println(class1.getClassLoader());
        System.out.println(class1.newInstance().toString());
     }
}      

輸出:

​​cn.nuc.edu.LogTest.load.FileClassLoader@4614ac54​​

demo2...

說明:FileClassLoader繼承ClassLoader重寫findClass方法,保證了雙親委派模型。在main方法中調用了loadClass()方法加載指定路徑下的class檔案,由于bootstrap、Ext以及App類加載器都無法在其路徑下找到該類,是以最終将有自定義類加載器加載,即調用findClass()方法進行加載。

注:Demo.java不能放到IDE中。因為JVM加載類不關心rootPath,它隻關心包名組成的路徑和檔案名(類名),rootPath是由具體的類加載器定義死的(App查找classpath路徑),又因為FileClassLoader繼承自ClassLoader,調用loadClass方法會根據雙親委派會直接交給上級類加載器加載,最終會被AppClassloader在IED的classpath上加載(執行AppClassLoader中的findClass方法),進而不會執行FileClassLoader的findClass方法了。是以,即使上面的rootPath是E:,也不能将Demo.java放到IDE中。

注:如果将上面改成loader.findClass("cn.nuc.edu.LogTest.load.Demo"),無論Demo.java是否在IED中,最終都會是自定義類加載器加載Demo。(繞過了雙親委派)

1.2)繼承URLClassLoader實作自定義File類加載器:

package cn.nuc.edu.LogTest.load;

import java.io.File;
import java.net.URI;
import java.net.URL;
import java.net.URLClassLoader;

public class FileClassLoader2 extends URLClassLoader {
  public FileClassLoader2(URL[] urls) {
    super(urls);
  }

  public static void main(String[] args) throws Exception {
    String rootDir = "E:\\";
    // File to URI
    URI uri = new File(rootDir).toURI();
    URL[] urls = { uri.toURL() };

    // 建立自定義檔案類加載器
    FileClassLoader2 loader = new FileClassLoader2(urls);
    Class<?> class1 = loader.loadClass("cn.nuc.edu.LogTest.load.Demo");
    System.out.println(class1.getClassLoader());
    System.out.println(class1.newInstance().toString());
    
    loader.close();
  }
}      

輸出:

​​cn.nuc.edu.LogTest.load.FileClassLoader@4614ac54​​

demo2...

說明:繼承URLClassLoader,不需要自己再去實作findClass方法了,更加簡單。同樣,Demo.java不能放到IDE中。

注意:findClass()方法在URLClassLoader類中是protected的,而被protected修飾的成員對于本包和其子類可見,是以FileClassLoader2可以調用findClass()方法。

1.3)自定義網絡類加載器:

可以自己實作findClass也可以繼承URLClassLoader。

URL url = new URL(path);
InputStream ins = url.openStream();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int bufferSize = 4096;
byte[] buffer = new byte[bufferSize];
int bytesNumRead = 0;
// 讀取類檔案的位元組
while ((bytesNumRead = ins.read(buffer)) != -1) {
    baos.write(buffer, 0, bytesNumRead);
}      

1.4)加載jar包:

​​http://icejoywoo.github.io/2017/02/22/jvm-classloader-issue.html​​

 2、熱加載類:

想要知道熱部署的原理,必須要了解java類的加載過程:首先将java檔案編譯成class檔案,然後由類加載器将位元組碼檔案加載到記憶體生成類對應的Class對象,接下來連結、初始化。。。

一般在系統中,類的加載都是由系統自帶的類加載器完成,而且對于同一個全限定名的java類,隻能被加載一次,而且無法被解除安裝,而且上面也分析過類的解除安裝不可靠。那該如何做呢?

所謂的熱部署就是利用同一個class檔案不同的類加載器在記憶體建立出兩個不同的class對象,由于JVM在加載類之前會檢測請求的類是否已加載過(即在loadClass()方法中調用findLoadedClass()方法),如果被加載過,則直接從緩存擷取,不會重新加載。注意同一個類加載器的執行個體和同一個class檔案隻能被加載器一次,多次加載将報錯,是以我們實作的熱部署必須讓同一個class檔案可以根據不同的類加載器重複加載,以實作所謂的熱部署。

2.1)實作方式:

  1. 如果要被熱加載的類不會被bootstrap、Ext、App類加載器加載到(不在其路徑下),可以建立不同的自定義類加載器執行個體,然後調用loadClass()、findClass()方法,進而熱加載該類;(反正上面那些類加載器也加載不到,是以即使有雙親委派也沒事)
  2. 如果要被熱加載的類會被bootstrap、Ext、App類加載器加載到,建立不同的自定義類加載器執行個體,然後調用findClass()方法,進而熱加載該類;(繞過雙親委派,以及findLoadedClass()緩存的檢查)
  3. 重寫loadClass方法,破壞雙親委派;(強烈不建議)

說明:真正的類加載是調用了findClass()方法完成的(Ext、App也都是)。loadClass方法隻是雙親委派算法。

2.2)示例1:要熱替換的Demo類可以被AppClassLoader加載到

1)使用FileClassLoader自定義類測試:

實際上前面的FileClassLoader已具備這個功能,但前提是直接調用findClass()方法,而不是調用loadClass()方法,因為ClassLoader中loadClass()方法體中調用findLoadedClass()方法進行了檢測是否已被加載,是以我們直接調用findClass()方法就可以繞過這個問題。

A)使用loadClass方法,無法多次加載同一個類:

public static void main(String[] args) throws Exception {
    //建立自定義檔案類加載器
    FileClassLoader loader = new FileClassLoader("E:\\workspace_suike\\LogTest\\target\\classes");//"E:"
    Class<?> class1 = loader.loadClass("cn.nuc.edu.LogTest.load.Demo");
    System.out.println(class1.getClassLoader());
    System.out.println(class1.hashCode());
    
    FileClassLoader loader2 = new FileClassLoader("E:\\workspace_suike\\LogTest\\target\\classes");
    Class<?> class2 = loader2.loadClass("cn.nuc.edu.LogTest.load.Demo");
    System.out.println(class2.getClassLoader());
    System.out.println(class2.hashCode());
}
//輸出
sun.misc.Launcher$AppClassLoader@7d05e560
607547411
sun.misc.Launcher$AppClassLoader@7d05e560
607547411      

說明:由于Demo類可以被AppClassLoader加載到,是以雖然建立了兩個自定義類加載器執行個體,由于雙親委派,也無法使用自定義類加載器來加載該類。

B)使用findClass方法,多次加載同一個類:

public static void main(String[] args) throws Exception {
    //建立自定義檔案類加載器
    FileClassLoader loader = new FileClassLoader("E:\\workspace_suike\\LogTest\\target\\classes");//"E:"
    Class<?> class1 = loader.findClass("cn.nuc.edu.LogTest.load.Demo");
    System.out.println(class1.getClassLoader());
    System.out.println(class1.hashCode());
    
    FileClassLoader loader2 = new FileClassLoader("E:\\workspace_suike\\LogTest\\target\\classes");
    Class<?> class2 = loader2.findClass("cn.nuc.edu.LogTest.load.Demo");
    System.out.println(class2.getClassLoader());
    System.out.println(class2.hashCode());
}
//輸出
cn.nuc.edu.LogTest.load.FileClassLoader@1a3a2a52
1649907150
cn.nuc.edu.LogTest.load.FileClassLoader@642c39d2
1433743869      

說明:這裡調用了findClass方法,繞過了雙親委派檢測,是以雖然AppClassLoader可以加載到Demo,但仍然使用的是自定義類加載器來加載的Demo類。此外如果用同一個類加載,調用兩次findClass方法加載同一個類,會報LinkError錯誤。

2)使用FileClassLoader2類做測試:

A)調用loadClass方法,無法多次加載同一個類:

public class FileClassLoader2 extends URLClassLoader {
  public FileClassLoader2(URL[] urls) {
    super(urls);
  }

  public static void main(String[] args) throws Exception {
    String rootDir = "E:\\";
    URI uri = new File(rootDir).toURI();// File to URI
    URL[] urls = { uri.toURL() };

    FileClassLoader2 loader1 = new FileClassLoader2(urls);
    Class<?> class1 = loader1.loadClass("cn.nuc.edu.LogTest.load.Demo");
    System.out.println(class1.getClassLoader());
    System.out.println(class1.hashCode());
    
    FileClassLoader2 loader2 = new FileClassLoader2(urls);
    Class<?> class2 = loader2.loadClass("cn.nuc.edu.LogTest.load.Demo");
    System.out.println(class2.getClassLoader());
    System.out.println(class2.hashCode());
  }
}
//輸出
sun.misc.Launcher$AppClassLoader@5736ab79
2053014315
sun.misc.Launcher$AppClassLoader@5736ab79
2053014315      

B)使用findClass方法,多次加載同一個類:

public class FileClassLoader2 extends URLClassLoader {
  public FileClassLoader2(URL[] urls) {
    super(urls);
  }

  public static void main(String[] args) throws Exception {
    String rootDir = "E:\\";
    URI uri = new File(rootDir).toURI();// File to URI
    URL[] urls = { uri.toURL() };

    FileClassLoader2 loader1 = new FileClassLoader2(urls);
    Class<?> class1 = loader1.findClass("cn.nuc.edu.LogTest.load.Demo");
    System.out.println(class1.getClassLoader());
    System.out.println(class1.hashCode());
    
    FileClassLoader2 loader2 = new FileClassLoader2(urls);
    Class<?> class2 = loader2.findClass("cn.nuc.edu.LogTest.load.Demo");
    System.out.println(class2.getClassLoader());
    System.out.println(class2.hashCode());
  }
}
//輸出:
cn.nuc.edu.LogTest.load.FileClassLoader2@1e0196f8
1391835856
cn.nuc.edu.LogTest.load.FileClassLoader2@2bbd83d
892236228      

注:雖然URLClassLoader有close方法,但例子中沒有調用該方法也可以加載新資源。

2.3)示例2:要熱替換的Demo類無法被AppClassLoader加載到

這裡我們就用FileClassLoader2做個測試:

1)使用loadClass方法:

public class FileClassLoader2 extends URLClassLoader {
  public FileClassLoader2(URL[] urls) {
    super(urls);
  }

  public static void main(String[] args) throws Exception {
    String rootDir = "E:\\";
    URI uri = new File(rootDir).toURI();// File to URI
    URL[] urls = { uri.toURL() };

    FileClassLoader2 loader1 = new FileClassLoader2(urls);
    Class<?> class1 = loader1.loadClass("cn.nuc.edu.LogTest.load.Demo");
    System.out.println(class1.getClassLoader());
    System.out.println(class1.hashCode());
    
    FileClassLoader2 loader2 = new FileClassLoader2(urls);
    Class<?> class2 = loader2.loadClass("cn.nuc.edu.LogTest.load.Demo");
    System.out.println(class2.getClassLoader());
    System.out.println(class2.hashCode());
  }
}
//輸出
cn.nuc.edu.LogTest.load.FileClassLoader2@5fcbc39b
1931573327
cn.nuc.edu.LogTest.load.FileClassLoader2@1a61c596
195780229      

2)使用findClass方法:

public class FileClassLoader2 extends URLClassLoader {
  public FileClassLoader2(URL[] urls) {
    super(urls);
  }

  public static void main(String[] args) throws Exception {
    String rootDir = "E:\\";
    URI uri = new File(rootDir).toURI();// File to URI
    URL[] urls = { uri.toURL() };

    FileClassLoader2 loader1 = new FileClassLoader2(urls);
    Class<?> class1 = loader1.findClass("cn.nuc.edu.LogTest.load.Demo");
    System.out.println(class1.getClassLoader());
    System.out.println(class1.hashCode());
    
    FileClassLoader2 loader2 = new FileClassLoader2(urls);
    Class<?> class2 = loader2.findClass("cn.nuc.edu.LogTest.load.Demo");
    System.out.println(class2.getClassLoader());
    System.out.println(class2.hashCode());
  }
}
//輸出
cn.nuc.edu.LogTest.load.FileClassLoader2@5b202f4d
2053014315
cn.nuc.edu.LogTest.load.FileClassLoader2@52f5bad0
2054262321      

說明:無論使用findClass和loadClass都可以通過多個classLoader示例加載多次同一個類。

3、Tomcat類加載器:

3.1)思考一個問題

我們思考一下:Tomcat是個web容器, 那麼它要解決什麼問題:

  1. 一個web容器可能需要部署兩個應用程式,不同的應用程式可能會依賴同一個第三方類庫的不同版本,不能要求同一個類庫在同一個伺服器隻有一份,是以要保證每個應用程式的類庫都是獨立的,保證互相隔離。
  2. 部署在同一個web容器中相同的類庫相同的版本可以共享。否則,如果伺服器有10個應用程式,那麼要有10份相同的類庫加載進虛拟機。
  3. web容器也有自己依賴的類庫,不能于應用程式的類庫混淆。基于安全考慮,應該讓容器的類庫和程式的類庫隔離開來。
  4. web容器要支援jsp的修改,我們知道,jsp 檔案最終也是要編譯成class檔案才能在虛拟機中運作,但程式運作後修改jsp已經是司空見慣的事情,否則要你何用? 是以,web容器需要支援 jsp 修改後不用重新開機。

Tomcat 如果使用預設的類加載機制行不行?

答案是不行的。為什麼?我們看,第一個問題,如果使用預設的類加載器機制,那麼是無法加載兩個相同類庫的不同版本的,預設的累加器是不管你是什麼版本的,隻在乎你的全限定類名,并且隻有一份。第二個問題,預設的類加載器是能夠實作的,因為他的職責就是保證唯一性。第三個問題和第一個問題一樣。我們再看第四個問題,我們想我們要怎麼實作jsp檔案的熱修改(樓主起的名字),jsp 檔案其實也就是class檔案,那麼如果修改了,但類名還是一樣,類加載器會直接取方法區中已經存在的,修改後的jsp是不會重新加載的。那麼怎麼辦呢?我們可以直接解除安裝掉這jsp檔案的類加載器,是以你應該想到了,每個jsp檔案對應一個唯一的類加載器,當一個jsp檔案修改了,就直接解除安裝這個jsp類加載器。重新建立類加載器,重新加載jsp檔案。

3.2)Tomcat如何實作自己獨特的類加載機制?

我們看到,前面3個類加載和預設的一緻,CommonClassLoader、CatalinaClassLoader、SharedClassLoader和WebappClassLoader則是Tomcat自己定義的類加載器。它們分别加載/common/*、/server/*、/shared/*(在tomcat 6之後已經合并到根目錄下的lib目錄下)和/WebApp/WEB-INF/*中的Java類庫。其中WebApp類加載器和Jsp類加載器通常會存在多個執行個體,每一個Web應用程式對應一個WebApp類加載器,每一個JSP檔案對應一個Jsp類加載器。

  • CommonClassLoader能加載的類都可以被Catalina ClassLoader和SharedClassLoader使用,進而實作了公有類庫的共用,而CatalinaClassLoader和Shared ClassLoader自己能加載的類則與對方互相隔離。
  • WebAppClassLoader可以使用SharedClassLoader加載到的類,但各個WebAppClassLoader執行個體之間互相隔離。
  • JasperLoader的加載範圍僅僅是這個JSP檔案所編譯出來的那一個.Class檔案,它出現的目的就是為了被丢棄:當Web容器檢測到JSP檔案被修改時,會替換掉目前的JasperLoader的執行個體,并通過再建立一個新的Jsp類加載器來實作JSP檔案的HotSwap功能。

3.3)擴充:

如果tomcat 的 Common ClassLoader 想加載 WebApp ClassLoader 中的類,該怎麼辦?