面试必考之类的加载器

2020-05-05  本文已影响0人  bern85

类加载器简介

类加载器负责在运行时将Java class动态加载到JVM(Java虚拟机)。而且,它们是JRE(Java运行时环境)的一部分。 因此,由于类加载器的缘故,JVM无需了解底层文件或文件系统即可运行Java程序。

而且,这些Java class不会一次全部加载到内存中,而是在应用程序需要时加载。 这是类加载器发挥作用的时候, 他们负责将类加载到内存中。

在本篇内容中,我们将讨论不同类型的内置类加载器,它们如何工作以及自定义实现的介绍。

内置的类加载器类型

让我们从一个简单的示例开始学习如何使用各种类加载器加载不同的类:

public class ClassLoaderTest {
    @Test
    public void printClassLoaders() throws ClassNotFoundException {
        System.out.println("Classloader of this class:" + ClassLoaderTest.class.getClassLoader());

        System.out.println("Classloader of Logging:" + Logging.class.getClassLoader());

        System.out.println("Classloader of ArrayList:" + ArrayList.class.getClassLoader());
    }
}

// console output
Classloader of this class:sun.misc.Launcher$AppClassLoader@18b4aac2
Classloader of Logging:sun.misc.Launcher$ExtClassLoader@65b54208
Classloader of ArrayList:null

如我们所见,这里有三种不同的类加载器: AppClassLoader,ExtClassLoader和BootstrapClassLoader (显示为null)。

AppClassLoader 加载示例方法的类. 应用或系统类加载器加载classpath下的类文件.

接下来, ExtClassLoader加载 Logging class. ExtensionClassLoaders 加载作为标准核心Java类的扩展的类.

但是, 我们看到最后一个的输出, 对于ArrayList,输出显示为 null . 这是因为BootstrapClassLoader 是用native code编写的, 而非Java – 因此它不会显示为Java class. 由于这个原因,引导类加载器的行为在不同JVM之间会有所不同。

总体而言,下图显示了不同类加载器之间的联系和区别:


java class loader delegates.png

接下来我们详细讨论这些类加载器。

Bootstrap Class Loader

Java classes 由 java.lang.ClassLoader的实例加载. 但是, ClassLoaders 它们本身也是类. 那么问题来了, 谁来加载 java.lang.ClassLoader 本身呢?

这正是图中引导程序或原始类装载器应用的地方。

它主要负责加载JDK内部类, 通常是 rt.jar$JAVA_HOME/jre/lib 目录中的其他核心库. 此外, Bootstrap 类加载器还充当其他 ClassLoader instances的父类.

Bootstrap ClassLoader 是 JVM 核心的重要内容部分,它是用Native Code编写的 . 不同的平台( HotSpot 、 JRockit 、 J9 )可能对它有不同的实现方式.

Extension Class Loader

Extension ClassLoader 是Bootstrap ClassLoader的子类,负责加载标准核心 Java 的拓展类, 以便在平台上运行整个应用程序.

Extension ClassLoader 从 JDK 拓展目录, 通常是$JAVA_HOME/lib/ext 目录或者 java.ext.dirs 系统属性设置的目录.

System Class Loader

另一方面,System ClassLoader(有的称之为Application ClassLoader)负责加载应用级别的classes 到 JVM 中. 它负责加载classpath 环境变量下的文件, -classpath-cp 命令行选项中的文件. 而且,它是Extension ClassLoader的子类。

ClassLoader如何工作?

ClassLoaders 是 Java运行环境的一部分. JVM 请求一个类时, ClassLoader将使用完全限定名去定位该类,并加载到jvm运行时环境中.

java.lang.ClassLoader.loadClass() 方法负责将类定义加载到运行时. 它尝试使用完全限定名去加载类.

如果该类尚未被加载, 它将委托父类加载器去加载. 这是一个递归过程.

最终, 如果父类加载器没有找到该类, 则子类将调用 java.net.URLClassLoader.findClass() 方法在自己的文件系统中寻找该类.

如果最后一个子类也无法加载该类, 则会抛出java.lang.NoClassDefFoundErrorjava.lang.ClassNotFoundException.

让我们看一个抛出ClassNotFoundException的输出示例。

Exception in thread "main" java.lang.ClassNotFoundException: com.bern.classloader.SampleClassLoader
    at java.net.URLClassLoader.findClass(URLClassLoader.java:382)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
    at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:349)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
    at java.lang.Class.forName0(Native Method)
    at java.lang.Class.forName(Class.java:264)

如果我们从调用java.lang.Class.forName()开始查看事件轴,我们发现,它首先舱室通过父类加载器加载该类,然后通过java.net.URLClassLoader.findClass() 查找类本身. 当依旧无法找到该类,它将抛出 ClassNotFoundException

类加载器有三个重要的特性。

委托模型

ClassLoaders 遵循委托模型,在该模型中,根据请求查找class 或者 resource, ClassLoader实例将会委托给父类加载器去寻找class或者resource.

假设我们需要将应用程序类加载到JVM中的请求。 AppClassLoader首先将该类的加载委托给其父类Extension ClassLoader,而Extension ClassLoader又将其委托给Bootstrap ClassLoader。

仅当Bootstrap 和 Extension ClassLoader 未能成功加载该类时, Application ClassLoader才会舱室加载该类.

唯一性

作为委托模型的结果,很容易确保 唯一的类,因为我们总是尝试向上委托. 如果父类加载器没有找到该类, 则当前的实例才会去尝试寻找该类.

可见性

另外, 子ClassLoaders 对其父ClassLoaders加载的类可见.例如System ClassLoader加载的类对Extension ClassLoader和Bootstrap ClassLoader加载的类可见,反过来却不可以。

为了说明这一点, 如果Class A 是由Application ClassLoader加载的, class B 是由Extensions ClassLoader加载的, 则就Application ClassLoader加载的其他类而言 ,A 和 B 都是可见的.

但是,就Extensions ClassLoader加载的其他类而言,类B是唯一可见的类。

Custom ClassLoader

在大多数情况下,内置的类加载器就足够了。 但是,在需要从本地硬盘驱动器以外或网络中加载类的情况下,我们可能需要使用自定义类加载器。接下来,我们将介绍自定义类加载器的一些用例场景,并创建一个示例。

Custom Class Loaders Use-Cases

自定义类加载器不仅在运行时加载类有用,还包括如下一些使用场景:

1、帮助修改现有的字节码,例如 weaving agents 。

2、动态创建适合用户需求的类。 例如在JDBC中,通过动态类加载实现不同驱动程序之间的切换。

3、在具有相同名称和包的类的情况下,通过加载不同的字节码实现版本控制机制。这可以通过URL类加载器(通过URL加载jar)或自定义类加载器来完成。

在更具体的示例中,自定义类加载器可能会派上用场。 例如,浏览器使用自定义类加载器从网络加载可执行内容. 浏览器可以使用单独的类加载器从不同的网页加载applet. 用于运行applets 的 applet viewer包含一个ClassLoader, 该ClassLoader可访问远程服务器上的网站,而无需在本地文件系统查找内容。然后通过HTTP加载原始字节码文件,并将其装载到JVM中. 即使这些applets具有相同的名称, 但如果由不同的类加载器加载,它们也被视为不同的组件. 现在,我们已经对自定义ClassLoader的使用场景有了一些了解,接下来让我们通过一个自定义 ClassLoader 深入的理解JVM如何加载classes.

Creating our Custom Class Loader

下面的示例创建一个自定义的ClassLoader ,并从文件中读取class加载到JVM中, 我们需要继承ClassLoader类并重写 findClass()方法:

package com.bern.classloader;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;

public class CustomClassLoader extends ClassLoader {

    @Override
    public Class findClass(String name) throws ClassNotFoundException {
        byte[] b = loadClassFromFile(name);
        return defineClass(name, b, 0, b.length);
    }

    private byte[] loadClassFromFile(String fileName)  {
        InputStream inputStream = getClass().getClassLoader().getResourceAsStream(
                fileName.replace('.', File.separatorChar) + ".class");
        byte[] buffer;
        ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
        int nextValue = 0;
        try {
            while ( (nextValue = inputStream.read()) != -1 ) {
                byteStream.write(nextValue);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        buffer = byteStream.toByteArray();
        return buffer;
    }
}

深入理解java.lang.ClassLoader

我们接下来了解以下 java.lang.ClassLoader 类中的一些重要方法,以便能够更好的理解它的工作方式.

loadClass() 方法

public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {

此方法负责加载给定name参数的类. name 参数必须是类的完全限定名. 如果resolve 参数设置为true, 则Java虚拟机调用loadClass() 方法来解析class 引用. 但是, 并不一定非要解析一个类. 如果我们只想确定该类是否存在,而无需去解析, 只需要将参数 resolve 设置为false即可.** 此方法是类加载器的入口点。 我们可以通过查看java.lang.ClassLoader 的源码来了解loadClass()的工作原理:

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    c = findClass(name);

                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

该方法的默认实现按以下顺序搜索类:

  1. 调用findLoadedClass(String) 方法查看该类是否已经被加载.
  2. 调用父类加载器的 loadClass(String) 方法查找该类.
  3. 如果父类加载器没有找到,则调用 findClass(String) 方法查找该类.

defineClass() 方法

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

此方法负责将字节数组转换为类的实例. 在使用该类之前, 我们需要先对它进行解析. 如果类的格式不正确, 则会抛出一个ClassFormatError异常. 另外, 因为该方法时final类型,所以我们无法对它进行重写.

findClass() 方法

protected Class<?> findClass(String name) throws ClassNotFoundException {    throw new ClassNotFoundException(name);}

该方法使用完全限定名作为参数来查找class. 我们需要在自定义ClassLoader中重写该方法. 如果父ClassLoader找不到请求的类,则 loadClass() 会调用此方法。 如果所有的父ClassLoader没有找到该类,则默认实现将会抛出 ClassNotFoundException 异常.

getParent() 方法

public final ClassLoader getParent() 

此方法返回父类加载器进行委派. 有些JVM实现(如第二部分)使用null表示Bootstrap ClassLoader.

getResource() 方法

public URL getResource(String name) {

此方法用来查找给定名称的资源(资源的名称是用 / 分隔的路径名).

它首先委托父加载器查找资源. 如果 parent 返回null, 则搜索虚拟机内置的类加载器的路径. ** 如果都失败了, 则该方法将调用 findResource(String) 来查找资源. 资源名称相对于classpath来说可以是相对路径,也可以是绝对路径. 它通过读取资源,返回一个URL 对象, 如果没有找到该资源,或者没有权限读取该资源,则返回null. 特别需要要注意的是,Java是从 classpath 加载资源。 最后, Java中的资源加载被认为是与位置无关的 因为只是根据设置的环境变量来查找资源,代码在何处运行都无关紧要.

Context Classloaders

通常, Context ClassLoaders 为 J2SE引入类加载委托方案提供了一种替代方法. 就像我们之前所学的, JVM中的类加载器遵循分层模型,因此除了Bootstrap ClassLoader,每个类加载器都有且仅有一个父级. 但是, 有时当JVM核心类需要动态加载应用程序级别的类或资源时, 我们可能会遇到一些问题. 例如, 在JNDI中,核心功能由 rt.jar 中的Bootstrap Classes 实现. 但是这些 JNDI classes 可能会加载由 JNDI 独立供应商提供的JNDI的实现 (部署在应用的 classpath). 这种情况要求Bootstrap ClassLoader (父类加载器) 加载的类对Application ClassLoader (子类加载器)可见. J2SE 委托模型不能解决这种问题,为了解决这个问题, 我们需要找到替代的类加载方式. 我们可以使用线程上下文加载器来应对这种场景. java.lang.Thread 类有一个 getContextClassLoader()方法,该方法返回特定线程的ContextClassLoader . 当加载类或资源时,线程的创建者提供ContextClassLoader . 如果没有设置上下文加载器, 默认是父线程的上下文加载器.

类加载器与 OSGi

OSGi™是 Java 上的动态模块系统。它为开发人员提供了面向服务和基于组件的运行环境,并提供标准的方式用来管理软件的生命周期。OSGi 已经被实现和部署在很多产品上,在开源社区也得到了广泛的支持。Eclipse 就是基于 OSGi 技术来构建的。

OSGi 中的每个模块(bundle)都包含 Java 包和类。模块可以声明它所依赖的需要导入(import)的其它模块的 Java 包和类(通过 Import-Package),也可以声明导出(export)自己的包和类,供其它模块使用(通过 Export-Package)。也就是说需要能够隐藏和共享一个模块中的某些 Java 包和类。这是通过 OSGi 特有的类加载器机制来实现的。OSGi 中的每个模块都有对应的一个类加载器。它负责加载模块自己包含的 Java 包和类。当它需要加载 Java 核心库的类时(以 java开头的包和类),它会代理给父类加载器(通常是启动类加载器)来完成。当它需要加载所导入的 Java 类时,它会代理给导出此 Java 类的模块来完成加载。模块也可以显式的声明某些 Java 包和类,必须由父类加载器来加载。只需要设置系统属性org.osgi.framework.bootdelegation的值即可。

假设有两个模块 bundleA 和 bundleB,它们都有自己对应的类加载器 classLoaderA 和 classLoaderB。在 bundleA 中包含类com.bundleA.Sample,并且该类被声明为导出的,也就是说可以被其它模块所使用的。bundleB 声明了导入 bundleA 提供的类 com.bundleA.Sample,并包含一个类 com.bundleB.NewSample继承自 com.bundleA.Sample。在 bundleB 启动的时候,其类加载器 classLoaderB 需要加载类 com.bundleB.NewSample,进而需要加载类 com.bundleA.Sample。由于 bundleB 声明了类 com.bundleA.Sample是导入的,classLoaderB 把加载类 com.bundleA.Sample的工作代理给导出该类的 bundleA 的类加载器 classLoaderA。classLoaderA 在其模块内部查找类 com.bundleA.Sample并定义它,所得到的类 com.bundleA.Sample实例就可以被所有声明导入了此类的模块使用。对于以 java开头的类,都是由父类加载器来加载的。如果声明了系统属性org.osgi.framework.bootdelegation=com.example.core.*,那么对于包 com.example.core中的类,都是由父类加载器来完成的。

OSGi 模块的这种类加载器结构,使得一个类的不同版本可以共存在 Java 虚拟机中,带来了很大的灵活性。不过它的这种不同,也会给开发人员带来一些麻烦,尤其当模块需要使用第三方提供的库的时候。下面提供几条比较好的建议:

总结

ClassLoaders对于执行Java程序至关重要. 本文详细的讲解了相关的知识. 我们具体的讨论了不同类型的加载器 – Bootstrap, Extensions 和 System ClassLoaders. Bootstrap ClassLoader是所有ClassLoader的父级,并负责加载 JDK 内部 classes. Extensions 和 system, 分别加载从 Java 拓展目录和 classpath 加载类. 然后,我们讨论了类加载器的工作原理,并讨论了一些它们的特性,如委托模型,可见性,唯一性. 然后讲解了如何创建一个自定义的ClassLoader. 最后, we provided an introduction to Context class loaders. 然后,我们讨论了类加载器的工作方式,并讨论了一些功能,例如委托,可见性和唯一性,然后简要说明了如何创建自定义的。 然后,我们介绍了上下文类加载器。最后,我们介绍了osgi和类加载器的关系。

所有代码都已经上传至 GitHub.

上一篇下一篇

猜你喜欢

热点阅读