首页 课程 师资 教程 报名

Java类加载机制介绍

  • 2022-05-19 10:35:02
  • 1323次 动力节点

1.类加载器简介

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

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

在本教程中,我们将讨论不同类型的内置类加载器及其工作方式。然后我们将介绍我们自己的自定义实现。

2.内置类加载器的类型

让我们从学习如何使用各种类加载器加载不同的类开始:

public void printClassLoaders() throws ClassNotFoundException {
    System.out.println("Classloader of this class:"
        + PrintClassLoader.class.getClassLoader());
    System.out.println("Classloader of Logging:"
        + Logging.class.getClassLoader());
    System.out.println("Classloader of ArrayList:"
        + ArrayList.class.getClassLoader());
}

执行时,上述方法打印:

Class loader of this class:sun.misc.Launcher$AppClassLoader@18b4aac2
Class loader of Logging:sun.misc.Launcher$ExtClassLoader@3caeaf62
Class loader of ArrayList:null

正如我们所见,这里有三种不同的类加载器:应用程序、扩展程序和引导程序(显示为​​ null)。

应用程序类加载器加载包含示例方法的类。应用程序或系统类加载器在类路径中加载我们自己的文件。

接下来,扩展类加载器加载Logging类。扩展类加载器加载作为标准核心 Java 类的扩展的类。

最后,引导类加载器加载ArrayList类。引导程序或原始类加载器是所有其他类加载器的父级。

但是,我们可以看到,对于ArrayList,它在输出中显示为null 。这是因为引导类加载器是用本机代码而不是 Java 编写的,因此它不会显示为 Java 类。因此,引导类加载器的行为在不同的 JVM 中会有所不同。

现在让我们更详细地讨论这些类加载器。

(1)引导类加载器

Java 类由java.lang.ClassLoader的实例加载。但是,类加载器本身就是类。所以问题是,谁加载java.lang.ClassLoader本身?

这就是引导程序或原始类加载器发挥作用的地方。

它主要负责加载 JDK 内部类,通常是rt.jar和其他位于$JAVA_HOME/jre/lib目录下的核心库。此外,Bootstrap 类加载器充当所有其他ClassLoader实例的父级。

这个引导类加载器是核心 JVM 的一部分,并且是用本机代码编写的,如上面的示例中所指出的。不同的平台可能有这个特定类加载器的不同实现。

(2)扩展类加载器

扩展类加载器是引导类加载器的子类,负责加载标准核心 Java 类的扩展,以便平台上运行的所有应用程序都可以使用它们。

扩展类加载器从 JDK 扩展目录加载,通常是$JAVA_HOME/lib/ext目录,或java.ext.dirs系统属性中提到的任何其他目录。

(3)系统类加载器

另一方面,系统或应用程序类加载器负责将所有应用程序级别的类加载到 JVM 中。它加载在类路径环境变量、-classpath或-cp命令行选项中找到的文件。它也是扩展类加载器的子类。

3. 类加载器是如何工作的?

类加载器是 Java 运行时环境的一部分。当 JVM 请求一个类时,类加载器会尝试定位该类并使用完全限定的类名将类定义加载到运行时中。

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

如果该类尚未加载,它将请求委托给父类加载器。这个过程递归地发生。

最终,如果父类加载器没有找到该类,那么子类将调用java.net.URLClassLoader.findClass()方法在文件系统本身中查找类。

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

java.lang.ClassNotFoundException: com.baeldung.classloader.SampleClassLoader    
    at java.net.URLClassLoader.findClass(URLClassLoader.java:381)    
    at java.lang.ClassLoader.loadClass(ClassLoader.java:424)    
    at java.lang.ClassLoader.loadClass(ClassLoader.java:357)    
    at java.lang.Class.forName0(Native Method)    
    at java.lang.Class.forName(Class.java:348)

如果我们从调用java.lang.Class.forName()开始的事件序列,我们可以看到它首先尝试通过父类加载器加载类,然后java.net.URLClassLoader.findClass()到寻找课程本身。

当它仍然找不到类时,它会抛出ClassNotFoundException。

现在让我们来看看类加载器的三个重要特性。

(1)委托模型

类加载器遵循委托模型,在请求查找类或资源时,ClassLoader实例会将类或资源的搜索委托给父类加载器。

假设我们有一个将应用程序类加载到 JVM 的请求。系统类加载器首先将该类的加载委托给其父扩展类加载器,后者又将其委托给引导类加载器。

只有当引导程序和扩展类加载器加载类不成功时,系统类加载器才会尝试加载类本身。

(2)独特的课程

作为委托模型的结果,很容易确保类的唯一性,因为我们总是尝试向上委托。

如果父类加载器无法找到该类,那么当前实例才会尝试自己这样做。

(3)能见度

此外,子类加载器对其父类加载器加载的类是可见的。

例如,系统类加载器加载的类可以看到扩展和引导类加载器加载的类,反之则不行。

为了说明这一点,如果 A 类由应用程序类加载器加载,而 B 类由扩展类加载器加载,那么就应用程序类加载器加载的其他类而言,A 类和 B 类都是可见的。

但是,B 类是扩展类加载器加载的其他类唯一可见的类。

4.自定义类加载器

对于文件已经在文件系统中的大多数情况,内置的类加载器就足够了。

但是,在我们需要从本地硬盘驱动器或网络加载类的情况下,我们可能需要使用自定义类加载器。

在本节中,我们将介绍自定义类加载器的其他一些用例,并演示如何创建一个。

(1)自定义类加载器用例

自定义类加载器不仅仅有助于在运行时加载类。一些用例可能包括:

帮助修改现有的字节码,例如编织代理

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

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

下面是更具体的示例,自定义类加载器可能会派上用场。

例如,浏览器使用自定义类加载器从网站加载可执行内容。浏览器可以使用不同的类加载器从不同的网页加载小程序。用于运行小程序的小程序查看器包含一个访问远程服务器上的网站而不是查看本地文件系统的类加载器。

然后它通过 HTTP 加载原始字节码文件,并将它们转换为 JVM 中的类。即使这些小程序具有相同的名称,如果由不同的类加载器加载,它们也会被视为不同的组件。

现在我们了解了为什么自定义类加载器是相关的,让我们实现一个ClassLoader的子类来扩展和总结 JVM 如何加载类的功能。

(2)创建我们的自定义类加载器

出于说明目的,假设我们需要使用自定义类加载器从文件中加载类。

我们需要扩展ClassLoader类并重写findClass()方法:

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;
    }
}

在上面的例子中,我们定义了一个自定义的类加载器,它扩展了默认的类加载器,并从指定的文件中加载一个字节数组。

5.了解java.lang.ClassLoader

让我们讨论java.lang.ClassLoader类中的一些基本方法,以更清楚地了解它的工作原理。

(1)loadClass( )方法

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

此方法负责加载给定名称参数的类。name 参数是指完全限定的类名。

Java 虚拟机调用loadClass()方法来解析类引用,将 resolve 设置为true。但是,并不总是需要解析一个类。如果我们只需要确定类是否存在,则将 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.
                    c = findClass(name);
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

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

调用findLoadedClass(String)方法以查看该类是否已加载。

在父类加载器上调用loadClass(String)方法。

调用findClass(String)方法来查找类。

(2)defineClass ()方法

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

此方法负责将字节数组转换为类的实例。在我们使用这个类之前,我们需要解决它。

如果数据不包含有效的类,则会引发ClassFormatError。

此外,我们不能覆盖这个方法,因为它被标记为 final。

(3)findClass( )方法

protected Class<?> findClass(
  String name) throws ClassNotFoundException

 

此方法查找具有完全限定名称的类作为参数。我们需要在遵循委托模型的自定义类加载器实现中重写此方法以加载类。

此外,如果父类加载器找不到请求的类, loadClass()会调用此方法。

如果类加载器的父级没有找到该类,默认实现会抛出ClassNotFoundException 。

(4)getParent( )方法

public final ClassLoader getParent()

此方法返回用于委托的父类加载器。

一些实现,如之前在第 2 节中看到的实现,使用null来表示引导类加载器。

(5)getResource( )方法

public URL getResource(String name)

此方法尝试查找具有给定名称的资源。

它将首先委托给资源的父类加载器。如果 parent 为null,则搜索虚拟机内置的类加载器的路径。

如果失败,则该方法将调用findResource(String)来查找资源。指定为输入的资源名称对于类路径可以是相对的或绝对的。

它返回一个用于读取资源的 URL 对象,如果找不到资源或调用者没有足够的权限返回资源,则返回 null。

需要注意的是,Java 从类路径加载资源。

最后,Java 中的资源加载被认为与位置无关,因为只要将环境设置为查找资源,代码在哪里运行并不重要。

6.上下文类加载器

通常,上下文类加载器提供了一种替代 J2SE 中引入的类加载委托方案的方法。

就像我们之前了解到的,JVM 中的类加载器遵循分层模型,这样每个类加载器都有一个父类,但引导类加载器除外。

但是,有时当JVM核心类需要动态加载应用程序开发者提供的类或资源时,我们可能会遇到问题。

例如,在 JNDI 中,核心功能由rt.jar 中的引导类实现。但是这些 JNDI 类可能会加载由独立供应商实现的 JNDI 提供程序(部署在应用程序类路径中)。此场景要求引导类加载器(父类加载器)加载应用程序加载器(子类加载器)可见的类。

J2SE 委托在这里不起作用,为了解决这个问题,我们需要找到替代的类加载方式。这可以使用线程上下文加载器来实现。

java.lang.Thread类有一个方法getContextClassLoader(),它返回特定线程的ContextClassLoader。ContextClassLoader由线程的创建者在加载资源和类时提供。

以上就是关于“Java类加载机制介绍”,大家如果对此比较感兴趣,想了解更多相关知识,不妨来关注一下动力节点的Java基础教程,里面有更丰富的知识等着大家去学习,相信对大家会有所帮助的。

选你想看

你适合学Java吗?4大专业测评方法

代码逻辑 吸收能力 技术学习能力 综合素质

先测评确定适合在学习

在线申请免费测试名额
价值1998元实验班免费学
姓名
手机
提交