从零开始的JVM学习--类加载器

116 阅读12分钟

简单介绍

类加载过程

在我的博客从零开始的JVM学习--类加载机制中已经详细介绍了类加载过程,但是作为前置须知知识,我也列举一些和类加载器相关联的知识。

我们知道类加载过程分成:加载、连接、初始化

连接过程分成:验证、准备、解析

而非数组类是通过类加载器加载的。数组类是通过JVM直接创建,然后元素再通过类加载器加载。

非数组类的加载阶段(加载阶段获取类的二进制字节流的动作)是可控性最强的阶段,这一步我们可以去自定义类加载器去控制字节流的获取方式(重写一个类加载器的 loadClass() 方法)。

加载的作用简单来说就是将 .class文件加载到内存。

什么是类加载器?

什么是类加载器?

「类加载器」负责读取Java字节码,并转换成java.lang.Class类的一个实例代码模块。

「类加载器」除了用于加载类外,还可用于确定类在Java虚拟机中的唯一性:

  • 任意一个类,都由加载它的「类加载器」和这个类本身一同确定其在 Java 虚拟机中的唯一性。
  • 每一个「类加载器」,都有一个独立的类名称空间,而不同「类加载器」中是允许同名(指全限定名相同)类存在的。

比较两个类是否相等?

比较两个类是否“相等”,前提是这两个类由同一个类加载器加载。否则,即使这两个类来源于同一个Class 文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那么这两个类就必定不相等。

这里“相等”是指:类的Class对象的equals()方法、isInstance()方法的返回结果,使用instanceof关键字做对象所属关系判定等情况。

类加载器

JVM内置类加载器

JVM内置的三个重要类加载器

JVM 中内置了三个重要的 ClassLoader,除了 BootstrapClassLoader 其他类加载器均由 Java 实现且全部继承自java.lang.ClassLoader

  • BootstrapClassLoader(启动类加载器)

    最顶层的加载类,由 C++实现。

    负责加载 %JAVA_HOME%/lib目录下的 jar 包和类 或者 被 -Xbootclasspath参数指定的路径中的所有类。(将类库加载到虚拟机内存)

    「启动类加载器」无法被 Java 程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给「启动类加载器」,直接使用 null 代替即可。

  • ExtensionClassLoader(扩展类加载器)

    主要负责加载 %JRE_HOME%/lib/ext 目录下的 jar 包和类,或被 java.ext.dirs 系统变量所指定的路径下的 jar 包。

    开发者可以直接使用「扩展类加载器」

  • AppClassLoader(应用程序类加载器)

    面向我们用户的加载器,负责加载当前应用 classpath 下的所有 jar 包和类。

    由于这个类加载器是 ClassLoader 中的 getSystemClassLoader() 方法的返回值,因此一般称为系统类加载器。

    开发者可以直接使用「应用程序类加载器」,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

类加载器分类

「JVM内置类加载器」章节中的分类是从Java开发人员的角度来看的,相对来说划分比较细致。但是只介绍了JVM内置的类加载器,并不能描述所有的类加载器。

这章我们将把类加载器划分到能够描述JVM中所有的类加载器。

实际上对于JVM只存在两种类加载器:

  • BootstrapClassLoader(启动类加载器)

    使用 C++ 实现,是虚拟机自身的一部分

  • 所有其它类的加载器

    使用 Java 实现,独立于虚拟机,继承自抽象类 java.lang.ClassLoader

划分的标准不是功能,而是实现的语言和与虚拟机的关系。

类加载器的关系

以上我们介绍完成类加载器的分类,接着我们可以通过一段程序获取到上面所介绍的几种ClassLoader

 // App ClassLoader
 System.out.println(Foo.class.getClassLoader());
 // Ext ClassLoader
 System.out.println(Foo.class.getClassLoader().getParent());
 // Bootstrap ClassLoader
 System.out.println(Foo.class.getClassLoader().getParent().getParent());
 // Bootstrap ClassLoader
 System.out.println(String.class.getClassLoader());

执行结果:

 sun.misc.Launcher$AppClassLoader@18b4aac2
 sun.misc.Launcher$ExtClassLoader@6bc7c054
 null
 null

类加载器的关系

由上面的执行结果我们可知:

类加载器之间其实是有父子关系的

  • AppClassLoader的父类加载器为ExtClassLoader

  • ExtClassLoader的父类加载器为 BootstrapClassLoader

    null 并不代表ExtClassLoader没有父类加载器,而是 BootstrapClassLoader

在得知了类加载器中存在父子关系以后,我们将学习「双亲委派模型」,它将告诉我们,除了启动类加载器外,其他的类加载器实际上都得有自己的父加载器!

全盘负责

什么是全盘负责?

「全盘负责」是指当一个ClassLoader负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显示地使用另外一个类加载器来载入。(ClassLoader一条龙服务😂)

例如:

系统类加载器AppClassLoader加载入口类(含有main方法的类)时,会把main方法所依赖的类及引用的类也载,依次类推。

以上步骤只是调用了ClassLoader.loadClass(name)方法,并没有真正定义类。真正加载Class字节码文件生成Class对象由「双亲委派机制」完成。

缓存机制

什么是缓存机制?

「缓存机制」将会保证所有加载过的Class都将在内存中缓存

当程序中需要使用某个Class时,类加载器先从内存的缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。

修改Class后必须重启JVM,程序修改才会生效的原因

对于一个类加载器实例来说,相同全名的类只加载一次(即loadClass方法不会被重复调用)

因此,这就是为什么修改Class后,必须重启JVM,程序的修改才会生效的原因。

双亲委派机制

什么是双亲委派模型?

image.png

「双亲委派模型」也可以叫做父类委托模型。

「双亲委派模型」描述类加载器之间的层次关系。它要求除了顶层的「启动类加载器」外,其余的类加载器都应当有自己的父类加载器。父子关系一般不会以继承的关系实现,而是以组合关系来复用父加载器的代码。

「双亲委派模型」要求子类加载器如果没有加载过该目标类,就先委托父类加载器加载该目标类,只有在父类加载器找不到字节码文件的情况下才从自己的类路径中查找并装载目标类。

双亲委派模型的好处

  • 保证Java程序的稳定运行,避免类的重复加载

    JVM区分不同类的方式不仅仅根据类名,相同的类文件被不同的类加载器加载产生的是两个不同的类

  • 保证Java核心API不被篡改

    这里的Java核心API类都在rt.jar中,所以可以看到最顶的类加载器读取的就是这个jar包。

    如果没有使用「双亲委派模型」,而是每个类加载器加载自己的话就会出现一些问题

    假如编写一个称为java.lang.Object类,程序运行时,系统就会出现多个不同的Object类。

    反之使用「双亲委派模型」:无论使用哪个类加载器加载,最终都会委派给最顶端的「启动类加载器」加载,从而使得不同加载器加载的Object类都是同一个。

双亲委派机制加载Class的过程

  • ClassLoader先判断该Class是否已加载

    如果已加载,则返回Class对象,如果没有则委托给父类加载器

  • 父类加载器判断是否加载过该Class

    如果已加载,则返回Class对象,如果没有则委托给祖父类加载器

  • 依此类推,直到始祖类加载器(启动类加载器)

  • 始祖类加载器判断是否加载过该Class

    如果已加载,则返回Class对象。如果没有,则尝试从其对应的类路径下寻找Class字节码文件并载入。

    如果载入成功,则返回Class对象;如果载入失败,则委托给始祖类加载器的子类加载器

  • 始祖类加载器的子类加载器尝试从其对应的类路径下寻找class字节码文件并载入

    如果载入成功,则返回Class对象;如果载入失败,则委托给始祖类加载器的孙类加载器

  • 依此类推,直到源ClassLoader

  • ClassLoader尝试从其对应的类路径下寻找Class字节码文件并载入

    如果载入成功,则返回Class对象;如果载入失败,源ClassLoader不会再委托其子类加载器,而是抛出异常

介绍完双亲委派模型后,我们来讲讲ClassLoader的核心方法「loadClass方法」,因为它的默认实现就是双亲委派模型在代码层面的基础。

loadClass方法

我们点进ClassLoaderloadClass方法

  protected Class<?> loadClass(String name, boolean resolve)
         throws ClassNotFoundException
     {
         synchronized (getClassLoadingLock(name)) {
             // 检查类是否已经加载
             Class<?> c = findLoadedClass(name);
             if (c == null) {// 如果该类没有加载,则想办法让它的父加载器去加载它(向上寻找)
                 long t0 = System.nanoTime();
                 try {
                     // 父加载器不为空,调用父加载器loadClass()方法处理
                     if (parent != null) {
                         // 让上一层加载器进行加载
                         c = parent.loadClass(name, false);
                     } else {
                         // 父加载器为空,使用启动类加载器 BootstrapClassLoader 加载
                         c = findBootstrapClassOrNull(name);
                     }
                 } catch (ClassNotFoundException e) {
                      // 抛出异常说明父类加载器无法完成加载请求
                 }
 ​
                 if (c == null) {// 如果父加载器没有加载成功这个类
                     long t1 = System.nanoTime();
                     // 如果没有找到,则调用此类加载器所实现的findClass方法进行加载
                     c = findClass(name);
 ​
                     // 记录统计数据
                     sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                     sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                     sun.misc.PerfCounter.getFindClasses().increment();
                 }
             }
             if (resolve) {
                 // resolveClass方法是当字节码加载到内存后进行链接操作,对文件格式和字节码验证,并为 static 字段分配空间并初始化,符号引用转为直接引用,访问控制,方法覆盖等
                 resolveClass(c);
             }
             return c;
         }
     }

观察源码,我们知道loadClass方法是先用类的父加载器来加载类的,找不到再用本类的加载器来完成加载。

而我们如果点进findClass方法会发现是一个空方法:

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

说明findClass方法需要用户自己实现。

最终执行了resolveClass方法,它实际上是调用了一个native方法

 protected final void resolveClass(Class<?> c) {
     resolveClass0(c);
 }
 private native void resolveClass0(Class<?> c);

它所做的事情就是类的「加载」完成后让虚拟机去「连接」该类。(参考「类加载过程」章节)

这样一来我们加载我们自定义类的类加载器一般都是Application ClassLoader,加载Core Java API中的类的类加载器一般都是BootStrap ClassLoader,至于核心Java类的扩展的类的加载一般就交给Extension ClassLoader了。

打破双亲委派

如何打破双亲委派

「双亲委派机制」是Java推荐的机制,并不是强制的机制。而且有时候会带来一些些的问题。

例如:java.sql.Driver类,JDK只能提供一个规范接口,而不能提供实现。提供实现的是实际的数据库提供商,提供商的库不可能放JDK目录里。

可以继承java.lang.ClassLoader类,实现自己的类加载器:

  • 如果想保持「双亲委派模型」,应该重写findClass(name)方法
  • 如果想破坏「双亲委派模型」,可以重写loadClass(name)方法

实现自己的类加载器,重写loadClass方法

 package com.dyh.classloader;
 ​
 import java.io.*;
 ​
 public class MyClassLoader extends ClassLoader {
 ​
     @Override
     protected Class<?> findClass(String name) throws ClassNotFoundException {
         byte[] classData;
         try {// 从自己指定的类路径下寻找字节码并读入
             classData = loadClassData(name);
         } catch (IOException e) {
             throw new RuntimeException(e);
         }
         if (classData == null) {
             throw new ClassNotFoundException();
         } else {
             // 通过全类名和类数据来加载Class
             // 任意一个类,都由加载它的「类加载器」和这个类本身一同确定其在 Java 虚拟机中的唯一性
             return defineClass(name, classData, 0, classData.length);
         }
     }
 ​
     private byte[] loadClassData(String className) throws IOException {
         // 将类的全限定名替换成Java IO可以接收的全路径
         String replace = className.replace('.', File.separatorChar);
         String path = ClassLoader.getSystemResource("").getPath() + replace + ".class";
         InputStream inputStream = null;
         ByteArrayOutputStream byteArrayOutputStream = null;
         try {
             // 从我们指定的的类路径下寻找字节码文件并读入
             inputStream = new FileInputStream(path);
             byteArrayOutputStream = new ByteArrayOutputStream();
             int bufferSize = 1024;
             byte[] buffer = new byte[bufferSize];
             int length = 0;
             while ((length = inputStream.read(buffer)) != -1) {
                 byteArrayOutputStream.write(buffer, 0, length);
             }
             return byteArrayOutputStream.toByteArray();
         } catch (IOException e) {
             e.printStackTrace();
         } finally {
             if (byteArrayOutputStream != null) {
                 byteArrayOutputStream.close();
             }
             if (inputStream != null) {
                 inputStream.close();
             }
         }
 ​
         return null;
     }
 ​
     @Override
     protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
         synchronized (getClassLoadingLock(name)) {
             // 检查该对象的Class是否已经被加载
             Class<?> c = findLoadedClass(name);
             if (c == null) {
                 long t0 = System.nanoTime();
                 try {// 若是没有我们这里打破双亲委派,直接让自己的类加载器去寻找指定类路径加载Class
                     // 修改classloader的原双亲委派逻辑,从而打破双亲委派
                     if (name.startsWith("com.dyh.classloader")) {
                         c = findClass(name);
                     } else {// 非自己制定的包下的类仍然委派父加载器
                         c = this.getParent().loadClass(name);
                     }
                 } catch (ClassNotFoundException e) {
                 }
 ​
                 if (c == null) {
                     long t1 = System.nanoTime();
                     c = findClass(name);
 ​
                     sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                     sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                     sun.misc.PerfCounter.getFindClasses().increment();
                 }
             }
             if (resolve) {
                 resolveClass(c);
             }
             return c;
         }
     }
 }

编写测试程序:

 public static void main(String[] args) throws ClassNotFoundException {
     MyClassLoader classLoader = new MyClassLoader();
     Class<?> aClass = classLoader.loadClass(Foo.class.getName());
     System.out.println(aClass.getClassLoader());
 }

运行结果:

 com.dyh.classloader.MyClassLoader@4554617c

原来的loadClass是优先委派该类加载器的父类加载器去加载类的,运行结果应当是自定义类加载器的父类加载器——Application ClassLoader

而这里我们则是优先让我们自己的类加载器去在指定的类路径中读取字节码数据,并根据该类的全限定名去加载类(类的唯一性确定)。于是运行结果就是该类是被自己的类加载器加载的。

小结

本章我们学习了类加载器相关内容,类加载器就是类加载过程的具体执行者,到此为止我们可以理解Class文件,Class的生命周期,以及Class文件具体是如何被加载进内存,如何被JVM识别并加载的。

本章参考: