一文带你深扒ClassLoader内核,揭开它的神秘面纱!

  • 「MoreThanJava」 宣扬的是 「学习,不止 CODE」
  • 若是以为 「不错」 的朋友,欢迎 「关注 + 留言 + 分享」,文末有完整的获取连接,您的支持是我前进的最大的动力!

前言

ClassLoader 能够说是 Java 最为神秘的功能之一了,好像你们都知道怎么回事儿 (双亲委派模型好像都都能说得出来...),又都说不清楚具体是怎么一回事 (为何须要须要有什么实际用途就很模糊了...)html

今天,咱们就来深度扒一扒,揭开它神秘的面纱!java

Part 1. 类加载是作什么的?

首先,咱们知道,Java 为了实现 「一次编译,处处运行」 的目标,采用了一种特别的方案:先 编译与任何具体及其环境及操做系统环境无关的中间代码(也就是 .class 字节码文件),而后交由各个平台特定的 Java 解释器(也就是 JVM)来负责 解释 运行。git

ClassLoader (顾名思义就是类加载器) 就是那个把字节码交给 JVM 的搬运工 (加载进内存)。它负责将 字节码形式 的 Class 转换成 JVM 中 内存形式 的 Class 对象。程序员

字节码能够是来自于磁盘上的 .class 文件,也能够是 jar 包里的 *.class,甚至是来自远程服务器提供的字节流。字节码的本质其实就是一个有特定复杂格式的字节数组 byte[] (从后面解析 ClassLoader 类中的方法时更能体会)github

另外,类加载器不光能够把 Class 加载到 JVM 之中并解析成 JVM 统一要求的对象格式,还有一个重要的做用就是 审查每一个类应该由谁加载web

并且,这些 Java 类不会一次所有加载到内存,而是在应用程序须要时加载,这也是须要类加载器的地方。面试

Part 2. ClassLoader 类结构分析

如下就是 ClassLoader 的主要方法了:数据库

  • defineClass() 用于将 byte 字节流解析成 JVM 可以识别的 Class 对象。有了这个方法意味着咱们不只能够经过 .class 文件实例化对象,还能够经过其余方式实例化对象,例如经过网络接收到一个类的字节码。segmentfault

    (注意,若是直接调用这个方法生成类的 Class 对象,这个类的 Class 对象尚未 resolve,JVM 会在这个对象真正实例化时才调用 resolveClass() 进行连接)数组

  • findClass() 一般和 defineClass() 一块儿使用,咱们须要直接覆盖 ClassLoader 父类的 findClass() 方法来实现类的加载规则,从而取得要加载类的字节码。(如下是 ClassLoader 源码)

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

    若是你不想从新定义加载类的规则,也没有复杂的处理逻辑,只想在运行时可以加载本身制定的一个类,那么你能够用 this.getClass().getClassLoader().loadClass("class") 调用 ClassLoader 的 loadClass() 方法来获取这个类的 Class 对象,这个 loadClass() 还有重载方法,你一样能够决定再何时解析这个类。

  • loadClass() 用于接受一个全类名,而后返回一个 Class 类型的对象。(该方法源码蕴含了著名的双亲委派模型)

  • resolveClass() 用于对 Class 进行 连接,也就是把单一的 Class 加入到有继承关系的类树中。若是你想在类被加载到 JVM 中时就被连接(Link),那么能够在调用 defineClass() 以后紧接着调用一个 resolveClass() 方法,固然你也能够选择让 JVM 来解决何时才连接这个类(一般是真正被实实例化的时候)。

ClassLoader 是个抽象类,它还有不少子类,若是咱们要实现本身的 ClassLoader,通常都会继承 URLClassLoader 这个子类,由于这个类已经帮咱们实现了大部分工做。

例如,咱们来看一下 java.net.URLClassLoader.findClass() 方法的实现:

// 入参为 Class 的 binary name,如 java.lang.String
protected Class<?> findClass(final String name) throws ClassNotFoundException {
    // 以上代码省略
  
    // 经过 binary name 生成包路径,如 java.lang.String -> java/lang/String.class
    String path = name.replace('.''/').concat(".class");
    // 根据包路径,找到该 Class 的文件资源
    Resource res = ucp.getResource(path, false);
    if (res != null) {
        try {
           // 调用 defineClass 生成 java.lang.Class 对象
            return defineClass(name, res);
        } catch (IOException e) {
            throw new ClassNotFoundException(name, e);
        }
    } else {
        return null;
    }
  
    // 如下代码省略
}

Part 3. Java 类加载流程详解

如下就是 ClassLoader 加载一个 class 文件到 JVM 时须要通过的步骤。

事实上,咱们每一次在 IDEA 中点击运行时,IDE 都会默认替咱们执行如下的命令:

  • javac Xxxx.java ➡️ 找到源文件中的 public class,再找 public class 引用的其余类,Java 编译器会根据每个类生成一个字节码文件;
  • java Xxxx ➡️ 找到文件中的惟一主类 public class,并根据 public static 关键字找到跟主类关联可执行的 main 方法 (这也是为何 main 方法须要被定义为 public static void 的缘由了——咱们须要在类没有加载时访问),开始执行。

在真正的运行 main 方法以前,JVM 须要 加载、连接 以及 初始化 上述的 Xxxx 类。

第一步:加载(Loading)

这一步是读取到类文件产生的二进制流(findClass()),并转换为特定的数据结构(defineClass()),初步校验 cafe babe 魔法数 (二进制中前四个字节为 0xCAFEBABE 用来标识该文件是 Java 文件,这是不少软件的作法,好比 zip压缩文件、常量池、文件长度、是否有父类等,而后在 Java 中建立对应类的 java.lang.Class 实例,类中存储的各部分信息也须要对应放入 运行时数据区 中(例如静态变量、类信息等放入方法区)。

如下是一个 Class 文件具备的基本结构的简单图示:

若是对 Class 文件更多细节感兴趣的能够进一步阅读:https://juejin.im/post/6844904199617003528

这里咱们可能会有一个疑问,为何 JVM 容许尚未进行验证、准备和解析的类信息放入方法区呢?

答案是加载阶段和连接阶段的部分动做(好比一部分字节码文件格式验证动做)是 交叉进行 的,也就是说 加载阶段还没完成,连接阶段可能已经开始了。但这些夹杂在加载阶段的动做(验证文件格式等)仍然属于连接操做。

第二步:连接(Linking)

Link 阶段包括验证、准备、解析三个步骤。下面👇咱们来详细说说。

验证:确保被加载的类的正确性

验证是链接阶段的第一步,这一阶段的目的是 为了确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,而且不会危害虚拟机自身的安全。验证阶段大体会完成 4 个阶段的检验动做:

  • 文件格式验证: 验证字节流是否符合 Class 文件格式的规范;例如:是否以 0xCAFEBABE 开头、主次版本号是否在当前虚拟机的处理范围以内、常量池中的常量是否有不被支持的类型。
  • 元数据验证: 对字节码描述的信息进行语义分析(注意:对比 javac 编译阶段的语义分析),以保证其描述的信息符合 Java 语言规范的要求;例如:这个类是否有父类,除了 java.lang.Object 以外。
  • 字节码验证: 经过数据流和控制流分析,肯定程序语义是合法的、符合逻辑的。
  • 符号引用验证: 确保解析动做能正确执行。

验证阶段是很是重要的,但不是必须的,它对程序运行期没有影响,若是所引用的类通过反复验证,那么能够考虑采用 -Xverifynone 参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。

准备:为类的静态变量分配内存,并将其初始化为默认值

准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在 方法区 中分配。对于该阶段有如下几点须要注意:

  • 1️⃣ 这时候进行内存分配的 仅包括类变量(static),而不包括实例变量,实例变量会在对象实例化时随着对象一块分配在 Java 堆中。

  • 2️⃣ 这里所设置的 初始值一般状况下是数据类型默认的零值(如 00Lnullfalse等),而不是被在 Java 代码中被显式地赋予的值。

  • 3️⃣ 若是类字段的字段属性表中存在 ConstantValue 属性,即 同时被 finalstatic 修饰,那么在准备阶段变量 value 就会被初始化为 ConstValue 属性所指定的值。

➡️ 例如,假设这里有一个类变量 public static int value = 666;,在准备阶段时初始值是 0 而不是 666,在 初始化阶段 才会被真正赋值为 666

➡️ 假设是一个静态类变量 public static final int value = 666;,则再准备阶段 JVM 就已经赋值为 666 了。

解析:把类中的符号引用转换为直接引用(重要)

解析阶段是虚拟机将常量池内的 符号引用 替换为 直接引用 的过程,解析动做主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符 7 类符号引用进行。

➡️ 符号引用 的做用是在编译的过程当中,JVM 并不知道引用的具体地址,因此用符号引用进行代替,而在解析阶段将会将这个符号引用转换为真正的内存地址。

➡️ 直接引用 能够理解为指向 类、变量、方法 的指针,指向 实例 的指针和一个 间接定位 到对象的对象句柄。

为了理解👆上面两种概念的区别,来看一个实际的例子吧:

public class Tester {

    public static void main(String[] args) {
        String str = "关注【我没有三颗心脏】,关注更多精彩";
        System.out.println(str);
    }
}

咱们先在该类同级目录下运行 javac Tester 编译成 .class 文件而后再利用 javap -verbose Tester 查看类的详细信息 (为了节省篇幅只截取了 main 方法反编译后的代码)

// 上面是类的详细信息省略...
{
 // .....
  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=1
         0: ldc           #7                  // String 关注【我没有三颗心脏】,关注更多精彩
         2: astore_1
         3: getstatic     #9                  // Field java/lang/System.out:Ljava/io/PrintStream;
         6: aload_1
         7: invokevirtual #15                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        10: return
      LineNumberTable:
        line 4: 0
        line 5: 3
        line 6: 10
}
SourceFile: "Tester.java"

能够看到,上面👆定义的 str 变量在编译阶段会被解析称为 符号引用,符号引用的标志是 astore_<n>,这里就是 astore_1

store_1的含义是将操做数栈顶的 关注【我没有三颗心脏】,关注更多精彩 保存回索引为 1 的局部变量表中,此时访问变量 str 就会读取局部变量表索引值为 1 中的数据。因此局部变量 str 就是一个符号引用。

再来看另一个例子:

public class Tester {

    public static void main(String[] args) {
        System.out.println("关注【我没有三颗心脏】,关注更多精彩");
    }
}

这一段代码反编译以后获得以下的代码:

// 上面是类的详细信息省略...
{
  // ......
  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #7                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #13                 // String 关注【我没有三颗心脏】,关注更多精彩
         5: invokevirtual #15                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 4: 0
        line 5: 8
}
SourceFile: "Tester.java"

咱们能够看到这里直接使用了 ldc 指令将 关注【我没有三颗心脏】,关注更多精彩 推送到了栈,紧接着就是调用指令 invokevirtual,并无将字符串存入局部变量表中,这里的字符串就是一个 直接引用

第三步:初始化(Initialization)

初始化,为类的静态变量赋予正确的初始值,JVM 负责对类进行初始化,主要对类变量进行初始化。在 Java 中对类变量进行初始值设定有两种方式:

  • 1️⃣ 声明类变量是指定初始值;
  • 2️⃣ 使用静态代码块为类变量指定初始值;

JVM 初始化步骤:

  • 1️⃣ 假如这个类尚未被加载和链接,则程序先加载并链接该类
  • 2️⃣ 假如该类的直接父类尚未被初始化,则先初始化其直接父类
  • 3️⃣ 假如类中有初始化语句,则系统依次执行这些初始化语句

类初始化时机:只有当对类的主动使用的时候才会致使类的初始化,类的主动使用包括如下几种:

  • 建立类的实例,也就是 new 的方式
  • 访问某个类或接口的静态变量,或者对该静态变量赋值
  • 调用类的静态方法
  • 反射(如 Class.forName("com.wmyskxz.Tester")
  • 初始化某个类的子类,则其父类也会被初始化
  • Java 虚拟机启动时被标明为启动类的类,直接使用 java.exe 命令来运行某个主类
  • 使用 JDK 7 新加入的动态语言支持时,若是一个 java.lang.invoke.MethodHanlde 实例最后的解析结果为 REF_getstaticREF_putstaticREF_invokeStaticREF_newInvokeSpecial 四种类型的方法句柄时,都须要先初始化该句柄对应的类
  • 接口中定义了 JDK 8 新加入的默认方法( default修饰符), 实现类在初始化以前须要先初始化其接口

Part 4. 深刻理解双亲委派模型

咱们在上面👆已经了解了一个类是如何被加载进 JVM 的——依靠类加载器——在 Java 语言中自带有三个类加载器:

  • Bootstrap ClassLoader 最顶层的加载类,主要加载   核心类库%JRE_HOME%\lib 下的 rt.jarresources.jarcharsets.jarclass 等。
  • Extention ClassLoader 扩展的类加载器,加载目录 %JRE_HOME%\lib\ext 目录下的 jar 包和 class 文件。
  • Appclass Loader 也称为 SystemAppClass 加载当前应用的 classpath 的全部类。

咱们能够经过一个简单的例子来简单了解 Java 中这些自带的类加载器:

public class PrintClassLoader {

    public static void main(String[] args) {
        printClassLoaders();
    }

    public static void printClassLoaders() {
        System.out.println("Classloader of this class:"
            + PrintClassLoader.class.getClassLoader());
        System.out.println("Classloader of Logging:"
            + com.sun.javafx.util.Logging.class.getClassLoader());
        System.out.println("Classloader of ArrayList:"
            + java.util.ArrayList.class.getClassLoader());
    }
}

上方程序打印输出以下:

Classloader of this class:sun.misc.Launcher$AppClassLoader@18b4aac2
Classloader of Logging:sun.misc.Launcher$ExtClassLoader@60e53b93
Classloader of ArrayList:null

如咱们所见,这里分别对应三种不一样类型的类加载器:AppClassLoader、ExtClassLoader 和 BootstrapClassLoader(显示为 null)。

一个很好的问题是:Java 类是由 java.lang.ClassLoader 实例加载的,但类加载器自己也是类,那么谁来加载类加载器呢?

咱们伪装不知道,先来跟着源码一步一步来看。

先来看看 Java 虚拟机入口代码

在 JDK 源码 sun.misc.Launcher 中,蕴含了 Java 虚拟机的入口方法:

public class Launcher {
    private static Launcher launcher = new Launcher();
    private static String bootClassPath =
        System.getProperty("sun.boot.class.path");

    public static Launcher getLauncher() {
        return launcher;
    }

    private ClassLoader loader;

    public Launcher() {
        // Create the extension class loader
        ClassLoader extcl;
        try {
            extcl = ExtClassLoader.getExtClassLoader();
        } catch (IOException e) {
            throw new InternalError(
                "Could not create extension class loader", e);
        }

        // Now create the class loader to use to launch the application
        try {
            loader = AppClassLoader.getAppClassLoader(extcl);
        } catch (IOException e) {
            throw new InternalError(
                "Could not create application class loader", e);
        }

        // 设置 AppClassLoader 为线程上下文类加载器,这个文章后面部分讲解
        Thread.currentThread().setContextClassLoader(loader);
    }
    /*
     * Returns the class loader used to launch the main application.
     */

    public ClassLoader getClassLoader() {
        return loader;
    }
    /*
     * The class loader used for loading installed extensions.
     */

    static class ExtClassLoader extends URLClassLoader {}
  /**
     * The class loader used for loading from java.class.path.
     * runs in a restricted security context.
     */

    static class AppClassLoader extends URLClassLoader {}
}

源码有精简,可是咱们能够获得如下信息:

1️⃣ Launcher 初始化了 ExtClassLoader 和 AppClassLoader。

2️⃣ Launcher 没有看到 Bootstrap ClassLoader 的影子,可是有一个叫作 bootClassPath 的变量,大胆一猜就是 Bootstrap ClassLoader 加载的 jar 包的路径。

(ps: 能够本身尝试输出一下 System.getProperty("sun.boot.class.path") 的内容,它正好对应了 JDK 目录 libclasses 目录下的 jar 包——也就是一般你配置环境变量时设置的 %JAVA_HOME/lib 的目录了——一样的方式你也能够看看 Ext 和 App 的源码)

3️⃣ ExtClassLoader 和 AppClassLoader 都继承自 URLClassLoader,进一步查看 ClassLoader 的继承树,传说中的双亲委派模型也并无出现。(甚至看不到 Bootstrap ClassLoader 的影子,Ext 也没有直接继承自 App 类加载器)

ClassLoader 继承树

(⚠️注意,这里能够明确看到每个 ClassLoader 都有一个 parent 变量,用于标识本身的父类,下面👇详细说)

4️⃣ 注意如下代码:

ClassLoader extcl;
        
extcl = ExtClassLoader.getExtClassLoader();

loader = AppClassLoader.getAppClassLoader(extcl);

分别跟踪查看到这两个 ClassLoader 初始化时的代码:

// 一直追踪到最顶层的 ClassLoader 定义,构造器的第二个参数标识了类加载器的父类
private ClassLoader(Void unused, ClassLoader parent) {
  this.parent = parent;
  // 代码省略.....
}
// Ext 设置本身的父类为 null
public ExtClassLoader(File[] var1) throws IOException {
  super(getExtURLs(var1), (ClassLoader)null, Launcher.factory);
  SharedSecrets.getJavaNetAccess().getURLClassPath(this).initLookupCache(this);
}
// 手动把 Ext 设置为 App 的 parent(这里的 var2 是传进来的 extc1)
AppClassLoader(URL[] var1, ClassLoader var2) {
  super(var1, var2, Launcher.factory);
  this.ucp.initLookupCache(this);
}

由此,咱们获得了这样一个类加载器的关系图:

类加载器的父类都来自哪里?

奇怪,为何 ExtClassLoader 的 parent 明明是 null,咱们却通常地认为 Bootstrap ClassLoader 才是 ExtClassLoader 的父加载器呢?

答案的一部分就藏在 java.lang.ClassLoader.loadClass() 方法里面:(这也就是著名的「双亲委派模型」现场了)

protected Class<?> loadClass(String name, boolean resolve)
  throws ClassNotFoundException
{
  synchronized (getClassLoadingLock(name)) {
    // 首先检查是否已经加载过了
    Class<?> c = findLoadedClass(name);
    if (c == null) {
      long t0 = System.nanoTime();
      try {
        if (parent != null) {
     // 父加载器不为空则调用父加载器的 loadClass 方法
          c = parent.loadClass(name, false);
        } else {
          // 父加载器为空则调用 Bootstrap ClassLoader
          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();
        // 父加载器没有找到,则调用 findclass
        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()
      resolveClass(c);
    }
    return c;
  }
}

代码逻辑很好地解释了双亲委派的原理。

1️⃣ 当前 ClassLoader 首先从 本身已经加载的类中查询是否此类已经加载,若是已经加载则直接返回原来已经加载的类。(每一个类加载器都有本身的加载缓存,当一个类被加载了之后就会放入缓存,等下次加载的时候就能够直接返回了。)

2️⃣ 当前 ClassLoader 的缓存中没有找到被加载的类的时候,委托父类加载器去加载,父类加载器采用一样的策略,首先查看本身的缓存,而后委托父类的父类去加载,一直到 Bootstrap ClassLoader。(当全部的父类加载器都没有加载的时候,再由当前的类加载器加载,并将其放入它本身的缓存中,以便下次有加载请求的时候直接返回。)

因此,答案的另外一部分是由于最高一层的类加载器 Bootstrap 是经过 C/C++ 实现的,并不存在于 JVM 体系内 (不是一个 Java 类,没办法直接表示为 ExtClassLoader 的父加载器),因此输出为 null

(咱们能够很轻易跟踪到 findBootstrapClass() 方法被 native 修饰:private native Class<?> findBootstrapClass(String name);

➡️ OK,咱们理解了为何 ExtClassLoader 的父加载器为何是表示为 null 的 Bootstrap 加载器,那咱们 本身实现的 ClassLoader 父加载器应该是谁呢?

观察一下 ClassLoader 的源码就知道了:

protected ClassLoader(ClassLoader parent) {
    this(checkCreateClassLoader(), parent);
}
protected ClassLoader() {
    this(checkCreateClassLoader(), getSystemClassLoader());
}

类加载器的 parent 的赋值是在 ClassLoader 对象的构造方法中,它有两个状况:

1️⃣ 由外部类建立 ClassLoader 时直接指定一个 ClassLoader 为 parent

2️⃣ 由 getSystemClassLoader() 方法生成,也就是在 sun.misc.Laucher 经过 getClassLoader() 获取,也就是 AppClassLoader。直白的说,一个 ClassLoader 建立时若是没有指定 parent,那么它的 parent 默认就是 AppClassLoader。(建议去看一下源码)

为何这样设计呢?

简单来讲,主要是为了 安全性,避免用户本身编写的类动态替换 Java 的一些核心类,好比 String,同时也 避免了重复加载,由于 JVM 中区分不一样类,不只仅是根据类名,相同的 class 文件被不一样的 ClassLoader 加载就是不一样的两个类,若是相互转型的话会抛 java.lang.ClassCaseException

若是咱们要实现本身的类加载器,无论你是直接实现抽象类 ClassLoader,仍是继承 URLClassLoader 类,或者其余子类,它的父加载器都是 AppClassLoader。

由于无论调用哪一个父类构造器,建立的对象都必须最终调用 getSystemClassLoader() 做为父加载器 (咱们已经从上面👆的源码中看到了)。而该方法最终获取到的正是 AppClassLoader (别称 SystemClassLoader)

这也就是咱们熟知的最终的双亲委派模型了。

Part 5. 实现本身的类加载器

什么状况下须要自定义类加载器

在学习了类加载器的实现机制以后,咱们知道了双亲委派模型并不是强制模型,用户能够自定义类加载器,在什么状况下须要自定义类加载器呢?

1️⃣ 隔离加载类。在某些框架内进行中间件与应用的模块隔离,把类加载器到不一样的环境。好比,阿里内某容器框架经过自定义类加载器确保应用中依赖的 jar 包不会影响到中间件运行时使用的 jar 包。

2️⃣ 修改类加载方式。类的加载模型并不是强制,除了 Bootstrap 外,其余的加载并不是必定要引入,或者根据实际状况在某个时间点进行按需的动态加载。

3️⃣ 扩展加载源。好比从数据库、网络,甚至是电视机顶盒进行加载。(下面👇咱们会编写一个从网络加载类的例子)

4️⃣ 防止源码泄露。Java 代码容易被编译和篡改,能够进行编译加密。那么类加载器也须要自定义,还原加密的字节码。

一个常规的例子

实现一个自定义的类加载器比较简单:继承 ClassLoader,重写 findClass() 方法,调用 defineClass() 方法,就差很少行了。

Tester.java

咱们先来编写一个测试用的类文件:

public class Tester {

    public void say() {
        System.out.println("关注【我没有三颗心脏】,解锁更多精彩!");
    }
}

在同级目录下执行 javac Tester.java 命令,并把编译后的 Tester.class 放到指定的目录下(我这边为了方便就放在桌面上啦 /Users/wmyskxz/Desktop

MyClassLoader.java

咱们编写自定义 ClassLoader 代码:

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

public class MyClassLoader extends ClassLoader {

    private final String mLibPath;

    public MyClassLoader(String path) {
        // TODO Auto-generated constructor stub
        mLibPath = path;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        // TODO Auto-generated method stub

        String fileName = getFileName(name);

        File file = new File(mLibPath, fileName);

        try {
            FileInputStream is = new FileInputStream(file);

            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            int len = 0;
            try {
                while ((len = is.read()) != -1) {
                    bos.write(len);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }

            byte[] data = bos.toByteArray();
            is.close();
            bos.close();

            return defineClass(name, data, 0, data.length);

        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

        return super.findClass(name);
    }

    // 获取要加载的 class 文件名
    private String getFileName(String name) {
        // TODO Auto-generated method stub
        int index = name.lastIndexOf('.');
        if (index == -1) {
            return name + ".class";
        } else {
            return name.substring(index + 1) + ".class";
        }
    }
}

咱们在 findClass() 方法中定义了查找 class 的方法,而后数据经过 defineClass() 生成了 Class 对象。

ClassLoaderTester 测试类

咱们须要删除刚才在项目目录建立的 Tester.java 和编译后的 Tester.class 文件来观察效果:

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class ClassLoaderTester {

    public static void main(String[] args) {
        // 建立自定义的 ClassLoader 对象
        MyClassLoader myClassLoader = new MyClassLoader("/Users/wmyskxz/Desktop");
        try {
            // 加载class文件
            Class<?> c = myClassLoader.loadClass("Tester");

            if(c != null){
                try {
                    Object obj = c.newInstance();
                    Method method = c.getDeclaredMethod("say",null);
                    //经过反射调用Test类的say方法
                    method.invoke(obj, null);
                } catch (InstantiationException | IllegalAccessException
                    | NoSuchMethodException
                    | SecurityException |
                    IllegalArgumentException |
                    InvocationTargetException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
        } catch (ClassNotFoundException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
}

运行测试,正常输出:

关注【我没有三颗心脏】,解锁更多精彩!

加密解密类加载器

突破了 JDK 系统内置加载路径的限制以后,咱们就能够编写自定义的 ClassLoader。你彻底能够按照本身的意愿进行业务的定制,将 ClassLoader 玩出花样来。

例如,一个加密解密的类加载器。(不涉及完整代码,咱们能够来讲一下思路和关键代码)

首先,在编译以后的字节码文件中动一动手脚,例如,给文件每个 byte 异或一个数字 2:(这就算是模拟加密过程)

File file = new File(path);
try {
  FileInputStream fis = new FileInputStream(file);
  FileOutputStream fos = new FileOutputStream(path+"en");
  int b = 0;
  int b1 = 0;
  try {
    while((b = fis.read()) != -1){
      // 每个 byte 异或一个数字 2
      fos.write(b ^ 2);
    }
    fos.close();
    fis.close();
  } catch (IOException e) {
    // TODO Auto-generated catch block
    e.printStackTrace();
  }
catch (FileNotFoundException e) {
  // TODO Auto-generated catch block
  e.printStackTrace();
}

而后咱们再在 findClass() 中本身解密:

File file = new File(mLibPath,fileName);

try {
  FileInputStream is = new FileInputStream(file);

  ByteArrayOutputStream bos = new ByteArrayOutputStream();
  int len = 0;
  byte b = 0;
  try {
    while ((len = is.read()) != -1) {
      // 将数据异或一个数字 2 进行解密
      b = (byte) (len ^ 2);
      bos.write(b);
    }
  } catch (IOException e) {
    e.printStackTrace();
  }

  byte[] data = bos.toByteArray();
  is.close();
  bos.close();

  return defineClass(name,data,0,data.length);

catch (IOException e) {
  // TODO Auto-generated catch block
  e.printStackTrace();
}

(代码几乎与上面👆一个例子等同,因此只说一下思路和完整代码)

网络类加载器

其实很是相似,也不作过多讲解,直接上代码:

import java.io.ByteArrayOutputStream;  
import java.io.InputStream;  
import java.net.URL;  
  
public class NetworkClassLoader extends ClassLoader {  
  
    private String rootUrl;  
  
    public NetworkClassLoader(String rootUrl) {  
        // 指定URL  
        this.rootUrl = rootUrl;  
    }  
  
    // 获取类的字节码  
    @Override  
    protected Class<?> findClass(String name) throws ClassNotFoundException {  
        byte[] classData = getClassData(name);  
        if (classData == null) {  
            throw new ClassNotFoundException();  
        } else {  
            return defineClass(name, classData, 0, classData.length);  
        }  
    }  
  
    private byte[] getClassData(String className) {  
        // 从网络上读取的类的字节  
        String path = classNameToPath(className);  
        try {  
            URL url = new URL(path);  
            InputStream ins = url.openStream();  
            ByteArrayOutputStream baos = new ByteArrayOutputStream();  
            int bufferSize = 4096;  
            byte[] buffer = new byte[bufferSize];  
            int bytesNumRead = 0;  
            // 读取类文件的字节  
            while ((bytesNumRead = ins.read(buffer)) != -1) {  
                baos.write(buffer, 0, bytesNumRead);  
            }  
            return baos.toByteArray();  
        } catch (Exception e) {  
            e.printStackTrace();  
        }  
        return null;  
    }  
  
    private String classNameToPath(String className) {  
        // 获得类文件的URL  
        return rootUrl + "/"  
                + className.replace('.''/') + ".class";  
    }  
}  

(代码来自:https://blog.csdn.net/justloveyou_/article/details/72217806)

Part 6. 必要的扩展阅读

学习到这里,咱们对 ClassLoader 已经再也不陌生了,可是仍然有一些必要的知识点须要去掌握 (限于篇幅和能力这里不扩展了),但愿您能认真阅读如下的材料:(可能排版上面层次不齐,但内容都是有质量的,并用 ♨️ 标注了更加剧点一些的内容)

1️⃣ ♨️能不能本身写一个类叫 java.lang.System 或者 java.lang.String - https://blog.csdn.net/tang9140/article/details/42738433

2️⃣ 深刻理解 Java 之 JVM 启动流程 - https://cloud.tencent.com/developer/article/1038435

3️⃣ ♨️真正理解线程上下文类加载器(多案例分析) - https://blog.csdn.net/yangcheng33/article/details/52631940

4️⃣ ♨️曹工杂谈:Java 类加载器还会死锁?这是什么状况? - https://www.cnblogs.com/grey-wolf/p/11378747.html#_label2

5️⃣ 谨防JDK8重复类定义形成的内存泄漏 - https://segmentfault.com/a/1190000022837543

7️⃣ ♨️Tomcat 类加载器的实现 - https://juejin.im/post/6844903945496690695

8️⃣ ♨️Spring 中的类加载机制 - https://www.shuzhiduo.com/A/gVdnwgAlzW/

参考资料

  1. 《深刻分析 Java Web 技术内幕》 | 许令波 著
  2. Java 类加载机制分析 - https://www.jianshu.com/p/3615403c7c84
  3. Class 文件解析实战 - https://juejin.im/post/6844904199617003528
  4. 图文兼备看懂类加载机制的各个阶段,就差你了!- https://juejin.im/post/6844904119258316814
  5. Java面试知识点解析(三)——JVM篇 - https://www.wmyskxz.com/2018/05/16/java-mian-shi-zhi-shi-dian-jie-xi-san-jvm-pian/
  6. 一看你就懂,超详细Java中的ClassLoader详解 - https://blog.csdn.net/briblue/article/details/54973413
  • 本文已收录至个人 Github 程序员成长系列 【More Than Java】,学习,不止 Code,欢迎 star:https://github.com/wmyskxz/MoreThanJava
  • 我的公众号 :wmyskxz, 我的独立域名博客:wmyskxz.com,坚持原创输出,下方扫码关注,2020,与您共同成长!

(END)




有你想看的 精彩




「MoreThanJava」计算机发展史—从织布机到IBM
全网最通透的Java8版本特性讲解
什么?Java9这些史诗级更新你都不知道?Java9特性一文打尽!
Java 14版本特性【一文了解】它来了!
这里有你不得不了解的Java 11版本特性说明
面试问我,建立多少个线程合适?我该怎么说
【吐血推荐】领域驱动设计学习输出
「懂一点设计」一篇文章读懂交互设计7大定律



很是感谢各位人才能 看到这里,若是以为本篇文章写得不错,以为 「我没有三颗心脏」有点东西 的话,求点赞,求关注,求分享,求留言!

创做不易,各位的支持和承认,就是我创做的最大动力,咱们下篇文章见!


点击留言

本文分享自微信公众号 - 我没有三颗心脏(wmyskxz)。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。

相关文章
相关标签/搜索