Java 如何卸載類

Java 類卸載

起先

Java 旨在動態加載和卸載類。 類以類文件的形式放置在磁盤或網絡上,並在程序中真正需要它們時加載到 JavaVM 上。 類也由垃圾回收器動態檢索,並在不再使用時從 JavaVM 中卸載。

Servlet / J2EE服務器利用此屬性來實現熱交換,即在操作期間交換程序的一部分。 但是,實現此機制需要一點獨創性。

本文檔介紹如何實現類卸載。

1. 類加載和卸載的基本機制

類裝入器

在Java VM讀取類時,類加載器(Class Loader)起着重要的作用。形象地說,就像裝着類別的容器一樣。對象(java.lang.Object)是類(java.lang.Class)類屬於某個類加載器(java.lang.ClassLoader),從而屬於類。

Java VM在剛啓動時只存在一個默認的類加載器,稱爲BootstrapClassLoader。這個類加載器是爲了讀取不指定CLASSPATH也能加載的特殊類,例如從java.開頭的包的類。

BootstrapClassLoader讀取的類在Class.getClassLoader()中的值爲null。

Object  anObject    = new Object();
Class   objectClass = anObject.getClass(); 

System.out.println(objectClass.getClassLoader());  // null
System.out.println(Object.class.getClassLoader()); // null

從J2SE1.3開始,自舉類加載器讀取的是jdk/jre中包含的rt.jar。如果想指定別的類路徑,可以通過指定-Xbootclasspath:path選項來變更。 另外-Xbootclasspath/p:指定path選項後自舉類加載器可以在-Xbootclasspath之前指定要讀取的類文件。-Xbootclasspath/a:指定path選項後還可以指定要在-Xbootclasspath之後讀取的類文件。

還有一個被稱爲系統類加載器的類加載器會被隱式地創建。系統類加載器用於加載放置在 CLASSPATH 位置的類。啓動類和用戶編寫的類文件都會被系統類加載器加載。

語言規範沒有規定系統類加載器具有什麼類型,但是 SUN、IBM、BEA 等 JavaVM 的實現中,默認的系統類加載器是 rt.jar 中包含的 sun.misc.Launcher.AppClassLoader

class  Test {
  public static void main(String[] args) {
    //   
    System.out.println(Test.class.getClassLoader());
  }
}

如果執行該操作,則顯示爲sun.misc.Launcher$AppClassLoader@1a5ab41(數字部分在每次執行時變化)。這個實例是ClassLoader.getSystemClassLoadser()的返回值和一致的。

系統類加載器可以通過設置java.system.class.loader配置文件來改變爲獨一無二的。必須將自己定義的系統類加載器的類文件放在可從提升類加載器讀取的位置上。

除此之外的類加載通過派生java.lang.ClassLoader類並生成其實例來創建(ClassLoader是抽象類,不能直接生成)。另外還可以使用java.net.URLClassLoader等庫準備的類加載類。

如果想在自己生成的類加載器上讀取類,嘗試loadClass(String className)方法。下面的程序創建了兩個自己定義的類加載器,分別讀取名爲jp.nminoru.Hoge的類。 但是,如後所述,jp.nminoru.Hoge類不一定被讀取到ClassLoader上。

MyClassLoader aLoader1 = new MyClassLoader();
MyClassLoader aLoader2 = new MyClassLoader();

// 即使是相同的類名稱,也被認爲是不同的。。
Class aClass1 = aLoader1.loadClass("jp.nminoru.Hoge");
Class aClass2 = aLoader2.loadClass("jp.nminoru.Hoge");

雙親委派

如下圖所示,ClassLoader擁有親子關係。

這種親子關係是實例級的,與來自ClassLoader類的派生關係無關。如下圖所示的Derived ClassLoader 1 ~ 3都是MyClassLoader類型的實例。

BootstrapClassLoader以外的類加載器各有一個父類加載器。默認情況下新類裝入器由在其中創建它的類的類裝入器作爲父級。BootstrapClassLoader是頂級節點沒有父類。

                                           ------------------
                                                 Derived 
                                              Class Loader3
                                           ------------------
                                                         加載(Class)
                                                   ↓

    ------------------                     ------------------
        Derived                                  Derived 
      Class Loader1                            Class Loader2
    ------------------                     ------------------
(Class)   ╲                                   ╱         加載(Class)
           ↘                                ↙
                    -------------------
                        Bootstrap
                       Class Loader
                    -------------------

此類類父子關係用於類查找的委託模型。

  • 類加載器可以使用其父加載器(父類的父類,也是父類的父類)已調用的類。
  • 裝入新類時,首先將類搜索委託給父加載器。 如果指定的類可以自行解決,則父類將加載該類。如果無法解析,處理將返回到子加載器,子加載器(以自己的方式)裝入類。 在上面的程序中,我們創建了MyClassLoader實例loader類加載器,讀取jp.nminoru.Hoge類。但是,如果loader的父類能夠解決jp.nminoru.Hoge類,則jp.nminoru.Hoge類將由父類而不是loader讀取。

可以通過重寫 loadClass(String name)或 loadClass(String name,boolean resolve) 來破壞委託模型,但不建議這樣做。 或者更確切地說,永遠不要這樣做。 Java VM 使用 loadClass(String name) 進行隱式類加載(不使用 ClassLoader 類),而 loadClass(String name)只是 loadClass(String name,boolean resolve) 正在召喚。 因此,如果重寫 loadClass,委託給父加載器將非常麻煩,並且很有可能發生不可避免的錯誤。

卸載類

類的卸載發生在不再需要該類的時候。不再需要類的條件必須滿足全部以下三個條件:

  1. 從堆中消失該類的實例。
  2. 沒有線程正在執行該類的static方法。
  3. 使加載該類的類加載器出現的ClassLoader派生實例從堆中消失。

由於實現的原因,很多Java VM在GC時進行類是否滿足1 ~ 3的條件的判斷。

積極利用類卸載的情況下,3.成爲問題。

使用下面的程序來說明的話,調用MyClassLoaderloadClass方法的jp.nminoru.Hoge類是myLoader不能保證在裏面。因爲myLoader的父加載器有可能解決並加載jp.nminoru.Hoge類。在這種情況下,即使丟棄myLoader,也不會卸載jp.nminoru.Hoge

MyClassLoader myLoader = new MyClassLoader();

Class aClass = myLoader.loadClass("jp.nminoru.Hoge");

myLoader = null;
aClass   = null;

System.gc(); 

此外,只要 Java VM 存在,BootstrapClassLoader類加載器就不會消失。 因此,BootstrapClassLoader類加載器裝入的類永遠不會被卸載。 有必要考慮系統類加載器在正常的庫實現中也沒有卸載。

2. 如何卸載類

考慮在程序運行過程中,創建一個加載和卸載部分類的程序。

首先,作爲程序的設計,需要區分不卸載的類和要卸載的類。 不卸載的類用系統類加載器讀取。$(JRE)/lib/rt.jar JAR文件,基本上存儲了java.*等系統定義的類,以及java VM啓動時的-classpath中定義的路徑上的類文件JAR文件作爲搜索的對象。沒有卸載的類將被放置在這個-classpath上。

另一方面,將要卸載的類保存在不被讀取到系統類加載器的位置,從可卸載的類加載器讀取。在創建可卸載類的類加載器時,從java.net.URLClassLoader進行派生是很方便的。 在URLClassLoader中,Java VM的-classpath中沒有指定的路徑可以作爲搜索路徑。 在搜索路徑中包括本地盤的目錄時,file:/directory1/(以/結尾);在搜索路徑中包括本地盤上的JAR文件時, jar:/directory2/file.jar!/ 指定。

import java.net.URL;
import java.net.URLClassLoader;

URLClassLoader loader = new URLClassLoader( new URL[] { 
                                              new URL("file:/directory1/"),
           new URL("jar:/directory2/file.jar!/"),
           } );

// 類的加載
loader.loadClass( ...);

// 卸載
loader = null;
System.gc();

如果你想以與Java VM的-classpath參數相同的形式給出放置可卸載類的類路徑,請參考以下convertClasspathToURLs利用這樣的轉換方法(編譯這個程序需要J2SE v1.4以上)。 在識別系統的路徑分隔字符等的基礎上,將其轉換爲URL排列。

import java.io.File;
import java.io.InputStream;
import java.io.IOException; 
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.regex.PatternSyntaxException;
import java.util.Vector;

public static URL[] convertClasspathToURLs(String classpath) {
  Vector tmpArray = new Vector();
  URL[] urls = null;

  try {
    String[] parts = classpath.split(File.pathSeparator);
            
    for (int i=0 ; i<parts.length ; i++) {
      final String path = parts[i];

      try {
        URL url = null;
        final String postfix = path.substring(path.length() - 4, path.length());
        if (postfix.equalsIgnoreCase(".jar") || postfix.equalsIgnoreCase(".zip")) {
          final String base = (new File(path).getCanonicalFile().toURL()).toString();
          url = new URL("jar:" + base + "!/");
        } else {
          url = new File(path).getCanonicalFile().toURL();
        }

        tmpArray.add(url);
      } catch(IOException e) {
        // through
      }
    }
  } catch(PatternSyntaxException e) {
    throw new IllegalArgumentException();
  } catch(NullPointerException e) {
    throw new IllegalArgumentException();
  }

  urls = new URL[tmpArray.size()]; 
	
  for(int j=0 ; j<tmpArray.size() ; j++) {
    urls[j] = (URL) tmpArray.get(j);
  }

  return urls;
}

3. 示例程序

UnloadableClassLoader.java 這是使用類可裝入類加載器的示例。

首先,創建一個實現 main 方法的可執行 SampleProgram 類。 將其放在適當的目錄中(例如:/home/nminoru/program/)。 要在 Java 中執行此操作而不移動目錄: 不要在 CLASSPATH 環境中包含 /home/nminoru/program/

java -cp /home/nminoru/program/ SampleProgram arg1 arg2 ...

在上述方法中,SampleProgram 由引導類加載器加載。 它被執行。

下一個 UnloadableClassLoader.java編譯的UnloadableClassLoader.class 發生在 CLASSPATH 環境的路徑經過的點, 運行以下命令:

java UnloadableClassLoader /home/nminoru/program/ SampleProgram arg1 arg2 ...

在此示例中, 將 SampleProgram 加載到不可裝入的類加載器後, 它現在將運行。

AppLoader.java 破壞類加載器委派關係, 劫持系統類加載器的示例。

AppLoader 繼承自 URLClassLoader。 將類路徑設置爲搜索路徑。 但是,如果在類路徑中指定了目錄, 直接位於該目錄下的 JAR 文件也將自動包含在搜索路徑中。 通過重寫 AppLoader 方法, 而不是將類解析委託給系統類加載器, 將類解析委託給系統類加載器的父類加載器。 也就是說,跳過系統類加載器。 loadClass 系統類加載器的父類加載器是 由於它是受保護的方法,因此不能按原樣調用它。 使用反射強制調用。 loadClass(String name, boolean resolve) 要使用它,對於在類路徑中指定多個 JAR 文件的程序,

java -cp .:./lib/A.jar:./lib/B.jar:./lib/C.jar SampleProgram arg1 arg2 ...

如果您咬了應用程序加載程序並且以下內容是 自動將 JAR 文件添加到搜索路徑。 ../lib/

java -cp .:./lib/ AppLoader SampleProgram arg1 arg2 ...

您無法阻止提取 JAR 文件。 不應放置與類路徑中指定的目錄無關的 JAR 文件。

4. 贈品

如何監控類卸載

要了解某個類是否已從系統中卸載,只需檢查與要檢查的類對應的實例是否已被垃圾回收回收。 檢查弱引用的實例。 java.lang.Class

import java.lang.ref.WeakReference;

Class targetClass = ...
WeakReference targetClassWR = new WeakReference(targetClass);

// ...
 
if (targetClassWR.get() != null) {
  // targetClass 所指向的類仍被讀入。
} else {
  // targetClass 所指向的類已經卸載。
}

但是,目前尚不清楚 Java 語言規範是否允許此方法。 根據第三版的 Java 語言規範,類對象由 Java 虛擬機在裝入類時通過調用類加載器的 defineClass 方法自動構造。 但是,不能保證在卸載類時將銷燬 Class 對象。 但是Sun,IBM和BEA JavaVM可以按預期工作。

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