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可以按预期工作。

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