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

本文深入探讨了Java类加载过程中较复杂且罕见的问题,包括类路径配置错误、类可见性问题、loadClass方法重写注意事项及序列化对垃圾回收的影响。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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

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

术中心开发团队, IBM Hursley 实验室

2006 年 1 月 16 日

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

本文是本系列中四篇文章的第三篇,它研究了在 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 类有一个到其定义类装入器的引用,所以它也不可能完整地取消引用。

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



回页首


下期预报

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



回页首


参考资料

学习

获得产品和技术

讨论
  • 加入本文的论坛 。(您也可以通过点击文章顶部或者底部的论坛链接参加讨论。)

  • developerWorks blogs:加入 developerWorks 社区。


回页首


作者简介

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 中。他现在正在进行组件化的工作。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值