深入浅出“类加载器”

内容概述

  • “类加载”介绍
  • “类加载器”介绍
  • 深入“类加载器”
  • 深入“父亲委托机制”

一,“类加载”介绍

“加载”是类加载的时机的第一阶段。
类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括(5个阶段):
① 加载(Loading)
② 连接(Linking)
     [1] 验证(Verification)
     [2] 准备(Preparation)
     [3] 解析(Resolution)
③ 初始化(Initialization)
④ 使用(Using)
⑤ 卸载(Unloading)

类的生命周期
什么是“加载”

加载:就是把二进制形式的 Java 类型读入 Java 虚拟机中

类的加载的最终产品是位于内存中的 Class 对象。
Class 对象封装了类在方法区内的数据结构,并且向 Java 程序员提供了访问方法区的数据结构的接口。

注意:类的加载并不需呀等到某个类被“首次主动使用”时再加载它。JVM 规范允许类加载器在预料某个类将要被使用时就预先加载它。

二,“类加载器”介绍

主要有2种类型的类加载器
  1. Java 虚拟机自带的加载器
    ① 根类加载器(Bootstrap)
    ② 扩展类加载器(Extension)
    ③ 系统(应用)类加载器(System)
  2. 用户自定义的类加载器
    ① java.lang.ClassLoader 的子类
    ② 用户可以定制类的加载方式
父亲(双亲)委托机制

从 JDK 1.2 版本开始,类的加载过程采用父亲(双亲)委托机制,这种机制能更好地保证 Java 平台的安全。在此委托机制中,除了 Java 虚拟机自带的根类加载器以外,其余的类加载器都有且只有一个父加载器。

三,深入“类加载器”

各个加载器的加载路径(IntelliJ IDEA环境下):

public class MyTest18 {
    public static void main(String[] args) {
        System.out.println(System.getProperty("sun.boot.class.path"));
        System.out.println("=================");
        System.out.println(System.getProperty("java.ext.dirs"));
        System.out.println("=================");
        System.out.println(System.getProperty("java.class.path"));
    }
}

# 控制台
/Library/Java/JavaVirtualMachines/jdk1.8.0_144.jdk/Contents/Home/jre/lib/resources.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_144.jdk/Contents/Home/jre/lib/rt.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_144.jdk/Contents/Home/jre/lib/sunrsasign.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_144.jdk/Contents/Home/jre/lib/jsse.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_144.jdk/Contents/Home/jre/lib/jce.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_144.jdk/Contents/Home/jre/lib/charsets.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_144.jdk/Contents/Home/jre/lib/jfr.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_144.jdk/Contents/Home/jre/classes
=================
/Users/linling/Library/Java/Extensions:/Library/Java/JavaVirtualMachines/jdk1.8.0_144.jdk/Contents/Home/jre/lib/ext:/Library/Java/Extensions:/Network/Library/Java/Extensions:/System/Library/Java/Extensions:/usr/lib/java
=================
/Library/Java/JavaVirtualMachines/jdk1.8.0_144.jdk/Contents/Home/jre/lib/charsets.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_144.jdk/Contents/Home/jre/lib/deploy.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_144.jdk/Contents/Home/jre/lib/ext/cldrdata.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_144.jdk/Contents/Home/jre/lib/ext/dnsns.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_144.jdk/Contents/Home/jre/lib/ext/jaccess.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_144.jdk/Contents/Home/jre/lib/ext/jfxrt.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_144.jdk/Contents/Home/jre/lib/ext/localedata.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_144.jdk/Contents/Home/jre/lib/ext/nashorn.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_144.jdk/Contents/Home/jre/lib/ext/sunec.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_144.jdk/Contents/Home/jre/lib/ext/sunjce_provider.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_144.jdk/Contents/Home/jre/lib/ext/sunpkcs11.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_144.jdk/Contents/Home/jre/lib/ext/zipfs.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_144.jdk/Contents/Home/jre/lib/javaws.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_144.jdk/Contents/Home/jre/lib/jce.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_144.jdk/Contents/Home/jre/lib/jfr.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_144.jdk/Contents/Home/jre/lib/jfxswt.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_144.jdk/Contents/Home/jre/lib/jsse.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_144.jdk/Contents/Home/jre/lib/management-agent.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_144.jdk/Contents/Home/jre/lib/plugin.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_144.jdk/Contents/Home/jre/lib/resources.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_144.jdk/Contents/Home/jre/lib/rt.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_144.jdk/Contents/Home/lib/ant-javafx.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_144.jdk/Contents/Home/lib/dt.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_144.jdk/Contents/Home/lib/javafx-mx.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_144.jdk/Contents/Home/lib/jconsole.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_144.jdk/Contents/Home/lib/packager.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_144.jdk/Contents/Home/lib/sa-jdi.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_144.jdk/Contents/Home/lib/tools.jar:/Users/linling/Documents/code/IdeaWorkspace/shengsiyuan/jvm_lecture/out/production/classes:/Applications/IntelliJ IDEA.app/Contents/lib/idea_rt.jar


根(Bootstrap)类加载器

该加载器没有父加载器。它负责加载虚拟机的核心类库,如 java.lang.* 等。根类加载器从系统属性 sun.boot.class.path 所指定的目录中加载类库。根类加载器的实现依赖于底层操作系统,属于虚拟机的实现的一部分,它并没有继承 java.lang.ClassLoader 类。

  • 示例一:让启动类加载器加载我们自定义的类
    通过上面的例子我们能够看到。启动类加载会去加载“/Library/Java/JavaVirtualMachines/jdk1.8.0_144.jdk/Contents/Home/jre/classes”目录下的字节码文件。
    因此,这里,我们将 MyTest1.class 放到这个目录下。注意:“/Library/Java/JavaVirtualMachines/jdk1.8.0_144.jdk/Contents/Home/jre/classes”目录默认是不存在的,是我自己创建的。
# MyTest18_1
public class MyTest18_1 {
    public static void main(String[] args) throws Exception {
        MyTest16 loader1 = new MyTest16("loader1");
        loader1.setPath("/Users/linling/Documents/code/shengsisyuan/");
        Class<?> clazz = loader1.loadClass("com.bayern.shengsiyuan.jvm_lecture.lesson1.MyTest1");
        System.out.println("class : " + clazz.hashCode());
        System.out.println("class loader : " + clazz.getClassLoader());
    }
}

# 控制台
class : 1360875712
class loader : null

👆『class loader : null』可见,MyTest1 已经由“启动类加载器”加载了。

  • 示例二
修改“启动类加载器”的路径

即,修改“sub.boot.class.path”参数值的路径。

➜  classes git:(master) ✗ java -Dsun.boot.class.path=./ com.bayern.shengsiyuan.jvm_lecture.lesson8.MyTest22
Error occurred during initialization of VM
java/lang/NoClassDefFoundError: java/lang/Object

在 Oracle 的 Hotspot 实现中,系统属性 sun.boot.class.path 如果修改错了,则运行会出错,提示如下错误信息:
Error occurred during initialization of VM
java/lang/NoClassDefFoundError: java/lang/Object

扩展(Extension)类加载器

它的父加载器为根类加载器。它从 java.ext.dirs 系属性所指定的目录中加载类库,或者从 JDK 的安装目录的 jre\lib\ext 子目录(扩展目录)下加载类库。扩展类加载器是纯 Java 类,是 java.lang.ClassLoader 类的子类。

  • 示例一:修改“扩展类加载器”的加载路径
    a)修改前:
# MyTest19
public class MyTest19 {

    public static void main(String[] args) {
        AESKeyGenerator aesKeyGenerator = new AESKeyGenerator();

        System.out.println(aesKeyGenerator.getClass().getClassLoader());
        System.out.println(MyTest19.class.getClassLoader());

    }
}

# 控制台
sun.misc.Launcher$ExtClassLoader@4dc63996
sun.misc.Launcher$AppClassLoader@18b4aac2

b)修改“java.ext.dirs”属性

# 终端

➜  classes git:(master) ✗ pwd

/Users/linling/Documents/code/IdeaWorkspace/shengsiyuan/jvm_lecture/out/production/classes

➜  classes git:(master) ✗ java -Djava.ext.dirs=./ com.bayern.shengsiyuan.jvm_lecture.lesson7.MyTest19

Exception in thread "main" java.lang.NoClassDefFoundError: com/sun/crypto/provider/AESKeyGenerator
    at com.bayern.shengsiyuan.jvm_lecture.lesson7.MyTest19.main(MyTest19.java:8)
Caused by: java.lang.ClassNotFoundException: com.sun.crypto.provider.AESKeyGenerator
    at [java.net.URLClassLoader.findClass(URLClassLoader.java:381](http://java.net.urlclassloader.findclass(urlclassloader.java:381/))
    at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
    at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:335)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:357)


  • 示例二
# MyTest22
public class MyTest22 {

    static {
        System.out.println("MyTest22 initializer");
    }

    public static void main(String[] args) {
        System.out.println(MyTest22.class.getClassLoader());

        System.out.println(MyTest1.class.getClassLoader());
    }
}

# 终端
➜  classes git:(master) ✗ pwd
/Users/linling/Documents/code/IdeaWorkspace/shengsiyuan/jvm_lecture/out/production/classes
➜  classes git:(master) ✗ java -Djava.ext.dirs=./ com.bayern.shengsiyuan.jvm_lecture.lesson8.MyTest22
MyTest22 initializer
sun.misc.Launcher$AppClassLoader@73d16e93
sun.misc.Launcher$AppClassLoader@73d16e93

👆即便修改了“扩展类加载器”的加载路径(确保“扩展类加载器”可以找到MyTest22.class),但依旧显示是“应用类加载器”加载的MyTest22.class!!

这是因为,“扩展类加载器”所要加载的文件(.class文件)还不能以“.class”的文件存在,而必须以jar包的形式存在(因此你需要将 .class 文件打成一个jar包)。扩展类加载器会尝试从jar包加载 ‘.class’ 文件。
但,“启动类加载器”和“应用类加载器”没有这样的限制。他们可以从目录下直接加载’.class’文件

系统(System)类加载器

也称为应用类加载器,它的父加载器为扩展类加载器。它从环境变量 classpath 或者系统属性 java.class.path 所指定的目录中加载类,它是用户自定义的类加载器的默认父加载器。系统类加载器是纯 Java 类,是 java.lang.ClassLoader 类的子类。

  • 示例:将自定义的 MyTest16类 加载作为’系统类加载器’
# MyTest16 的实现见下文
# MyTest23
public class MyTest23 {
    public static void main(String[] args) {
    System.out.println(System.getProperty("java.system.class.loader"));
    System.out.println("-----------");
    System.out.println(ClassLoader.getSystemClassLoader());
    System.out.println("-----------");
    System.out.println(MyTest23.class.getClassLoader());
    }
}

# 终端
classes git:(master) ✗ java -Djava.system.class.loader=com.bayern.shengsiyuan.jvm_lecture.lesson6.MyTest16 
com.bayern.shengsiyuan.jvm_lecture.lesson6.MyTest16
-----------
com.bayern.shengsiyuan.jvm_lecture.lesson6.MyTest16@4e25154f
-----------
sun.misc.Launcher$AppClassLoader@18b4aac2

注意,前面说“默认的系统类加载器(AppClassLoader)”的加载路径是“java.class.path”属性的值。当你修改了系统类加载器为你自定的类加载器后,自定义的类加载器并不会去加载“java.class.path”属性值所执行的路径(因为,这是在AppClassLoader代码中去实现的)。因此,你会发现,类还是会由“AppClassLoader”来加载,即便此时它不再是系统类加载器了(而是系统类加载器的父加载器)

用户自定义类加载器

除了虚拟机自带的加载器外,用户还可以定制自己的类加载器。Java 提供了抽象类 java.lang.ClassLoader,所有用户自定义类的类加载器都应该继承 ClassLoader 类。

# “用户自定义类加载器”实例
public class MyTest16 extends ClassLoader {

    private String classLoaderName;

    private final String fileExtension = ".class";

    public MyTest16(String classLoaderName) {
        super();    // 将系统类加载器当做该类加载器的父加载器
        this.classLoaderName = classLoaderName;
    }

    public MyTest16(ClassLoader parent, String classLoaderName) {
        super(parent);  // 显示指定该类加载器的父加载器
        this.classLoaderName = classLoaderName;
    }

    @Override
    public String toString() {
        return "[" + this.classLoaderName + "]";
    }

    private byte[] loadClassData(String name) {
        byte[] data = null;
        try(InputStream is = new FileInputStream(new File(name + this.fileExtension));
        ByteArrayOutputStream baos = new ByteArrayOutputStream();) {
            this.classLoaderName = this.classLoaderName.replace(".","/");
            int ch;
            while (-1 != (ch = is.read())) {
                baos.write(ch);
            }
            data = baos.toByteArray();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return data;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] data = loadClassData(name);
        return defineClass(name, data, 0, data.length);
    }

    public static void test(ClassLoader classLoader) throws Exception {
        Class<?> clazz = classLoader.loadClass("com.bayern.shengsiyuan.jvm_lecture.lesson1.MyTest1");
        Object object = clazz.newInstance();
        System.out.println(object);
    }

    public static void main(String[] args) throws Exception {
        MyTest16 loader1 = new MyTest16("loader1");
        test(loader1);
    }
}

# 控制台
com.bayern.shengsiyuan.jvm_lecture.lesson1.MyTest1@60e53b93



public class MyTest13 {

    public static void main(String[] args) {
        ClassLoader classLoader = ClassLoader.getSystemClassLoader();
        System.out.println(classLoader);

        while (null != classLoader) {
            classLoader = classLoader.getParent();
            System.out.println(classLoader);
        }
    }
}

# 控制台
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@60e53b93
null


获取 ClassLoader 的几种途径

# 获得当前类的 ClassLoader
clazz.getClassLoader();

# 获得当前线程上下文的 ClassLoader
Thread.currentThread().getContextClassLoader()

# 获得系统的 ClassLoader
ClassLoader.getSystemClassLoader()

# 获得调用者的 ClassLoader
Reflection.getCallerClass()

注意:调用 ClassLoader 类的 loadClass 方法加载一个类,并不是对类的主动使用,不会导致类的初始化。

public class MyTest12 {
    public static void main(String[] args) throws Exception {
        ClassLoader loader = ClassLoader.getSystemClassLoader();
        Class<?> clazz = loader.loadClass("com.bayern.shengsiyuan.jvm_lecture.lesson4.CL");
        System.out.println(clazz);
    }
}

class CL {
    static {
        // 若 CL 类被初始化,下面的语句会被打印。
        System.out.println("Class CL");
    }
}

# 控制台
class com.bayern.shengsiyuan.jvm_lecture.lesson4.CL
可见 CL 类并没有被初始化。


数组 与 类加载器

对于数组类而言,情况就有所不同,数组类本身不通过类加载器创建,它是由Java虚拟机在运行时直接创建的(‘数组’的父类是’Object’)。但数组类与类加载器仍然有很密切的关系,因为数组类的元素类型(Element Type,指的是数组去掉所有维度的类型)最终是要靠类加载器去创建。
如果数组的组件类型(Component Type,指的是数组去掉一个维度的类型)是引用类型,那就递归采用本节中定义的加载过程去加载这个组件类型,数组C将在加载该组件类型的类加载器的类名称空间上被标识。
如果数组的组件类型不是引用类型(例如int[]数组),Java虚拟机将会把数组C标记为与引导类加载器关联。

public class MyTest15 {
    public static void main(String[] args) {
        String[][] strings = new String[2][];
        System.out.println(strings.getClass().getClassLoader());
        System.out.println("=============================");
        MyTest15[] myTest15s = new MyTest15[2];
        System.out.println(myTest15s.getClass().getClassLoader());
        System.out.println("=============================");
        int[] ints = new int[2];
        System.out.println(ints.getClass().getClassLoader());
    }
}

# 控制台
null
=============================
sun.misc.Launcher$AppClassLoader@18b4aac2
=============================
null

所以,这里。strings.getClass().getClassLoader() 和 ints.getClass().getClassLoader() 都返回 null,标签其都是通过“引导类加载器”加载的。
PS :即便是多维数组,其 getClass().getClassLoader() 返回的也是,加载元素类的类加载器。

类加载器之间的关系

系统的类(除了数组,数组是由JVM创建的)一定是由类加载器来去加载的。

“扩展类加载器”和“应用类加载器”以及“自定义类加载器”都是由“启动类加载器”加载的,具体详见从 sun.misc.Launcher 类源码深入探索 ClassLoader

“启动类加载器”是C++代码,并且内建与JVM,也就是说,它是JVM的一部分。而其他类加载器都是Java代码(前提,在HotSpot中)

内建于JVM中的启动类加载器会加载java.lang.ClassLoader以及其他的Java平台类,

当JVM启动时,一块特殊的机器码会运行,它会加载扩展类加载器与系统类加载器,

这块特殊的机器码叫做'启动类加载器'(Bootstrap)。

启动类加载器并不是Java类,而其他的加载器则都是Java类,

启动类加载器是特定于平台的机器指令,它负责开启整个加载过程。

所有类加载器(除了启动类加载器)都被实现为Java类,不过,总归要有一个组件来加载第一个Java类加载器,从而让整个加载过程能够顺利进行下去,加载第一个纯Java类加载器就是启动类加载器的职责。

启动类加载器还会负责加载供JRE正常运行所需要的基本组件,这包括java.util与java.lang包中的类。

四,深入“父亲委托机制”

“父亲委托机制”是什么?

除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。这里类加载器之间的父子关系一般不会以继承(Inheritance)的关系来实现,而是都使用组合(Composition)关系来复用父加载器的代码。类加载器之间的这种层次关系,称为类加载器的双亲委派模型(Parents Delegation Model)。

双亲委派模式并不是一个强制性的约束模型,而是Java设计者推荐给开发者的一种类加载器实现方式。

双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。

自底向上检测类是否已经加载,自顶向下尝试加载类

双亲委托机制是HotSpot VM默认自带的机制,但并不是所有环境下类加载器都遵循着这种委托机制。比如,OSGI标准就打破了类加载器的这种双亲委托机制!

类加载器的双亲委托模型的好处:
  1. 可以确保Java核心库类型安全(核心库:即,JDK自带的基础类):所有的Java应用都至少会引用java.lang.Object类,也就是说在运行期,java.lang.Object这个类会被加载到Java虚拟机中;如果这个加载过程是由Java应用自己的类加载器所完成的,那么很可能就会在JVM中存在多个版本的java.lang.Object类,而且这些类之间还是不兼容的,相互不可见的(正是命名空间在发挥着作用)。借助于双亲委托机制,Java核心类库中的类的加载工作都是由启动类加载器来统一完成,从而确保了Java应用所使用的都是同一个版本的Java核心类库,他们之间是相互兼容的。
  2. 可以确保Java核心类库所提供的类不会被自定义的类所替代。(因为,双亲委派的机制会确保 Java核心类库是由启动类加载器加载的,这样就无法通过自定义类加载器加载了。同时,JVM也有相应的检测来强制Java核心类库只能由启动类加载器加载,即便你强制修改了启动类加载器的加载路径)
  3. 不同的类加载器可以为相同名称(binary name)的类创建额外的命名空间,相同名称的类可以并存在Java虚拟机中,只需要用不同的类加载器来加载他们即可。不同类加载器所加载的类之间是不兼容的,这就相当于在Java虚拟机内部创建了一个又一个相互隔离的Java类空间,这类技术在很多框架中都得到了实际应用。

Tomcat 等web应用框架,并没有使用默认的双亲委派机制,而是使用了自己的类加载机制的实现方式。

“定义类加载器” 与 “初始类加载器”

若有一个类加载器能够成功加载 Test 类,那么这个类加载器被成为“定义类加载器”,所有能成功返回 Class 对象引用的类加载器(包括定义类加载器)都被成为“初始类加载器”。
👆这个概念是基于“类的父亲委托机制”的。
也就是说,真正负责成功加载这个类的加载器,我们称之为“定义类加载器”。
接受类加载请求,通过调用loadClass来开启类的加载过程的加载器被称为“初始类加载器”。

两种类加载器的关联之处在于:一个类的定义加载器是它引用的其它类的初始加载器。(有点晦涩,这句话可以举例说明:ClassA的类加载器为ClassLoaderA,ClassLoaderB是ClassLoaderA父类加载器,那么当ClassLoaderA初始加载ClassA时,由于类加载器的代理模式,则会调用父类加载器ClassLoaderB来定义ClassA,所以ClassLoaderA叫做ClassA的初始加载器,而ClassLoaderB叫做ClassA的定义加载器,然而ClassA中引用了ClassC,那么当父类加载器ClassLoaderB定义ClassLoaderA时,会初始加载ClassC,所以ClassLoaderB又叫做ClassC的初始加载器,又由于类加载器的代理模式,则会调用ClassLoaderB的父类加载器--系统类加载器来定义ClassC,所以最后系统类加载器叫作ClassC的定义加载器)。

命名空间

每个类加载器都有自己的命名空间,命名空间由该加载器及所有父加载器所记载的类组成。
在同一个命名空间中,不会出现类的完整名字(包括类的包名)相同的两个类
在不同的命名空间中,有可能会出现类的完整名字(包括类的包名)相同的两个类

# 修改 MyTest16 类的 main 方法
public static void main(String[] args) throws Exception {
        MyTest16 loader1 = new MyTest16("loader1");
        loader1.setPath("/Users/linling/Documents/code/shengsisyuan/");
        Class<?> clazz = loader1.loadClass("com.bayern.shengsiyuan.jvm_lecture.lesson1.MyTest1");
        System.out.println("class : " + clazz.hashCode());
        Object object = clazz.newInstance();
        System.out.println(object);
        System.out.println();

        loader1 = new MyTest16("loader1");
        loader1.setPath("/Users/linling/Documents/code/shengsisyuan/");
        clazz = loader1.loadClass("com.bayern.shengsiyuan.jvm_lecture.lesson1.MyTest1");
        System.out.println("class : " + clazz.hashCode());
        object = clazz.newInstance();
        System.out.println(object);
    }

a)注意,前提,将CLASSPATH下的“MyTest1.class”保留!!!("/Users/linling/Documents/code/shengsisyuan/“目录下存在所需的所有字节码文件)

# 控制台
class : 1625635731
com.bayern.shengsiyuan.jvm_lecture.lesson1.MyTest1@5e2de80c

class : 1625635731
com.bayern.shengsiyuan.jvm_lecture.lesson1.MyTest1@1d44bcfa

首先,class对象的hashcode一样,说明类只会被加载一次,前面被加载过后,后面就直接用了!!
其次,说明此时MyTest1类是由系统类加载器加载的。

b)注意,前提,将CLASSPATH下的“MyTest1.class”删除!!!("/Users/linling/Documents/code/shengsisyuan/“目录下存在所需的所有字节码文件)

# 控制台
findClass invoked: com.bayern.shengsiyuan.jvm_lecture.lesson1.MyTest1
class loader name : loader1
class : 491044090
com.bayern.shengsiyuan.jvm_lecture.lesson1.MyTest1@266474c2

findClass invoked: com.bayern.shengsiyuan.jvm_lecture.lesson1.MyTest1
class loader name : loader1
class : 1725154839
com.bayern.shengsiyuan.jvm_lecture.lesson1.MyTest1@63947c6b

这里,MyTest1被加载了2次,因为loader1和loader2的父加载器都是系统类加载器,而在系统类加载器中是无法查询到MyTest1的,所以分别由自定的类加载器loader1和loader2进行MyTest1的加载。

不同类加载器的命名空间的关系

a)同一个命名空间内的类是相互可见的。
b)子加载器的命名空间包含所有父加载器的命名空间。因此由子加载器加载的类能看见父加载器加载的类。例如,系统类加载器加载的类能看见根类加载器加载的类。
c)由父加载器加载的类不能看见子加载器加载的类。
如果两个加载器之间没有直接或间接的父子关系,那么它们各自加载的类相互不可见。

注意,这里说是“可见的”,但能不能访问是由“访问修饰符”决定的。

# MyTest17
public class MyTest17 extends ClassLoader {
    public static void main(String[] args) throws Exception {
        MyTest16 loader1 = new MyTest16("loader1”);
        loader1.setPath("/Users/linling/Documents/code/shengsisyuan/");
        Class<?> clazz = loader1.loadClass("com.bayern.shengsiyuan.jvm_lecture.lesson7.MySample");
        System.out.println("class : " + clazz.hashCode());
        // 如果注释掉该行,那么并不会实例化 MySample 对象,即 MySample 构造不会被调用。
        // 因此不会实例化 MyCat 对象,即没有对 MyCat 进行主动使用。
        Object obj = clazz.newInstance();

    }
}

# MySample
public class MySample {
    public MySample() {
        System.out.println("MySample is loaded by : " + this.getClass().getClassLoader());
        new MyCat();
    }
}

# MyCat
public class MyCat {
    public MyCat() {
        System.out.println("MyCat is loaded by : " + this.getClass().getClassLoader());
    }
    
}

a) 直接运行(classPath 下的 MySample.class、MyCat.class 并没有删除)

# 控制台
class : 1360875712
MySample is loaded by : sun.misc.Launcher$AppClassLoader@18b4aac2
MyCat is loaded by : sun.misc.Launcher$AppClassLoader@18b4aac2

b)将 classPath 下的 MySample.class、MyCat.class 删除后("/Users/linling/Documents/code/shengsisyuan/“目录下存在所需的所有字节码文件):

# 控制台
findClass invoked: com.bayern.shengsiyuan.jvm_lecture.lesson7.MySample
class loader name : loader1
class : 1580066828
MySample is loaded by : [loader1]
findClass invoked: com.bayern.shengsiyuan.jvm_lecture.lesson7.MyCat
class loader name : loader1
MyCat is loaded by : [loader1]

👆MySample.class 和 MyCat.class 都由 loader1 加载器加载了。

c)仅将 classPath 下的 MyCat.class 删除后("/Users/linling/Documents/code/shengsisyuan/“目录下存在所需的所有字节码文件):

# 控制台

class : 1360875712
MySample is loaded by : sun.misc.Launcher$AppClassLoader@18b4aac2
Exception in thread "main" java.lang.NoClassDefFoundError: com/bayern/shengsiyuan/jvm_lecture/lesson7/MyCat
    at com.bayern.shengsiyuan.jvm_lecture.lesson7.MySample.<init>(MySample.java:8)
    at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
    at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
    at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
    at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
    at java.lang.Class.newInstance(Class.java:442)
    at com.bayern.shengsiyuan.jvm_lecture.lesson7.MyTest17_1.main(MyTest17_1.java:15)
Caused by: java.lang.ClassNotFoundException: com.bayern.shengsiyuan.jvm_lecture.lesson7.MyCat
    at [java.net.URLClassLoader.findClass(URLClassLoader.java:381](http://java.net.urlclassloader.findclass(urlclassloader.java:381/))
    at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
    at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:335)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
    ... 7 more

因为,此时 MySample 是由 应用类加载器 加载的,而应用类加载器(及其父类加载器)无法加载 MyCat。

d)修改:MyCat 中添加对MySample Class 对象的引用。同时仅将 classPath 下的 MySample.class 删除("/Users/linling/Documents/code/shengsisyuan/“目录下存在所需的所有字节码文件):

# MyCat
public class MyCat {
    public MyCat() {
        System.out.println("MyCat is loaded by : " + this.getClass().getClassLoader());
        System.out.println("from MyCat : " + MySample.class);
    }
}

# 控制台
findClass invoked: com.bayern.shengsiyuan.jvm_lecture.lesson7.MySample
class loader name : loader1
class : 1580066828
MySample is loaded by : [loader1]
MyCat is loaded by : sun.misc.Launcher$AppClassLoader@18b4aac2
Exception in thread "main" java.lang.NoClassDefFoundError: com/bayern/shengsiyuan/jvm_lecture/lesson7/MySample
    at com.bayern.shengsiyuan.jvm_lecture.lesson7.MyCat.<init>(MyCat.java:8)
    at com.bayern.shengsiyuan.jvm_lecture.lesson7.MySample.<init>(MySample.java:8)
    at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
    at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
    at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
    at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
    at java.lang.Class.newInstance(Class.java:442)
    at com.bayern.shengsiyuan.jvm_lecture.lesson7.MyTest17_1.main(MyTest17_1.java:15)
Caused by: java.lang.ClassNotFoundException: com.bayern.shengsiyuan.jvm_lecture.lesson7.MySample
    at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
    at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:335)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
    ... 8 more

MyCat 和 MySample 是由不同的类加载器加载的。MyCat 与 MySample的命名空间不同,且 MyCat 的命名空间层级高于MySample 的命名空间。
MyCat 是由系统类类加载器加载的,它是看不到子加载器(自定义加载器loader1)所加载的类的!

  • 命名空间经典示例
    将 classPath 下 MyPerson.class 文件删除
# MyPerson
public class MyPerson {
    private MyPerson myPerson;
    public void setMyPerson(MyPerson object) {
        this.myPerson = object;
    }
}

# MyTest21
public class MyTest21 {
    public static void main(String[] args) throws Exception {
        MyTest16 loader1 = new MyTest16("loader1");
        MyTest16 loader2 = new MyTest16("loader2");
        loader1.setPath("/Users/linling/Documents/code/shengsisyuan/");
        loader2.setPath("/Users/linling/Documents/code/shengsisyuan/");
        Class<?> clazz1 = loader1.loadClass("com.bayern.shengsiyuan.jvm_lecture.lesson7.MyPerson");
        Class<?> clazz2 = loader2.loadClass("com.bayern.shengsiyuan.jvm_lecture.lesson7.MyPerson");
        System.out.println(clazz1 == clazz2);
        Object object1 = clazz1.newInstance();
        Object object2 = clazz2.newInstance();
        Method method = clazz1.getMethod("setMyPerson", MyPerson.class);
        method.invoke(object1, object2);
    }
}

# 控制台
findClass invoked: com.bayern.shengsiyuan.jvm_lecture.lesson7.MyPerson
class loader name : loader1
findClass invoked: com.bayern.shengsiyuan.jvm_lecture.lesson7.MyPerson
class loader name : loader2
false
Exception in thread "main" java.lang.NoClassDefFoundError: com/bayern/shengsiyuan/jvm_lecture/lesson7/MyPerson
    at com.bayern.shengsiyuan.jvm_lecture.lesson8.MyTest21.main(MyTest21.java:25)
Caused by: java.lang.ClassNotFoundException: com.bayern.shengsiyuan.jvm_lecture.lesson7.MyPerson
    at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
    at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:335)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
    ... 1 more

👆类型转换异常!!!并且,clazz1 != clazz2
此处,clazz1 和 clazz2 分别由各自的自定义类加载器加载的。因此,clazz1 和 clazz2 处于不同的命名空间中。
每个类加载器都有自己的命名空间。命名空间由该加载器及所有父类加载器所加载的类组成。
在不同的命名空间中,有可能会出现类的完整名字(包括类的包名)相同的两个类。

Q:这里为什么 setMyPerson 方法(MyPerson#setMyPerson(Object object))的参数类型是 Object 了?可以写成 MyPerson 吗?
A:不能写成 MyPerson 类型。因为写成 MyPerson 类型的话,👆示例(MyTest21)中的

Method method = clazz1.getMethod("setMyPerson", Object.class);
将写成:
Method method = clazz1.getMethod("setMyPerson", MyPerson.class);

而,我们已经将 MyPerson.class 从classPath中删除了,因此,程序走到“Method method = clazz1.getMethod("setMyPerson", MyPerson.class);”就抛异常了,而不是在“invoke”的时候。因为,MyPerson.class 对 MyTest21.class 不可见。MyPerson.class 由“自定义类加载器”加载的;而 MyTest21.class 由“应用类加载器”加载的。

破坏双亲委派模型
双亲委派模型的第一次“被破坏”:发生在双亲委派模型出现之前

👆双亲委派的具体逻辑就实现在『ClassLoader#loadClass()』这个方法之中,JDK 1.2之后已不提倡用户再去覆盖loadClass()方法,而应当把自己的类加载逻辑写到findClass()方法中,在loadClass()方法的逻辑里如果父类加载失败,则会调用自己的findClass()方法来完成加载,这样就可以保证新写出来的类加载器是符合双亲委派规则的。

双亲委派模型的第二次“被破坏”:是由这个模型自身的缺陷所导致的。

越基础的类由越上层的加载器进行加载,如果基础类又要调用回用户的代码,那该怎么办?
解决方案:使用“线程上下文类加载器”

为了解决这个问题,Java设计团队只好引入了一个不太优雅的设计:线程上下文类加载器(Thread Context ClassLoader)。这个类加载器可以通过java.lang.Thread类的setContextClassLoaser()方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器。

有了线程上下文类加载器,也就是父类加载器请求子类加载器去完成类加载的动作(即,父类加载器加载的类,使用线程上下文加载器去加载其无法加载的类),这种行为实际上就是打通了双亲委派模型的层次结构来逆向使用类加载器,实际上已经违背了双亲委派模型的一般性原则。

Java中所有涉及SPI的加载动作基本上都采用这种方式,例如JNDI、JDBC、JCE、JAXB和JBI等。

原因“线程上下文类加载器”详见深入探索“线程上下文类加载器”

双亲委派模型的第三次“被破坏”:是由于用户对程序动态性的追求而导致的。如,代码热替换(HotSwap)、模块热部署(Hot Deployment)等。

OSGi实现模块化热部署的关键则是它自定义的类加载器机制的实现。每一个程序模块(OSGi中称为Bundle)都有一个自己的类加载器,当需要更换一个Bundle时,就把Bundle连同类加载器一起换掉以实现代码的热替换。

在OSGi环境下,类加载器不再是双亲委派模型中的树状结构,而是进一步发展为更加复杂的网状结构

相关文章

深入探索“线程上下文类加载器”
从 sun.misc.Launcher 类源码深入探索 ClassLoader
ClassLoader 源码详解

参考

圣思园《深入理解JVM》

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容