天天看點

jvm的類加載機制其實也沒有想象中那麼難以琢磨jvm的類加載機制

jvm的類加載機制

1、類加載過程

多個java類通過編譯打包成可運作的jar包,最終由java指令運作某個主類的main函數啟動程式。

首先需要通過類加載器把主類加載到jvm。

主類在運作過程中使用其它類的時候,才逐漸加載這些類。

注意,jar包裡面的類不是一次性加載的,是使用到的時候才會加載到jvm中。

類加載到使用的整個過程有如下幾步:

加載>>驗證>>準備>>解析>>初始化>>使用>>解除安裝

  • 加載:在磁盤上查找并通過IO讀入位元組碼檔案,使用到類時才會加載,比如new對象的時候,調用靜态方法的時候等等。
  • 驗證:檢驗文法,校驗位元組碼檔案的正确性。
  • 準備:給類的靜态變量配置設定記憶體,并賦予預設值(比如:static int i = 1,此時給i變量配置設定記憶體,指派為0)
  • 解析:靜态連結(把一些靜态方法替換為指向所存記憶體的指針或句柄)和動态連結(在程式運作期間完成的,将符号引用替換為直接引用)過程
  • 初始化:對類的靜态變量初始化為指定的值将i指派為1,執行靜态代碼塊
    jvm的類加載機制其實也沒有想象中那麼難以琢磨jvm的類加載機制

2、類加載器

類加載過程主要是通過類加載器來實作的,java裡面的類加載器主要有:

  • 啟動類加載器:負責加載支撐jvm運作的位于JRE的lib目錄下的核心類庫,比如rt.jar、charsets.jar等
  • 擴充類加載器:負責加載支撐jvm運作的位于JRE的lib目錄下的ext擴充目錄中的jar類包
  • 應用程式類加載器:負責加載ClassPath路徑下的類包,主要就是加載自己在項目中寫的類
  • 自定義類加載器:負責加載使用者自定義路徑下的類包

檢視各個類别的類加載器:

public class TestJDKClassLoader {

	public static void main(String[] args) {
    	System.out.println(String.class.getClassLoader()); // 啟動類加載器
    	System.out.println(com.sun.crypto.provider.AESKeyGenerator.class.getClassLoader().getClass().getName());// 擴充類加載器
    	System.out.println(TestJDKClassLoader.class.getClassLoader().getClass().getName());// 應用程式類加載器
    	System.out.println(ClassLoader.getSystemClassLoader().getClass().getName());// 應用程式類加載器
	}
}

運作結果:
null
sun.misc.Launcher$ExtClassLoader
sun.misc.Launcher$AppClassLoader
sun.misc.Launcher$AppClassLoader
           

自定義類加載器

自定義類加載器需要繼承java.lang.ClassLoader類,該類定義了兩個核心方法:

1、loadClass(String name, boolean resolve)

loadClass()方法實作了雙親委派機制,大體邏輯如下:

  1. 檢查指定名稱的類是否已經被加載,如果已經加載過了,則不需要再加載,直接傳回
  2. 如果此類沒有被加載,則判斷是否有父級類加載器,如果有父級類加載器,則由父級類加載器加載(parent.loadClass(name, false)),或者是調用bootStrap類加載器來加載
  3. 如果父加載器和bootstrap類加載器都沒有找到指定的類,那麼調用目前類加載器的findClass(String name)方法來完成類加載

ClassLoader中的loadClass()方法源碼如下:

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);// 父級加載器為空,由bootstrap類加載器加載
                  }
              } catch (ClassNotFoundException e) {
                  // ClassNotFoundException thrown if class not found
                  // from the non-null parent class loader
              }

              if (c == null) { // 如果父級類加載器和bootstrap類加載器都沒有找到這個類,則由目前類加載器調用findClass方法加載此類
                  // 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;
      }
  }
           

2、findClass(String name)

findClass()預設是抛出異常,自定義類加載器主要是重寫findClass方法,實作過程如下:

(1)先定義一個User類

public class User
{
  public void userLoad()
  {
    System.out.println("user類被加載 : " + User.class.getClassLoader().getClass().getName());
  }
}
           

(2)将User編譯過後的class檔案放在自定義的目錄

jvm的類加載機制其實也沒有想象中那麼難以琢磨jvm的類加載機制

(3)自定義classLoader加載自定義目錄下的User類

public class MyClassLoader extends ClassLoader {

    private String classPath;
    public MyClassLoader(String classPath) {
        this.classPath = classPath;
    }

    /**
     * 重寫ClassLoader的findClass方法
     * @param name 類名(全限定名)
     * @return
     * @throws ClassNotFoundException
     */
    protected Class<?> findClass(String name)
            throws ClassNotFoundException
    {
        try {
            byte[] data = loadByte(name); // 将指定的class檔案讀取為位元組數組
            return defineClass(name,data,0,data.length); // defineClass 是ClassLoader封裝好的,将位元組數組轉為class對象,直接調用即可
        }catch (Exception e){
            e.printStackTrace();
            throw new ClassNotFoundException();
        }
    }

    /**
     *  将指定的class檔案轉為byte數組
     * @param name
     * @return
     * @throws IOException
     */
    private byte[] loadByte(String name) throws IOException {
        name = name.replaceAll("\\.","/"); // 全類名中的 . 轉為 /  解析為檔案路徑
        FileInputStream fis = new FileInputStream(classPath + "/" + name + ".class");
        int len = fis.available();
        byte[] data = new byte[len];
        fis.read(data);
        fis.close();
        return data;
    }


    public static void main(String[] args) throws Exception {
        MyClassLoader classLoader = new MyClassLoader("F:/classTest");
        Class clazz = classLoader.loadClass("com.std.jvm.classLoader.User"); // 加載自定義位置的User類
        Object obj = clazz.newInstance(); // 建立對象執行個體
        Method method = clazz.getDeclaredMethod("userLoad",null);
        method.invoke(obj,null); // 執行User中定義的方法
    }
}
           

(4)執行結果:

user類被加載 : com.std.jvm.classLoader.MyClassLoader

Process finished with exit code 0
           

3、雙親委派機制

jvm類加載器是有親子層級結構的,如下:

jvm的類加載機制其實也沒有想象中那麼難以琢磨jvm的類加載機制

類加載遵循雙親委派機制,加載某個類時會先委托父加載器尋找目标,如果所有的父加載器在自己加載的類路徑下都找不到目标類,則在目前類加載器的加載路徑中查找并載入目标類。

比如上面自定義加載類加載的User類:

1、最先會找到自定義類加載器(MyClassLoader)加載,自定義類加載器會委托應用程式類加載器(AppClassLoader)加載,應用程式類加載器會委托擴充類加載器(ExtClassLoader)加載,擴充類加載器委托啟動類加載器加載(c語言寫的是以看不到)。

2、頂層的啟動類加載器在自己的類加載路徑下尋找User類,并沒有找到,則向下退回加載的請求,擴充類加載器收到回複則自己加載,尋找自己的類加載路徑下,也沒有找到User類,繼續向下退回請求,應用程式類加載器收到回複在自己的類加載路徑下依然沒有找到User類,繼續向下退回加載請求,最終,自定義類加載器收到回複,在自己的類加載路徑下(F:/classTest)找到了User類(com.std.jvm.classLoader.User),則會加載User類到jvm

3、簡單說就是:類加載的時候,先由父級類加載器加載,父級類加載器沒有加載到再由子級類加載器加載

為什麼要設計雙親委派機制?

  • 沙箱安全機制:自己寫的java.lang.String.class類不會被加載,這樣可以防止java核心類庫被随意修改
  • 避免類的重複:當父級類加載器加載了該類時,就沒有必要子級類加載器再加載一次(要不然用到該類時,應該用哪一個?),保證了被加載類的唯一性

沙箱安全機制示例,比如自定義一個java.lang.String:

package java.lang;
	
public class String {

    public static void main(String[] args) {
        System.out.println("我是java.lang.String,我的類加載器是: "+String.class.getClassLoader().getClass().getName());
    }

}

執行結果為:

錯誤: 在類 java.lang.String 中找不到 main 方法, 請将 main 方法定義為:
public static void main(String[] args)
否則 JavaFX 應用程式類必須擴充javafx.application.Application
           

出現這個情況的原因就是應用程式類加載器加載自定義的java.lang.String的時候,遵循雙親委派機制,會先向上推送加載請求,當頂級啟動類加載器收到此請求時,在自己的加載路徑下找到了java.lang.String,并将其成功加載到了jvm中。最終請求再傳回到應用程式類加載器的時候,loadClass()中判斷java.lang.String已經被加載到了jvm,則不會再繼續加載此類,是以jvm中加載的類并不是目前自定義的String類

雙親委派機制是由loadClass()方法實作的,那麼自定義的類加載器可否重寫loadClass()方法,嘗試打破雙親委派機制,java.lang.String不想上傳送,直接由目前自定義類加載器加載呢?

1、依然采用以上的自定義類加載器,先将自定義的java.lang.String.class存放在自定義的目錄

jvm的類加載機制其實也沒有想象中那麼難以琢磨jvm的類加載機制

2、在自定義的類加載器中重寫loadClass()方法嘗試打破雙親委派

public class MyClassLoader extends ClassLoader {

    private String classPath;
    public MyClassLoader(String classPath) {
        this.classPath = classPath;
    }

    /**
     * 重寫ClassLoader的findClass方法
     * @param name 類名(全限定名)
     * @return
     * @throws ClassNotFoundException
     */
    protected Class<?> findClass(String name)
            throws ClassNotFoundException
    {
        try {
            byte[] data = loadByte(name); // 将指定的class檔案讀取為位元組數組
            return defineClass(name,data,0,data.length); // defineClass 是ClassLoader封裝好的,将位元組數組轉為class對象,直接調用即可
        }catch (Exception e){
            e.printStackTrace();
            throw new ClassNotFoundException();
        }
    }

    /**
     *  将指定的class檔案轉為byte數組
     * @param name
     * @return
     * @throws IOException
     */
    private byte[] loadByte(String name) throws IOException {
        name = name.replaceAll("\\.","/"); // 全類名中的 . 轉為 /  解析為檔案路徑
        FileInputStream fis = new FileInputStream(classPath + "/" + name + ".class");
        int len = fis.available();
        byte[] data = new byte[len];
        fis.read(data);
        fis.close();
        return data;
    }


    /**
     * 重寫類加載方法,目前類加載器加載指定路徑下面的類,不向上委派,直接自己加載
     * @param name
     * @param resolve
     * @return
     * @throws ClassNotFoundException
     */
    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();

                // 如果目标類未被加載,不判斷父級類加載器,直接自己來加載
                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    c = findClass(name); // 直接調用自己的findclass方法

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

    public static void main(String[] args) throws Exception {
        MyClassLoader classLoader = new MyClassLoader("F:/classTest");
        Class clazz = classLoader.loadClass("java.lang.String"); // 加載自定義位置的String類
        Object obj = clazz.newInstance(); // 建立對象執行個體
    }
}

執行結果:

	java.lang.SecurityException: Prohibited package name: java.lang
	at java.lang.ClassLoader.preDefineClass(ClassLoader.java:662)
	at java.lang.ClassLoader.defineClass(ClassLoader.java:761)
           

抛異常說非法的命名,這個是沙箱安全機制的非法命名驗證,可以說明重寫loadClass方法以後,确實沒有向上委派,加載的是目前的自定義的java.lang.String類。

現實應用中打破雙親委派的執行個體:tomcat

以Tomcat類加載為例,Tomcat 如果使用預設的雙親委派類加載機制行不行?

我們思考一下: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檔案。

Tomcat自定義加載器詳解

jvm的類加載機制其實也沒有想象中那麼難以琢磨jvm的類加載機制

tomcat的幾個主要類加載器:

  • commonLoader:Tomcat基本的類加載器,加載路徑中的class可以被 Tomcat容器本身以及各個Webapp通路;
  • catalinaLoader:Tomcat容器私有的類加載器,加載路徑中的class對于 Webapp不可見;
  • sharedLoader:各個Webapp共享的類加載器,加載路徑中的class對于所有 Webapp可見,但是對于Tomcat容器不可見;
  • WebappClassLoader:各個Webapp私有的類加載器,加載路徑中的class隻對 目前Webapp可見;

從圖中的委派關系中可以看出:

CommonClassLoader能加載的類都可以被CatalinaClassLoader和SharedClassLoader使用,進而實作了公有類庫的共用,而CatalinaClassLoader和SharedClassLoader自己能加載的類則與對方互相隔離。

WebAppClassLoader可以使用SharedClassLoader加載到的類,但各個 WebAppClassLoader執行個體之間互相隔離。

而JasperLoader的加載範圍僅僅是這個JSP檔案所編譯出來的那一個.Class檔案,它出現的 目的就是為了被丢棄:當Web容器檢測到JSP檔案被修改時,會替換掉目前的 JasperLoader的執行個體,并通過再建立一個新的Jsp類加載器來實作JSP檔案的熱加載功能。

tomcat 這種類加載機制違背了java 推薦的雙親委派模型了嗎?答案是:違背了。 我們前面說過,雙親委派機制要求除了頂層的啟動類加載器之外,其餘的類加載器都應當由 自己的父類加載器加載。

自己能加載的類則與對方互相隔離。

WebAppClassLoader可以使用SharedClassLoader加載到的類,但各個 WebAppClassLoader執行個體之間互相隔離。

而JasperLoader的加載範圍僅僅是這個JSP檔案所編譯出來的那一個.Class檔案,它出現的 目的就是為了被丢棄:當Web容器檢測到JSP檔案被修改時,會替換掉目前的 JasperLoader的執行個體,并通過再建立一個新的Jsp類加載器來實作JSP檔案的熱加載功能。

tomcat 這種類加載機制違背了java 推薦的雙親委派模型了嗎?答案是:違背了。 我們前面說過,雙親委派機制要求除了頂層的啟動類加載器之外,其餘的類加載器都應當由 自己的父類加載器加載。

很顯然,tomcat 不是這樣實作,tomcat 為了實作隔離性,沒有遵守這個約定,每個 webappClassLoader加載自己的目錄下的class檔案,不會傳遞給父類加載器,打破了雙 親委派機制。