Java中的类加载器

原文:Class Loaders in Java by baeldung 翻译:陈同学 可以参考笔者另一篇译文 深入JVM内幕 中的类装载器部分

类加载器简介

Class loaders属于JRE的一部分,负责在运行时将Java类动态加载到JVM。得益于class loaders,JVM在无需知晓底层文件或文件系统时就可以运行Java程序。

此外,Java类是按需加载,并不会一次全部加载到内存中。Class loaders负责将类加载到内存。

在本教程中,我们将聊聊几种不同的内置class loaders,它们如何工作以及如何创建自定义的class loader。

几种内置类加载器

我们先以一个简单例子了解下不同类被类加载器加载的区别(PrintClassLoader为当前测试类)。

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

可以看到,有三种不同class loader:application(系统类加载器)、extension(拓展类加载器)和bootstrap(启动类或引导类加载器,显示为null)。

application class loader加载上面样例代码所属的类,application class loader 或(system class loader)用于加载classpath下的文件,是用于加载应用程序class的加载器。

然后,extension class loader加载了上面的 Logging 类,它用于加载Java核心库之外的拓展类。

最后,bootstrap class loader加载了 AarrayList 类,bootstrap(或 primordial) class loader是其他所有class loader的父类。

最后一行 ArrayList 之所以输出值为 null这是因为bootstrap class loader是由native代码所写,所以它不会以Java类的形式体现。由于这个原因,bootstrap class loader在不同JVM之中行为会有所不同。

让我们了解下几种不同class loader。

启动类加载器(Bootstrap Class Loader)

Java类由 java.lang.ClassLoader 的实例进行加载,不过,class loader本身也是Java类,那么 java.lang.ClassLoader 又是由谁加载的呢?

这就是Bootstrap class loader大显身手的地方。它主要负责加载JDK核心类,通常是 rt.jar 和位于 $JAVA_HOME/jre/lib 下的核心库。此外,它也是所有其他 ClassLoader实例的父类。

Bootstrap class loader 是JVM核心之一,由Native代码所写,这点在上述例子中提到过。不同平台Bootstrap class loader可能有不同的实现。

拓展类加载器(Extension Class Loader)

Extension class loader是Bootstrap class loader的子类,负责加载Java核心库外的拓展类,正因如此所有的应用程序都能够运行在Java平台上。

Extension class loader从JDK拓展目录加载类,通常是 $JAVA_HOME/lib/ext 目录或 java.ext.dirs 系统属性中配置的目录。

系统类加载器(System Class Loader)

System class loader是Extensions class loader的子类,负责加载所有应用程序级别的类到JVM,它会加载classpath环境变量或 -classpath以及-cp命令行参数中指定的文件

Class Loaders是如何工作的?

Class loaders是JRE的一部分。当JVM请求一个类时,class loaders会通过类的全限定名尝试加载类并将class definition加载到runtime。

java.lang.ClassLoader.loadClass()方法负责通过类的全限定名将class definition加载到runtime

如果class尚未加载,class loader会将加载请求委派给父加载器,这个委派加载的处理过程会递归进行。

如果父加载器最终没有找到该类,子加载器将调用 java.net.URLClassLoader.findClass() 方法从文件系统中加载该类。如果最终子加载器也无法加载该类,将抛出 java.lang.NoClassDefFoundErrorjava.lang.ClassNotFoundException

让我们看一个抛出 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

Class Loaders有三个很重要的特性。

委派模型(Delegation Model)

当需要查找class或resource时,Class loaders会遵守委派模型,它们首先会将查找请求委派给其父加载器。

假设我们需要将应用中的一个类加载到JVM,system class loader首先会将加载请求委派给extension class loader,后者又会将加载请求委派给bootstrap class loader。

只有当bootstrap class loader和extension class loader都无法加载该类时,system class loader才会尝试自行加载该类。

唯一性(Unique Classes)

作为委派模型的结果,我们总是尝试向上委托,因此很容易保证类的唯一性。如果父加载器无法找到该类,当前加载器才会尝试加载该类。

可见性(Visibility)

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

举个例子,system class loader可以看到extension class loader和bootstrap class loader加载的类,但是反之不行,父加载器无法看到子加载器加载的类。

为了说明这一点,假如类A由system class loader加载,类B由extension class loader加载,那么A和B对于对于system class loader来说都是可见的,extension class loader只能看到类B。

自定义ClassLoader

对于文件系统中的文件来说,内置class loader已经可以满足大部分场景。然而,有些场景并不是从本机硬件设备或网络上加载类,因此我们需要自定义class loader来处理。

在本小节,我们将介绍自定义加载器的一些场景,也会介绍如何创建一个自定义加载器。

自定义classloader的场景

自定义classloader不仅仅只用于在运行时加载类,还有这么一些场景:

  1. 用于更新已存在的字节码,如:编织代理(weaving agent)。
  2. 根据需求动态创建类,如:在JDBC中通过加载类来完成不同驱动程序之间的切换。
  3. 在加载具有相同类名、包名的类的字节码时实现类的版本控制机制,可以通过URL类加载器(通过URL加载jar)或自定义加载器。

还有很多自定义加载器可以派上用场的例子。

例如,浏览器使用自定义加载器从网站加载可执行的内容。浏览器可以使用独立的class loader从不同网页加载applet,用于运行applet的applet查看器包含了一个ClassLoader,它不从本地文件系统检索类,而是访问远程服务器上的站点。然后通过HTTP加载字节码原文件,并将其转换为JVM中的类。虽然这些applet具有相同的名称,但由于它们被不同的class loader所加载,因此它们也被看作不同的组件。

现在我们理解了自定义加载器的意义,那就让我们实现一个ClassLoader的子类来总结JVM类的加载。

创建我们自己的class loader

为了便于说明,假设我们需要通过FTP加载类。由于类不在classpath中,无法通过内置加载器加载这些类。

public class CustomClassLoader extends ClassLoader {
    public CustomClassLoader(ClassLoader parent) {
        super(parent);
    }
    public Class getClass(String name) throws ClassNotFoundException {
        byte[] b = loadClassFromFTP(name);
        return defineClass(name, b, 0, b.length);
    }
 
    @Override
    public Class loadClass(String name) throws ClassNotFoundException {
 
        if (name.startsWith("com.baeldung")) {
            System.out.println("Loading Class from Custom Class Loader");
            return getClass(name);
        }
        return super.loadClass(name);
    }
 
    private byte[] loadClassFromFTP(String fileName)  {
        // Returns a byte array from specified file.
    }
}

在上面例子中,我们定义了一个class loader用于从包 com.baeldung 加载文件,拓展了默认class loader。

我们在构造器中传入了parent class loader,然后使用类的全限定名通过FTP加载类。

理解java.lang.ClassLoader

让我们了解下 java.lang.ClassLoader 中的几个基础方法,以便对ClassLoader的工作方式有个清晰的脑图。

loadClass()方法

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

该方法通过给定的类全限定名加载类,如果参数 resolvetrue,JVM将执行 loadClass() 解析该类。然而,我们并非总是需要解析一个类。 如果只需要判断类是否存在,可以将 resolve参数设置为false

这个方法是class loader的入口,我们可以通过源码了解 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;
        }
    }

方法中查找类的默认实现按照如下顺序进行:

  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标记,我们不能override。

findClass()方法

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

该方法以类全限定名来查找类,在自定义的class loader中,我们需要override这个方法,并且需要遵守委派模型。

当然,如果父加载器无法找到目标类,将会执行 loadClass() 方法。

在默认实现中,如果所有父加载器都无法查找到该类,将抛出 ClassNotFoundException

getParent()方法

这个方法返回父加载器用于委派。

有些实现像最上面例子中使用 null 来代表bootstrap class loader。

getResource()方法

public URL getResource(String name)

该方法用于查找给定名称的资源。

首先,查找请求会委托给父加载器。如果父加载器为null,则将请求交给bootstrap class loader。

如果依然失败,该方法将调用 findResource(String) 来查找资源。它返回一个用于读取资源的URL对象,如果没有找到资源或没有足够的权限访问资源将返回 null

值得注意的是,Java会从classpath路径中加载资源。

线程上下文加载器(Context Classloaders)

Context Classloaders为J2SE中引入的类加载委派方案提供了另一种方式。

前面我们学到,JVM中的class loaders遵循层级模型,除bootstrap class loader外,每个类加载器都有一个父类。

然而,有时当JVM核心类需要加载由开发人员提供的类或资源时,我们可能会遇到问题。

例如,在JNDI中,其核心功能由 rt.jar 中的引导类实现。但是这些JNDI引导类可能需要加载由各独立服务商提供的JNDI实现类(部署在应用的classpath中),这个场景需要bootstrap class loader加载一些仅对child class loader可见的类。

J2SE委派在这里并不管用,我们需要找到一种替代方法来加载类。这可以使用线程上下文加载器来实现。

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

小结

Class loaders是执行Java程序的基础,本文我们进行了简单介绍。

我们介绍了几种不同的class loader——Bootstrap,Extension和System class loaders。Bootstrap作为所有class loader的父类,负责加载JDK核心类。Extension和System负责加载Java拓展目录和classpath中的类。

然后,我们介绍了class loader的工作方式,通过创建一个简单的自定义class loader介绍了几个特性,如:委派、可见性和唯一性。最后,我们简介了 Context class loaders。

本文的样例代码见 github

原文链接:https://www.baeldung.com/java-classloaders

原文作者:baeldung

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏刘望舒

Android解析ClassLoader(一)Java中的ClassLoader

前言 热修复和插件化是目前比较热门的技术,要想更好的掌握它们需要了解ClassLoader,因此也就有了本系列的产生,这一篇我们先来学习Java中的ClassL...

2215
来自专栏散尽浮华

linux运维中的命令梳理(三)

----------文本操作命令---------- sed命令:文本编辑工具 sed是一个很好的文件处理工具,本身是一个管道命令,主要是以行为单位进行处理,可...

2428
来自专栏程序你好

Java虚拟机JVM架构解析

每个Java开发人员都知道字节码将由JRE (Java运行时环境)运行。但是许多人不知道JRE是Java虚拟机(JVM)的实现,它分析字节码、解释并执行代码。作...

702
来自专栏张首富-小白的成长历程

Linux-四剑客-find-awk-grep-sed解释----未完结版

find - search for files in a directory hierarchy 搜索目录层次结构中的文件 用来在指定目录下面查找文件或目录,任...

1423
来自专栏pythonlove

Bash脚本编程(原创)

Bash,Unix shell的一種,在1987年由布萊恩·福克斯為了GNU計劃而编写。1989年釋出第一個正式版本,原先是計劃用在GNU作業系統上,但能运行于...

1103
来自专栏Java技术栈

JDK9新特性实战:简化流关闭新姿势。

做Java开发的都知道,每个资源的打开都需要对应的关闭操作,不然就会使资源一直占用而造成资源浪费,从而降低系统性能。 关于资源的关闭操作,从JDK7-JDK9有...

3538
来自专栏JavaQ

高并发编程-Condition深入解析

Condition接口位于java.util.concurrent.locks包下,实现类有 AbstractQueuedLongSynchronizer.Co...

944
来自专栏JavaEdge

类加载器与双亲委派模型1 类加载器 2 双亲委派模型

类加载器(ClassLoader)是Java语言的一项创新,也是Java流行的一个重要原因。 在类加载的第一阶段“加载”过程中,需要通过一个类的全限定名来获取...

1372
来自专栏TechBox

【iOS】运行时消息传递与转发机制前言(一)对象的消息传递机制 objc_msgSend()(二)消息转发流程参考文章

1264
来自专栏MasiMaro 的技术博文

8086cpu中的标志寄存器与比较指令

在8086CPU中有一个特殊的寄存器——标志寄存器,该寄存器不同于其他寄存器,普通寄存器是用来存放数据的读取整个寄存器具有一定的含义,但是标志寄存器是每一位都有...

1191

扫码关注云+社区

领取腾讯云代金券