Java ClassLoader详解

3,700 阅读9分钟

翻译原文链接

背景

Java平台旨在提供健壮,安全和可扩展的功能,以支持代码和数据的移动性。Java虚拟机(JVM)中的Java ClassLoader是实现这些目标的关键组件。

JVM负责在Java平台上加载和执行代码。它使用ClassLoader将Java类加载到Java运行时环境中。ClassLoader的架构使得在启动时JVM不需要知道将在运行时加载的类的任何信息。几乎所有基于Java的容器(如EJB或servlet容器)都实现了自定义ClassLoader,以支持热部署和运行时平台可扩展性等功能。 在实现此类基于Java的容器时,深入了解ClassLoaders对于开发人员非常重要。

对于开发部署在这些容器上的组件的企业开发人员,这些知识将帮助您了解容器的工作方式以及调试问题。 本文介绍了Java ClassLoader体系结构,并讨论了ClassLoaders对平台安全性和可扩展性的影响,以及实现用户定义的ClassLoader的方法。

由ClassLoader加载的最小执行单元是Java类文件。类文件包含Java类的二进制表示形式,该类具有可执行字节码和对该类使用的其他类的引用,包括对Java API中的类的引用。换句话说,ClassLoader定位需要加载的Java类的字节码,读取字节码,并创建java.lang.Class类的实例。以便JVM执行。当JVM启动时,不会加载任何内容。会首先加载正在执行的程序的类文件,然后加载其他正在执行的字节码中被引用的类和接口。因此,JVM表现出延迟加载特性,即仅在需要时加载类,在启动时,JVM不需要知道在运行时期间将加载的类。延迟加载在为Java平台提供动态可扩展性方面起着关键作用。通过在程序中实现自定义ClassLoader,可以自定义Java ClassLoader。

Java 2 类委托模型

Java 运行时中存在多个ClassLoaders实例,每个实例都从不同的代码库加载类。例如,Java核心API类由引导程序(或原始)ClassLoader加载。特定应用程序类由系统(或应用程序)ClassLoader加载。另外,应用程序可以定义自己的ClassLoader以从自定义库加载代码。Java 定义了ClassLoaders之间的父子关系。除引导程序ClassLoader之外的每个ClassLoader都有一个父类ClassLoader,从概念上形成了ClassLoader的树状结构。引导程序ClassLoader是此树的根,因此没有父级。这种关系如图所示。

以下是当客户端请求加载类时由ClassLoader执行的高级类加载算法:

  1. 执行检查以查看当前ClassLoader是否已加载所请求的类。如果是,则返回加载的类并完成请求。JVM缓存由ClassLoader加载的所有类。之前由ClassLoader加载的类不会再次加载。
  2. 如果尚未加载类,则在当前ClassLoader尝试加载之前,请求被委托给父ClassLoader。这个委托可以一直到引导程序ClassLoader,之后不再进行进一步的委托。
  3. 如果父级无法加载到类,则当前的ClassLoader将尝试搜索所请求的类。每个ClassLoader都定义了搜索要加载的类的位置。例如,引导程序ClassLoader搜索sun.boot.class.path系统属性中指定的位置(目录和zip / jar文件)。系统ClassLoader在JVM开始执行时传入的类路径(设置为java.class.path系统属性)命令行变量指定的位置中搜索类。如果找到该类,则将其加载到系统中并返回,完成请求。
  4. 如果找不到该类,则抛出java.lang.ClassNotFoundException。

实现Java 2 自定义类加载器

除了引导程序ClassLoader,它是在JVM中的本机代码中实现的外,其他ClassLoader都是通过扩展java.lang.Class.Loader类来实现的。以下代码显示了Java 2 ClassLoader API的相关方法:

public abstract class ClassLoader extends Object {
    protected ClassLoader(ClassLoader parent) {

    }

    protected final Class defineClass( String name,byte[] b,int off,int len) throws ClassFormatError{

    }

    protected Class findClass(String className) throws ClassNotFoundException {

    }

    public Class loadClass(String className) throws ClassNotFoundException {
   
    }
}

根据父委派模型,每个ClassLoader在创建时都会分配一个父级。客户端在ClassLoader的实例上调用loadClass方法来加载类。这启动了前面解释的类加载算法。在Java 2之前,java.lang.ClassLoader类中的loadClass方法被声明为abstract,在扩展java.lang.ClassLoader类时需要自定义ClassLoaders来实现它。实现loadClass方法相当复杂,因此在Java 2中已经改变了。随着ClassLoader父委托模型的引入,java.lang.ClassLoader有一个loadClass方法的实现,它本质上是一个执行类加载的模板方法算法。 loadClass方法在类加载算法的第3步中调用findClass方法(在Java 2中引入)。自定义类加载器应该重写此方法,以提供定位和加载Java类的自定义方法。这极大地简化了自定义ClassLoader的实现。

findClass方法调用loadFromCustomRepository来搜索存储库中的给定类,如果找到,则读取并返回该类的字节码。该类的原始字节码被传递到java.lang.ClassLoader类中实现的defineClass方法,该类返回java.lang.Class对象的实例。 这使得新类可用于正在运行的Java程序。defineClass方法还确保自定义ClassLoader不会通过从自定义存储库加载来重新定义核心Java API类。 如果传递给defineClass的类名以“java”开头,则抛出SecurityException。

应该注意的是,在启动时,JVM不需要知道传递给loadClass方法的字符串所代表的类。

与Java 2委派模型的偏差

Java 2委派模型并非适用于所有情况。在某些情况下,ClassLoader必须与Java 2模型不同。例如,servlet规范建议,实现Web应用程序ClassLoader,以便包含在Web应用程序归档中的类和资源优先于驻留在容器范围的JAR文件中的类和资源。为了满足此建议,Web应用程序ClassLoader应首先在其本地存储库中搜索类和资源,然后再委托父类ClassLoader,从而偏离Java 2委派模型。此建议使Web应用程序可以使用不同于servlet容器使用的类/资源版本。例如,可以使用较新版本的XML解析器中提供的功能而不是servlet容器使用的功能来实现Web应用程序。

可以通过覆盖java.lang.Classloader类的loadClass方法来实现满足servlet规范建议的Web应用程序ClassLoader。

应用程序加载器

ClassLoaders提供了一些可在Java程序中使用的强大功能。

热部署

在正在运行的应用程序中升级软件而不重新启动它称为热部署。对于Java应用程序,热部署意味着在运行时升级Java类。ClassLoaders在基于Java的应用程序服务器中发挥重要作用,以实现热部署。大多数基于Java的应用程序服务器(如EJB服务器和servlet容器)都使用此功能。ClassLoader无法重新加载已加载的类,但使用ClassLoader的新实例会将类重新加载到正在运行的程序中。

ClassLoader customLoader = new CustomClassLoader(repository);

loadAndInvoke(customLoader,classToLoad);

System.out.println("waiting.Hit Enter to continue");

System.in.read();

customLoader = new CustomClassLoader(repository);

loadAndInvoke(customLoader,classToLoad);

创建CustomClassLoader的实例以从指定为命令行参数的存储库加载类。loadAndInvoke加载一个类HelloWorld,它也被指定为命令行参数,并在其实例上调用一个方法,该方法在控制台上输出一条消息。当程序在第6行等待用户输入时,可以更改HelloWorld类(通过更改在控制台上打印的消息)并重新编译。 当程序继续执行时,在第7行创建一个新的CustomClassLoader实例。当loadAndInvoke执行第9行时,它会加载HelloWorld的更新版本,并在控制台上打印一条新消息。

修改类文件

ClassLoader在findClass方法中搜索类文件的字节码。找到字节码并将其读入程序后,可以在调用defineClass之前修改它们。例如,在调用defineClass之前,可能会将额外的调试信息添加到类文件中。某些安全应用程序的类文件数据可以加密存储在存储库中;findClass方法可以在调用defineClass之前解密数据。 程序可以动态生成字节码,而不是从存储库中检索它们。 这构成了JSP技术的基础。

类加载器与安全

由于ClassLoader负责将代码引入JVM,因此它的架构使得平台的安全性不会受到影响。 每个ClassLoader为它加载的类定义一个单独的命名空间,因此在运行时,一个类由其包名和加载它的ClassLoader唯一标识。一个类在其命名空间之外是不可见的;在运行时,在单独的命名空间中存在的类之间存在保护屏障。父委托模型使ClassLoader可以请求由其父级加载的类,因此ClassLoader不需要加载它所需的所有类。

Java运行时存在的各种类加载器具有不同可以从中加载代码的存储库。分离存储库位置可以将不同的信任级别分配给不同的存储库。由引导程序ClassLoader加载的Java运行时库在JVM中具有最高级别的信任。用户定义的ClassLoader的存储库具有较低的信任级别。此外,ClassLoaders可以将每个加载的类分配到保护域中。要根据系统安全策略(java.security.Policy的一个实例)定义代码权限,自定义ClassLoader应扩展java.security.SecureClassLoader类并调用其defineClass方法,该方法将java.security.CodeSource对象作为参数。SecureClassLoader的defineClass方法从系统策略中获取与CodeSource关联的权限,并基于此定义java.security.Protection域。有关安全模型的详细讨论超出了本文的范围。更多细节可以从Bill Venners的Inside the Java Virtual Machine一书中获得。

总结

ClassLoaders提供了一种强大的机制,通过它可以在运行时以有趣的方式扩展Java平台。 自定义类加载器可用于实现正常运行的Java程序可用的功能。 本文已讨论了其中一些应用程序。ClassLoaders在当前J2EE平台提供的一些技术中发挥着重要作用。有关Java类加载机制的更多详细信息,请阅读Java虚拟机内部。