跳转到主要内容

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件.

当您初次登录到 developerWorks 时,将会为您创建一份概要信息。您在 developerWorks 概要信息中选择公开的信息将公开显示给其他人,但您可以随时修改这些信息的显示状态。您的姓名(除非选择隐藏)和昵称将和您在 developerWorks 发布的内容一同显示。

所有提交的信息确保安全。

  • 关闭 [x]

当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。

昵称长度在 3 至 31 个字符之间。 您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件.

所有提交的信息确保安全。

  • 关闭 [x]

类装入问题解密,第 3 部分: 处理更少见的类装入问题

理解类装入并解决微妙的异常

Lakshmi Shankar, Java 技术中心开发团队, IBM Hursley 实验室
Lakshmi Shankar
Lakshmi Shankar 是英国 IBM Hursley 实验室的软件工程师。他为 IBM 工作超过两年了,有广泛的经验,一直在 Hursley 实验室从事 Java 性能、测试和开发工作。他目前是 IBM Java 技术的类装入组件的所有人。他现在是信息管理团队的一名开发人员。
Simon Burns (simon_burns@uk.ibm.com), Java 技术中心开发团队, IBM Hursley 实验室
Simon Burns
Simon Burns 是 Shiraz(可重置 JVM 和 IBM Java 共享类)组件的所有人,也是 IBM Hursley 实验室的 Java 技术团队负责人。他在 JVM 开发上工作了三年,专攻 Shiraz 组件和 z/OS 平台。他和 CICS 紧密合作,帮助他们利用这项技术。Simon 开发的 OSGi 框架是开放源码的 Eclipse Equinox 项目的一部分,已经集成到 Eclipse 3.1 中。他现在正在进行组件化的工作。

简介: 这个四部分构成的文章系列研究 Java™ 的类装入问题,帮助应用程序开发人员理解和调试可能遇到的问题。在第 3 部分中,来自 IBM Hursley 实验室的作者 Lakshmi Shankar 和 Simon Burns 在本系列前两部分的基础之上,详细介绍了不同种类的类装入问题,包括与类路径、类可视性和垃圾收集有关的问题。

查看本系列更多内容

发布日期: 2006 年 1 月 16 日
级别: 中级
访问情况 : 927 次浏览
评论: 


本文是本系列中四篇文章的第三篇,它研究了在 Java 开发过程中的一些更复杂、更少见的类装入问题。造成这些问题的原因通常无法直接从症状得出;所以,解决起来既困难又费时。与本系列以前的文章一样,我们仍然提供示例来演示问题,然后讨论各种解决技术。

在开始这篇文章之前,应当熟悉类装入委托模型,以及类链接的阶段和过程。我们强烈建议您从阅读本系列的 第一篇文章 开始。

与类路径有关的问题

有一个非常简单的问题通常与用户设置的类路径有关。清单 1 和清单 2 中的示例演示了这个问题。

测试用例创建了两个类装入器,每个类装入器使用的类路径看起来相同。但是,有一个微小但是却重大的区别:一个类路径末尾有 /,而另一个没有。在这两个类路径中的一个名为 cp 的子目录中有一个类 Z(在清单 2 中)。两个类装入器都试图装入 Z


清单 1. ClasspathTest.java
				
import java.net.URL;
import java.net.URLClassLoader;
public class ClasspathTest {
	
    String userDir;
    URL withSlash;
    URL withoutSlash;
    ClasspathTest() {
        try {
            userDir = System.getProperty("user.dir");
            withSlash = new URL("file://C:/CL_Article/ClasspathIssues/cp/");
            withoutSlash = new URL("file://C:/CL_Article/ClasspathIssues/cp");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    void run() {
        try {
            System.out.println(withSlash);
            URLClassLoader cl1 = new URLClassLoader(new URL[] { withSlash });
            Class c1 = cl1.loadClass("Z");
            System.out.println("Class Z loaded.");
            System.out.println(withoutSlash);
            URLClassLoader cl2 = new URLClassLoader(new URL[] { withoutSlash });
            Class c2 = cl2.loadClass("Z");
            System.out.println("Class Z loaded.");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    public static void main(String[] args) {
        new ClasspathTest().run();
    }
}


清单 2. Z.java
				
public class Z {
}

这个测试用例产生以下输出:

file://C:/CL_Article/ClasspathIssues/cp/
Class Z loaded.
file://C:/CL_Article/ClasspathIssues/cp
java.lang.ClassNotFoundException: Z
    at java.net.URLClassLoader.findClass(URLClassLoader.java:376)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:572)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:504)
    at ClasspathTest.run(ClasspathTest.java:28)
    at ClasspathTest.main(ClasspathTest.java:36)

可以看到,传递给每个 URLClassloader 的参数略有不同。提供给第一个类装入器 cl1 的类路径末尾有 /。提供给第二个类装入器 cl2 的类路径末尾没有 /。这个区别是显著的,因为类装入器假设不以 / 结尾的路径指向的是 JAR 文件。只有以 / 结尾的路径才被假定为指向目录。

因为 cl1 的类路径被当作目录,所以这个类装入器能够找到在这个位置的类 Z,并能够装入它。cl2 的类路径被假定为 JAR 文件;这个类装入器不能发现类 Z ,因为没有这个文件。所以,cl2.loadClass() 抛出 ClassNotFoundException

显然,修复这个问题的方法是确保指向目录的路径以 / 结尾。


与类的可视性有关的问题

在系统中,可能有许多类装入器看不到的类。这是因为类装入器只能看到它自己装入的类,或者它有引用(直接或间接)的其他类装入器装入的类。在标准的类装入委托模型中,类装入器能看到的类被限制在它自己装入的那些类上,或者它的双亲和祖先类装入器装入的类 —— 换句话说,类装入器不能向下看。

图 1 演示了这类问题的示例:


图 1. 可视性示例
可视性示例

A 在系统类装入器的类路径中,而 A 的超类 B,在用户自定义的类装入器的类路径中,这个类装入器是系统类装入器的孩子。当系统类装入器试图装入类 A 时,装入失败,因为它看不到类 B。这是因为 B 不在系统类装入器或者它的双亲或祖先类装入器的类路径中。

清单 3 到 5 的测试用例实现了这个场景:


清单 3. VisibilityTest.java
				
import java.net.*;
public class VisibilityTest {
    public static void main(String[] args) {
        try {
            URLClassLoader mycl = new URLClassLoader(new URL[] { new URL(
                "file://C:/CL_Article/VisibilityTest/cp/") });
            Class c2 = mycl.loadClass("A");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}


清单 4. A.java
				
public class A extends B {
    public static void method1() {
        System.out.println("HELLO!");
    }
}


清单 5. B.java
				
public class B {
}

这个测试用例产生以下输出:

Exception in thread "main" java.lang.NoClassDefFoundError: B
    at java.lang.ClassLoader.defineClass0(Native Method)
    at java.lang.ClassLoader.defineClass(ClassLoader.java:810)
    at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:147)
    at java.net.URLClassLoader.defineClass(URLClassLoader.java:475)
    at java.net.URLClassLoader.access$500(URLClassLoader.java:109)
    at java.net.URLClassLoader$ClassFinder.run(URLClassLoader.java:848)
    at java.security.AccessController.doPrivileged1(Native Method)
    at java.security.AccessController.doPrivileged(AccessController.java:389)
    at java.net.URLClassLoader.findClass(URLClassLoader.java:371)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:572)
    at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:442)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:563)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:504)
    at VisibilityTest.main(VisibilityTest.java:9)
    at VisibilityTest.main(VisibilityTest.java:10)

解决类可视性问题的惟一方法是确保所有的应用程序类都可见。为了确保类的可视性而如何确切安放类,取决于使用的不同的类装入模型。但是,如果正在使用标准的类装入委托模型,那么类的可视性就是一个简单的问题:所要做的全部工作就是确保没有引用指向更低的类空间。例如,在系统类装入器类空间中的类,不应当指向孩子或子孙类装入器的类空间中的类。


重载 loadClass() 时的问题

如果类装入器只使用标准委托模型,那么就不需要重载 loadClass() 方法。但是,如果需要不同的模型,那么就必须重载 loadClass(),在这种情况下,必须重视一些特殊的考虑因素。

委托

清单 6 是 loadClass() 的简单实现:


清单 6. 简单的 loadClass() 实现
				
public Class loadClass(String name) throws ClassNotFoundException {
    return findClass(name);
}

虽然这看起来合理,但是对这个方法的调用会导致以下异常:

Exception in thread "main" java.lang.NoClassDefFoundError: java/lang/Object

这个异常的抛出,是因为重载的 loadClass() 方法不再委托给它的双亲。这个实现假设所有需要的类都在这个类装入器的类路径中。这个实现从基本上来说是有缺陷的,因为所有的类(隐式地)都扩展了 java.lang.Object,而后者必须是引导类装入器所装入的版本。

可以通过修改 loadClass() 的实现来修复这个问题,如清单 7 所示:


清单 7. 改进的 loadClass() 实现
				
public Class loadClass(String name) throws ClassNotFoundException {
    Class c = null;
    try {
        c = getParent().loadClass(name);
    } catch (ClassNotFoundException e) {
    }
    if(c == null)
        c = findClass(name);
    return c;
}

方法现在在试图自己找到类之前,先委托给自己的双亲类装入器。这意味着它现在找到了通过引导类装入器装入的 java.lang.Object

缓存

虽然清单 7 提供的 loadClass() 委托实现解决了这个问题,但是实现仍然是不完整的。在使用这个版本的 loadClass() 时,还会出现另一个问题。下面就是该异常在 IBM JVM 中看起来的样子:

Exception in thread "main" java.lang.LinkageError: 
    JVMCL048:redefine of class A (&name=44CA3B08). old_cb=ACEE80,
     new_cb=ACED50, (&old_name=44CA3B08) old_name=A

下面是在 Sun JVM 中的样子:

Exception in thread "main" java.lang.LinkageError: duplicate class definition: A

这个异常发生的原因是,应用程序要求类装入器装入同一个类两次,而 loadClass() 则试图从头开始重新装入类。这造成了两个版本之间的冲突。这个问题可以在 loadClass() 中处理,先检查类装入器的缓存。如果在缓存中发现了类,那么就返回这个版本。这个逻辑被添加到了清单 8 中的 loadClass() 方法版本中:


清单 8. loadClass(),进一步细化
				
public Class loadClass(String name) throws ClassNotFoundException {
    Class c = findLoadedClass(name);
    if(c == null) {
        try {
            c = getParent().loadClass(name);
        } catch (ClassNotFoundException e) {
        }
        if(c == null)
            c = findClass(name);
    }
    return c;
}

这个方法现在工作得很好;但是,它现在遵循的是标准类装入委托(缓存、双亲、磁盘)。当然,如果要求标准委托模型,那么不需要首先重载 loadClass()。可以编写不符合标准委托模型的有用的 loadClass() 方法,后果是可能会出现潜在的问题,但是这超出了本系列的范围。


与垃圾收集和序列化有关的问题

垃圾收集器与类装入器的交互很密切。在众多的事情当中,收集器检查类装入器的数据结构,来判断哪个类是活动的 —— 也就是说,不应当被当作垃圾收集的。这通常会带来一些意料之外的问题。

图 2 演示了一个场景,在这个场景中,序列化以一种意料之外的方式影响了类的垃圾收集(GC):


图 2. 序列化示例
序列化示例

在这个示例中,SerializationTest 实例化了一个 URLClassLoader,叫做 loader。在装入 SerializationClass 之后,对类装入器的引用被取消。想法是希望这样可以允许类装入器装入的类被垃圾收集掉。这些类的代码如清单 9 和 10 所示:


清单 9. SerializationTest.java
				
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
public class SerializationTest extends ClassLoader {
   public static void main(String args[]) {
      try {
         URLClassLoader loader = new URLClassLoader(new URL[] { new URL(
               "file://C:/CL_Article/Serialization/dir1/") });
         System.out.println("Loading SerializationClass");
         Class c = loader.loadClass("SerializationClass");
         System.out.println("Creating an instance of SerializationClass");
         c.newInstance();
         System.out.println("Dereferencing the class loader");
         c = null;
         loader = null;
         
         System.out.println("Running GC...");
         System.gc();
         System.out.println("Triggering a Javadump");
         com.ibm.jvm.Dump.JavaDump();
         
      } catch (MalformedURLException e) {
         e.printStackTrace();
      } catch (InstantiationException e) {
         e.printStackTrace();
      } catch (IllegalAccessException e) {
         e.printStackTrace();
      } catch (ClassNotFoundException e) {
         e.printStackTrace();
      }
   }
}


清单 10. SerializationClass.java
				
import java.io.File;
import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
public class SerializationClass implements Serializable {
    private static final long serialVersionUID = 5024741671582526226L;
    public SerializationClass() {
        try {
            File file = new File("C:/CL_Article/Serialization/test.txt");
            FileOutputStream fos = new FileOutputStream(file);
            ObjectOutputStream oos = new ObjectOutputStream(fos);
            oos.writeObject(this);
            oos.reset();
            oos.close();
            fos.close();
            oos = null;
            fos = null;
            file = null;
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

使用 Javadump,可以发现类装入器是否被垃圾收集了。(关于使用 Javadump 的更多信息,请参阅本系列的第一篇文章。)如果在类装入器的列表中出现以下部分,就说明它没有被收集:

------a- Loader java/net/URLClassLoader(0x44DC6DE0), Shadow 0x00ADB6D8,
        Parent sun/misc/Launcher$AppClassLoader(0x00ADB7B0) 
        Number of loaded classes 1 
        Number of cached classes 11      
        Allocation used for loaded classes 1      
        Package owner 0x00ADB6D8

虽然取消对用户定义的类装入器的引用看起来像是一种确保类被垃圾收集的方法,但实际并不是这回事。在前面的示例中,由于 java.io.ObjectOutputStream.writeObject(Object obj) 的使用以及它对 GC 的影响,所以产生了问题。

在调用 writeObject() 时(用来序列化 SerializationClass),对这个类对象的引用就在内部被传递给 ObjectStreamClass 并保存在一个查询表中(也就是内部缓存)。保存这个引用是为了加快日后对同一个类的序列化。

当取消对类装入器的引用时,它装入的类就变成无法进行垃圾收集的了。这是因为在 ObjectStreamClass 查询表中,没有了对 SerializationClass 类的活动引用。ObjectStreamClass 是一个原始类,所以永远不会被垃圾收集。查询表是从 ObjectStreamClass 中的静态字段引用的,而且保存在类本身之中,而不是保存在实例中。所以,对 SerializationClass 的引用存在于 JVM 的生命周期中,所以类就不能被垃圾收集。重要的是,SerializationClass 类有一个到其定义类装入器的引用,所以它也不可能完整地取消引用。

为了避免这个问题,凡是要进行序列化的类,都应当由不需要被垃圾收集的类装入器装入 —— 例如由系统类装入器装入。


下期预报

在这篇文章中,学习了一些在类装入中可能发生的更复杂的问题。在本系列的最后一篇文章中,将研究两个可能发生的最复杂的问题:死锁和违反约束。


参考资料

学习

获得产品和技术

讨论

作者简介

Lakshmi Shankar

Lakshmi Shankar 是英国 IBM Hursley 实验室的软件工程师。他为 IBM 工作超过两年了,有广泛的经验,一直在 Hursley 实验室从事 Java 性能、测试和开发工作。他目前是 IBM Java 技术的类装入组件的所有人。他现在是信息管理团队的一名开发人员。

Simon Burns

Simon Burns 是 Shiraz(可重置 JVM 和 IBM Java 共享类)组件的所有人,也是 IBM Hursley 实验室的 Java 技术团队负责人。他在 JVM 开发上工作了三年,专攻 Shiraz 组件和 z/OS 平台。他和 CICS 紧密合作,帮助他们利用这项技术。Simon 开发的 OSGi 框架是开放源码的 Eclipse Equinox 项目的一部分,已经集成到 Eclipse 3.1 中。他现在正在进行组件化的工作。

关于报告滥用的帮助

报告滥用

谢谢! 此内容已经标识给管理员注意。


关于报告滥用的帮助

报告滥用

报告滥用提交失败。 请稍后重试。


developerWorks:登录


需要一个 IBM ID?
忘记 IBM ID?


忘记密码?
更改您的密码

单击提交则表示您同意developerWorks 的条款和条件。 使用条款

 


当您初次登录到 developerWorks 时,将会为您创建一份概要信息。您在 developerWorks 概要信息中选择公开的信息将公开显示给其他人,但您可以随时修改这些信息的显示状态。您的姓名(除非选择隐藏)和昵称将和您在 developerWorks 发布的内容一同显示。

请选择您的昵称:

当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。

昵称长度在 3 至 31 个字符之间。 您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。

(长度在 3 至 31 个字符之间)


单击提交则表示您同意developerWorks 的条款和条件。 使用条款.

 


为本文评分

评论

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Java technology
ArticleID=101824
ArticleTitle=类装入问题解密,第 3 部分: 处理更少见的类装入问题
publish-date=01162006
author1-email=shankarl@uk.ibm.com
author1-email-cc=Copy email address
author2-email=simon_burns@uk.ibm.com
author2-email-cc=Copy email address

标签

Help
使用 搜索 文本框在 My developerWorks 中查找包含该标签的所有内容。

使用 滑动条 调节标签的数量。

热门标签 显示了特定专区最受欢迎的标签(例如 Java technology,Linux,WebSphere)。

我的标签 显示了特定专区您标记的标签(例如 Java technology,Linux,WebSphere)。

使用搜索文本框在 My developerWorks 中查找包含该标签的所有内容。热门标签 显示了特定专区最受欢迎的标签(例如 Java technology,Linux,WebSphere)。我的标签 显示了特定专区您标记的标签(例如 Java technology,Linux,WebSphere)。