JVM--类加载机制(超细节)

1. 类加载

       类加载机制指虚拟机把描述类的数据从Class文件加载到内存中,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型。

       在Java代码中,类型的加载、连接与初始化过程都是在程序运行期间完成的。

       在运行期间的好处是更加灵活,增加了更多的可能性。如反射、动态代理等。

1.1 类的生命周期

       类的生命周期包括:加载、验证、准备、解析(Resolution)、初始化、使用和卸载 7 个阶段。
在这里插入图片描述

1.2 类加载流程

在这里插入图片描述

1.3 类的加载、连接与初始化

  • 加载:查找并加载类的二进制数据(即Class文件)
  • 连接:
    • 验证:确保被加载的类的正确性
    • 准备:为类的静态变量分配内存,并将其初始化为默认值
    • 解析:把类中的符号引用转换为直接引用
  • 初始化:为类的静态变量赋予正确的初始值

       注意:并不意味着执行类的加载就一定会进行连接、初始化。

1.3.1 加载

       类的加载指的是将类的Class文件中读入到内存中,将其放在运行时数据区的方法区内,然后在内存中创建一个java.lang.Class对象。

       类加载的时机

       Java虚拟机规范中并没有进行强制约束,这点可以交给虚拟机的具体实现来自由把握。

1.3.2 连接

1.3.2.1 验证

       对Class文件的内容进行验证,需要其符合Java的语法和要求。

  1. 文件格式验证
  2. 元数据验证
  3. 字节码验证
  4. 符号引用验证
1.3.2.2 准备

       准备阶段为类变量在方法区中分配内存并初始化。注意这里只是对类变量,不包含实例变量。对类变量的初始化是指初始化为类型默认值

       如果类变量为编译期常量,那么JVM在准备阶段便可以直接赋值。

1.3.2.3 解析

       解析阶段指虚拟机将常量池内的符号引用替换为直接引用的过程,这样我们便可以直接通过对象或类调用其成员。

       符号引用以一组符号来描述所引用的目标,可以是任何形式的字面量。

       直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。

1.3.3 初始化

       类初始化阶段是类加载过程的最后一步,初始化阶段,才真正开始执行类中定义的Java程序代码.

	public static int a = 1;

       在准备阶段a被复制为0,在初始化阶段,a被赋值为1,所以初始化阶段才算真正执行按我们意愿所进行的初始化操作.

1.3.3.1 类的初始化时机

       Java虚拟机对于类初始化的时机进行指定了严格的规定。所有的Java虚拟机实现必须在每个类或接口被Java程序“首次主动使用”时才初始化他们。

       Java程序对类的使用分为两种:

  • 主动使用
  • 被动使用
1.3.3.1.1 主动使用
  1. 创建类的实例
  2. 访问某个类或接口的静态变量,或者对该静态变量赋值(被final修饰的编译器常量除外)
  3. 调用类的静态方法
  4. 初始化一个类的子类
  5. 反射
  6. 具有main方法的类
  7. JDK1.7开始提供的对动态语言的支持,如果一个java.lang.invoke.MethodHandle实例的解析结果REF_getStaticREF_putStaticREF_invokeStaticREF_newInvokeSpecial四种类型的方法句柄,并且句柄对应的类没有初始化,则初始化。
  8. 当一个接口具有默认方法(即被default注解修饰),那么其自其实例类被初始化之前初始化。
1.3.3.1.2 被动使用

       除了上述7种情况下,其他使用Java类的方式都被看作是对类的被动使用(可能会加载、连接类也不可能不会),都不会导致类的初始化。

示例一
	class MyParent1{
	
	    public static String str = "hello world";
	
	    static {
	        System.out.println("MyParent1 static block");
	    }
	}


	class MyChild1 extends MyParent1{
	
	    public static String str2 = "welcome";
	    static {
	        System.out.println("MyChild1 static block");
	    }
	}
	public class MyTest1 {
	    public static void main(String[] args) {
	        System.out.println(MyChild1.str);   
	    }
	}

       输出:

   MyParent1 static block
   hello world

       对于静态字段来说,只有直接定义了该字段的类才会被初始化,不管是谁调用,都只会导致该字段所在的类被初始化。 为什么MyChild1没有被初始化呢?这是因为对MyChild1的调用属于被动调用。

示例二
public class MyTest4 {
    public static void main(String[] args) {
        MyParent4[] myParent4s = new MyParent4[5];
        System.out.println(myParent4s.getClass());
        // class [Lcom.wangzhao.jvm.classloader.MyParent4;

        MyParent4[][] myParent4s2 = new MyParent4[5][1];
        System.out.println(myParent4s2.getClass());
        // class [[Lcom.wangzhao.jvm.classloader.MyParent4;

        System.out.println(myParent4s.getClass().getSuperclass());
        // class java.lang.Object

        System.out.println(myParent4s2.getClass().getSuperclass());
        // class java.lang.Object

        int[] array = new int[1];
        System.out.println(array.getClass());
        //  class [I
        System.out.println(array.getClass().getSuperclass());
        //  class java.lang.Object
    }
}

class MyParent4{

    static{
        System.out.println("MyParent4 static block");
    }

}

       通过输出可以看到并没有输出 MyParent4 static block,这是因为创建MyParent4类型的数组,并不属于对MyParent4的主动使用,在上述主动使用中并不包含这种情况.

        对于数组实例来说,其类型是由JVM在运行期动态生成的

1.3.3.1.3 类的初始化示例
示例一

       我们更改被动使用示例一的main方法,如下所示:

	public class MyTest1 {
	    public static void main(String[] args) {
	        System.out.println(MyChild1.str2);
	    }
	}

       输出:

   MyParent1 static block
   MyChild1 static block
   welcome

       当一个类在初始化时,要求其父类全部都已经初始化完毕了.

示例二
	public class MyTest5 {
	
	    public static void main(String[] args) {
	        System.out.println(MyChild5.b);
	    }
	
	}
	
	interface MyParent5{
	    public static Thread thread = new Thread(){
	        {
	            //实例化代码块
	            System.out.println("MyParent 5 invoked ");
	        }
	    };
	}
	
	interface MyChild5 extends MyParent5{
	    public static final int b = new Random().nextInt(4);
	}

       输出:

	0

        在初始化一个接口时,并不会先初始化他的父接口

示例三
	class MyParent2{
	    public static String str = "hello world";
	    static {
	        System.out.println("MyParent2 static block");
	    }
	}
	public class MyTest2 {
	
	    public static void main(String[] args) {
	
	          System.out.println(MyParent2.str);
	    }
	}

       想必你能很快的猜出答案,这属于对MyParent2的主动使用,所以输出:

   MyParent2 static block
   hello world

       但是如果我们将str改为常量会输出什么?

	class MyParent2{
	
	    public static final String str = "hello world";
	    static {
	        System.out.println("MyParent2 static block");
	    }
	}

       输出:

   hello world

       这里并没有输出 MyParent2 static block ,没道理啊,MyParent2.str属于对MyParent2的主动使用啊。

       这是因为常量在编译阶段会存入到调用这个常量的方法所在的类的常量池中,本质上,调用类并没有直接引用到定义常量的类,因此并不会触发定义常量的类的初始化

在这里插入图片描述
       可以明显看到,该Class文件并没有MyParent2的任何信息.

示例四
	public class MyTest3 {
	
	    public static void main(String[] args) {
	        System.out.println(MyParent3.str);
	    }
	}
	
	class MyParent3{
	    public static final String str = UUID.randomUUID().toString();
	
	    static{
	        System.out.println("MyParent3 static code");
	    }
	}

       输出:

	MyParent3 static code
	4ef5aa3a-6773-4678-858d-08e8e108d742

       当一个常量的值并非编译期间可以确定的,那么其值就不会被放到调用类的常量池中,这时在程序运行时,会导致主动使用这个常量所在的类,导致这个类被初始化

1.3.4 类加载器准备阶段和初始化阶段的意义

	public class MyTest6 {
	
	    public static void main(String[] args) {
	        Singleton.getInstance();
	        System.out.println(Singleton.counter1);
	        System.out.println(Singleton.counter2);
	    }
	
	}

	class Singleton{
	    public static int counter1;
	
	    public static int counter2 = 0;
	     
	    public static Singleton singleton = new Singleton();
	
	    private Singleton(){
	        counter1++;
	
	        counter2++;
	
	        System.out.println("静态代码块 counter1: " + counter1);
	        System.out.println("静态代码块 counter2: " + counter2);
	    }
	
	    public static Singleton getInstance(){
	        return singleton;
	    }
	}

        输出:

	静态代码块 counter1: 1
	静态代码块 counter2: 1
	1
	1

        这是因为当调用 getInstance()时,属于对Singleton类的主动使用,所以会进行初始化。

        在初始化前counter1counter2singleton的值分别在准备阶段被赋值为00null。然后开始至上向下初始化,先对counter1初始化,因为并没有进行赋值,所以使用默认值。然后对counter2,这里将0赋值给counter2。当初始化singleton时,调用构造方法,对初始化后的counter1counter2进行递增操作。

        如果我们更改counter2的顺序,输出会如何?

	class Singleton{
	    public static int counter1;
	
	    public static Singleton singleton = new Singleton();
	
	    private Singleton(){
	        counter1++;
	        
	        counter2++;
	
	        System.out.println("静态代码块 counter1: " + counter1);
	        System.out.println("静态代码块 counter2: " + counter2);
	    }
	
	    public static int counter2 = 0;
	
	    public static Singleton getInstance(){
	        return singleton;
	    }
	}

        输出:

	静态代码块 counter1: 1
	静态代码块 counter2: 1
	1
	0

        答案是不是有点出乎你的意料。同样的初始化阶段和之前一样,接着开始初始化阶段。counter1依然使用准备阶段的默认值,接着是对singleton的初始化(因为初始化是自上而下的)。进入其构造方法,首先执行counter1++操作(此时counter1使用的是初始化后的值),counter1 = 1;接着进行counter2++操作(注意这里使用的是counter2准备阶段的值),counter2 = 1

        注意构造方法结束并不意味着初始化阶段结束,因为counter2还没有被初始化。

        接着开始进行counter2的初始化,counter2由准备阶段的1变为0

        准备阶段的重要意义:如果类变量还未初始化,对类变量进行类似++的操作,没有默认值是不是会报错

2. 类加载器

       通过一个类的全限定名来获取描述此类的二进制字节流,以便让应用程序自己决定如何去获取所需要的类,实现这个动作的代码模块成为"类加载器"。

2.1 JDK自带类加载器的分类

2.1.1 启动类加载器(Bootstrap ClassLoader)

       这个类加载器负责将存在在<JAVA_HOME>\lib目录中的,或者被-Xbootclasspath参数所指定的路径,并且能够被虚拟机识别的类库(如rt.jar)加载到虚拟机内存中。

2.1.2 扩展类加载器(Extension ClassLoader)

       这个类加载器由sun.misc.Launcher$ExtClassLoader实现,负责加载<JAVA_HOME>\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库。

2.1.3 应用程序类加载器(Application ClassLoader)

       这个类加载器由 sun.misc.Launcher$AppClassLoader实现,负责加载用户类路径(ClassPath) 上所指定的类库,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是默认的类加载器。

2.2 双亲委托模型

       双亲委派模型可以理解为类加载时,类加载被调用顺序一种机制。

在这里插入图片描述
       上图所展示的类加载器之间的关系层次如上所示,该关系成为类加载器的双亲委派模型。需要注意的是,以上类加载器直接的关系并非继承。可观察继承图如下所示:

在这里插入图片描述
       可以明显看到APPClassLoader以及ExtClassLoader都是继承自URLClassLoader。同时我们也可以看到并没有BootstrapClassLoader,这是因为BootstrapClassLoader是由C++编写而成的,与Java的类并没有任何关系。同样的,我们也无法获取该类的实例。

       获取BootstrapClassLoader

	public static void main(String[] args) throws ClassNotFoundException {
        	Class<?> clazz = Class.forName("java.lang.String");
        	System.out.println(clazz);
        	System.out.println(clazz.getClassLoader());
    }

       输出如下所示

	class java.lang.String
	null

       可以观察到,如果我们获取BootstrapClassLoader时,返回的是null

       获取AppClassLoader

	class C{
		public static void main(String[] args) throws ClassNotFoundException {
	       	Class<?> c = Class.forName("com.wangzhao.jvm.classloader.C");
	        System.out.println(c);
	        System.out.println(c.getClassLoader());
	    }
	}

       输出如下所示

	class com.jvm.classloader.C
	sun.misc.Launcher$AppClassLoader@b4aac2	

       可以观察到,我们自定义类是使用AppClassLoader进行加载的,其属于Launcher类下的一个内部类。

2.2.1 工作流程

       如果一个类加载器收到了类加载的请求,它首先不会尝试加载这个类,而是将请求委托给其父类。直到没有父类为止,否则一直委托给父类。接着父类开始再其资源路径下尝试加载这个类,如果加载不到。那么回到子类去加载,如果最后的子类也不能加载到,那么程序抛出异常。

在这里插入图片描述
       类加载过程的代码如下

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) {
                        c = parent.loadClass(name, false);
                    } else {
                    	// 如果父加载器为null的话,则使用启动类加载器
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                	// 如果一直到启动类加载器,该类依然没有被加载
                	// 那么在该类加载器对应的路径下去寻找,如果找不到,再去子加载器的路径寻找
                    long t1 = System.nanoTime();
                    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(c);
            }
            return c;
        }
    }
}

       首先可以明显看出,类加载的过程与上面的时序图过程是相同的。

        其次,我们在书写单例模式的时候,有如下一种方式

public class Singleton {

    public static Singleton singleton = new Singleton();

    private Singleton(){}

    public static Singleton getInstance(){
        return singleton;
    }

}

       我们在书写单例模式的时候,都听过类加载的过程是线程安全的,但是不知道你有没有看过其源码,为什么是线程安全的?因为类加载的过程被synchronized所修饰。

2.2.2 为什么使用双亲委派模型

       类加载器的作用都是对Class文件进行加载,那么为什么不直接使用最底层那个类加载器去加载不就可以了吗?

       一个显而易见的好处是,Java类随着他的类加载器一起具备了一种带有优先级的层次关系

       以 java.lang.Object 为例,最终是由BootstrapClassLoader进行的加载,所以Object类在程序的各种类加载器都是同一个类。如果没有双亲委派机制的话,用户自己编写了一个称为java.lang.Object的类,那么程序中将会出现多个不同的Object类,程序将会很混乱。

        如下代码所示

	public class Object {
	
	}
	
	class ObjectClassLoaderTest{
	   public static void main(String[] args) throws ClassNotFoundException {
	       Class<?> clazz = Class.forName("java.lang.Object");
	       System.out.println(clazz);
	   }
	}

        程序运行后抛出异常信息如下所示:

Error: A JNI error has occurred, please check your installation and try again
Exception in thread "main" java.lang.SecurityException: Prohibited package name: java.lang

       双亲委派机制保证了一个类在程序中是由一个类加载器进行加载,且这种加载是具有优先级的

2.3 获取类加载器的方式

  • 获得当前类的类加载器
	clazz.getClassLoader()
  • 获得当前线程的上下文类加载器
	Thread.currentThread().getContextClassLoader()
  • 获得系统的ClassLoader
 	ClassLoader.getSystemClassLoader()

2.4 类加载器加载类与反射加载类的区别

public class MyTest8 {

    public static void main(String[] args) throws ClassNotFoundException {
        ClassLoader loader = ClassLoader.getSystemClassLoader();
        Class<?> clazz = loader.loadClass("com.wangzhao.jvm.classloader.CL");
        System.out.println(clazz);
        System.out.println("=============");
        clazz = Class.forName("com.wangzhao.jvm.classloader.CL");
        System.out.println(clazz);
    }

}

       输出

    class com.wangzhao.jvm.classloader.CL
    =============
    CL static block
    class com.wangzhao.jvm.classloader.CL

       通过上面的输出可以看到,类加载器加载类并不是对类的主动使用,所以不会初始化类

2.5 自定义类加载器

2.5.1 方式一

public class MyClassLoader extends ClassLoader{

    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        try {
            String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
            System.out.println(fileName);
            InputStream is = getClass().getResourceAsStream(fileName);
            if (is == null) {
                return super.loadClass(name);
            }
            byte[] b = new byte[is.available()];
            is.read(b);
            return defineClass(name, b, 0, b.length);
        } catch (IOException e) {
            throw new ClassNotFoundException();
        }
    }
}

2.5.2 方式二

public class MyTest11 extends ClassLoader {

    private String classLoaderName;

    private static final String FILE_EXTENSION = ".class";

    public MyTest11(String classLoaderName){
        super(); // 将系统类加载器设置为父加载器
        this.classLoaderName = classLoaderName;
    }

    public MyTest11(ClassLoader parent,String classLoaderName){
        super(parent); // 显式指定父加载器
        this.classLoaderName = classLoaderName;
    }

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

    }

    public byte[] loadClassData(String className) throws ClassNotFoundException {
        InputStream is = null;
        byte[] data = null;
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        try{
            String fileName = className.replace(".","/")+FILE_EXTENSION;
            is = new FileInputStream(new File(fileName));
            int len = 0;
            while((len = is.read())!=-1){
                baos.write(len);
            }
            data = baos.toByteArray();
            System.out.println(123);
            return data;
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                baos.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
            try {
                is.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return null;
    }

    public static void test(ClassLoader classLoader) throws Exception {
        System.out.println(classLoader.getParent());
        Class<?> clazz = classLoader.loadClass("com.wangzhao.jvm.classloader.MyTest1");
        Object obj = clazz.newInstance();
        System.out.println(obj);
    }

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

       这里我们谈一下这两种自定义有何区别,当使用第一种时,我们直接调用我们重写后的loadClass()方法。但是对于第二种来说,我们相当与重写了findClass(),这种情况下,该方法会在ClassLoaderloadClass()中调用。如果其父加载器可以进行类的加载,那么就不会执行我们重写后的findClass(),因为在前面说过,双亲委派模型并不是继承关系,这样的加载方式更符合双亲委派机制。

2.6 类是否相等

        例如有如下一段代码(前提,该项目类路径下并没有这个Class文件),那么这两个类加载器加载的这个类相等吗?

public static void main(String[] args) throws Exception {
        MyTest11 loader1 = new MyTest11("loader1");
        loader1.setPath("/home/wangzhao/Desktop/");
        Class<?> clazz = loader1.loadClass("com.wangzhao.jvm.classloader.MyTest1");
        System.out.println(clazz);
        System.out.println(clazz.hashCode());
        Object obj = clazz.newInstance();
        

        MyTest11 loader2 = new MyTest11("loader2");
        loader2.setPath("/home/wangzhao/Desktop/");
        Class<?> clazz2 = loader2.loadClass("com.wangzhao.jvm.classloader.MyTest1");
        System.out.println(clazz2);
        System.out.println(clazz2.hashCode());
        Object obj2 = clazz2.newInstance();
        System.out.println(obj2);
}

        输出如下

findClass
24324022
com.wangzhao.jvm.classloader.MyTest11@a14482
findClass
24079912
com.wangzhao.jvm.classloader.MyTest11@14ae5a5

       通过输出我们可以清楚看到这个类执行了我们自定义的findClass方法,也就是说类加载器是我们的自定义类。但是可以明显看到这两个类的hashCode不同,在类加载器不同的情况下,也就是说这两个类对象是不同的

       那么如果我们在项目的类路径下添加这个文件会输出什么呢?

10568834
sun.misc.Launcher$AppClassLoader@b4aac2
10568834
sun.misc.Launcher$AppClassLoader@b4aac2

       可以看到,如果类加载器是同一个的话,那么类对象是相等的

       定义如下类

public class MyPerson {

    private MyPerson myPerson;

    public void setMyPerson(Object obj){
        this.myPerson = (MyPerson)obj;
    }
}

       执行下面的代码

    public static void main(String[] args) throws Exception {
        MyTest11 loader1 = new MyTest11("loader1");
        MyTest11 loader2 = new MyTest11("loader2");

        Class<?> clazz1 =loader1.loadClass("com.wangzhao.jvm.classloader.MyPerson");
        Class<?> clazz2 = loader2.loadClass("com.wangzhao.jvm.classloader.MyPerson");

        System.out.println(clazz1 == clazz2);

        Object obj1 = clazz1.newInstance();
        Object obj2 = clazz2.newInstance();

        Method method = clazz1.getMethod("setMyPerson",Object.class);
        method.invoke(obj1,obj2);

}

       程序正常执行,这是因为clazz1clazz2都是由AppClassLoader加载。

       对于任意一个类,都需要由加载他的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。即比较两个类是否“相等”,只有在这两个类是由同一个类加载器实例 加载的前提下才有意义,否则,即使这两个类来源于同一个Class文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那么这两个类就必定不相等

2.7 命名空间

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

       同样的在类路径下没有该class文件的前提下,运行如下代码

public static void main(String[] args) throws Exception {
        MyTest11 loader1 = new MyTest11("loader1");
        loader1.setPath("/home/wangzhao/Desktop/");
        Class<?> clazz = loader1.loadClass("com.wangzhao.jvm.classloader.MyTest1");
        System.out.println(clazz.hashCode());
        Object obj = clazz.newInstance();
        System.out.println(obj.getClass().getClassLoader());


        MyTest11 loader2 = new MyTest11(loader1,"loader2");
        loader2.setPath("/home/wangzhao/Desktop/");
        Class<?> clazz2 = loader2.loadClass("com.wangzhao.jvm.classloader.MyTest1");
        System.out.println(clazz2.hashCode());
        Object obj2 = clazz2.newInstance();
        System.out.println(obj2.getClass().getClassLoader());
}

       输出结果如下

   findClass
   24324022
   com.wangzhao.jvm.classloader.MyTest11@a14482
   24324022
   com.wangzhao.jvm.classloader.MyTest11@a14482

       通过输出可以看到,loader2加载器并没有执行findClass(),即MyTest1被没有被loader2加载。这是因为loader2的父加载器loader1已经加载了MyTest1,通过双亲委托机制,当loader2加载MyTest1时,先让其父加载器加载。

2.7.1 类加载器命名空间示例

示例一

       存在下面这样一个类

public class MyPerson {

    private MyPerson myPerson;

    public void setMyPerson(Object obj){
        this.myPerson = (MyPerson)obj;
    }
}

       运行下面的代码,前提类路径下存在MyPerson.class文件

public class MyTest14 {

    public static void main(String[] args) throws Exception {
        MyTest11 loader1 = new MyTest11("loader1");
        MyTest11 loader2 = new MyTest11("loader2");	
	
		loader1.setPath("C:\\Users\\25519\\Desktop\\");
		loader2.setPath("C:\\Users\\25519\\Desktop\\");

        Class<?> clazz1 =loader1.loadClass("com.wangzhao.jvm.classloader.MyPerson");
        Class<?> clazz2 = loader2.loadClass("com.wangzhao.jvm.classloader.MyPerson");

        System.out.println(clazz1 == clazz2);

        Object obj1 = clazz1.newInstance();
        Object obj2 = clazz2.newInstance();

        Method method = clazz1.getMethod("setMyPerson",Object.class);
        method.invoke(obj1,obj2);

    }

       输出如下

	true

       如果我们将类路径下的MyPerson文件删除后,放到桌面后。继续运行上面的程序,输出如下

	findClass
	findClass
	false
	Exception in thread "main" java.lang.reflect.InvocationTargetException
	Caused by: java.lang.ClassCastException: com.wangzhao.jvm.classloader.MyPerson cannot be cast to com.wangzhao.jvm.classloader.MyPerson

       我们分析一下为什么会出现这种情况,首先我们没有删除类路径下得MyPerson.class文件时,MyTest11的父加载器可以在类路径下找到Class文件并加载,所以clazz1clazz2是同一个对象。

       而删除Class文件后,是由我们自定义的类加载器所加载,但是loader1loader2属于不同的实例,所以loader1loader2的命名空间不同,所以clazz1clazz2是不同的class对象。当对象实例属于不同得命名空间时,即使是由同一份Class文件加载,当进行类型转换时(非继承、实现关系)一定会出现类型转换异常

示例二

       存在如下这些类

public class MyCat {

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

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

       当我们执行如下的代码时

public class MyTest12 {

    public static void main(String[] args) throws Exception {

        MyTest11 loader1 = new MyTest11("loader1");
        Class<?> clazz = loader1.loadClass("com.wangzhao.jvm.classloader.MySample");
        System.out.println("class: " + clazz.hashCode());

        // 如果注释掉该行,那么并不会实例化MySample对象,即MySample构造方法不会被调用
        // 因此不会实例化MyCat对象,即没有对MyCat进行主动使用,这里就不会加载MyCat Class
        // 注意:没有对类进行主动使用,并不意味着一定不会加载Class文件
        Object obj = clazz.newInstance();
    }
}

       输出如下

   class: 21029277
   MySample loaded by : sun.misc.Launcher$AppClassLoader@b4aac2
   MyCat loaded by : sun.misc.Launcher$AppClassLoader@b4aac2

       这是因为loader1的父加载器AppCLassLoader在类路径下可以找到MySampleClass文件,所以MySample是由AppCLassLoader所加载,MyCat位于MySample的构造方法中,所以MyCat是由MySanple的类加载器或其父加载器去加载,因为AppClassLoader可以在类路径下找到MyCatClass文件,所以可以加载成功。

       将MyCat.class从项目路径下删除,放到桌面,保留MySample.class在类路径下。执行下面的代码

    public static void main(String[] args) throws Exception {

        MyTest11 loader1 = new MyTest11("loader1");
        loader1.setPath("/home/wangzhao/Desktop/");
        Class<?> clazz = loader1.loadClass("com.wangzhao.jvm.classloader.MySample");
        System.out.println("class: " + clazz.hashCode());

        Object obj = clazz.newInstance();
    }

       输出

   class: 21029277
   MySample loaded by : sun.misc.Launcher$AppClassLoader@b4aac2
   Exception in thread "main" java.lang.NoClassDefFoundError: com/wangzhao/jvm/classloader/MyCat

       通过输出可以看到,MyCat加载失败,这是因为MySampleAppClassLoader加载,所以MyCat也需要由AppClassLoader加载,但是类路径下没有MyCat.class,所以加载失败。

       将MySample.class从项目路径下删除,放到桌面,保留MyCat.class在类路径下,继续执行上面的代码,输出

   findClass
   class: 21685669
   MySample loaded by : com.wangzhao.jvm.classloader.MyTest11@140e19d
   MyCat loaded by : sun.misc.Launcher$AppClassLoader@b4aac2

       通过输出可以看到,当加载MySample时,是由我们自己定义的类加载器加载的。这是因为我们自定义的类加载器的父加载器不可以加载MySample,而我们自定义的类加载器可以加载,所以按照我们自定义类加载器的方式去加载。而加载MyCat时,根据双亲委派机制,先由loader1的父加载器去加载,而AppClassLoader可以加载到MyCat

        修改MyCat的代码,如下所示

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

        重新编译后,在类路径下保留MyCat.classMySample.class后,输出如下

	class: 21029277
	MySample loaded by : sun.misc.Launcher$AppClassLoader@b4aac2
	MyCat loaded by : sun.misc.Launcher$AppClassLoader@b4aac2
	from MyCat : class com.wangzhao.jvm.classloader.MySample

        如果将MySample.class从类路径中删除,放到桌面后,输出如下

	findClass
	class: 21685669
	MySample loaded by : com.wangzhao.jvm.classloader.MyTest11@140e19d
	MyCat loaded by : sun.misc.Launcher$AppClassLoader@b4aac2
	Exception in thread "main" java.lang.NoClassDefFoundError: com/wangzhao/jvm/classloader/MySample

       可以看到,获得MySample的class对象失败这是因为MySampleloader1加载,而MyCat是由其父加载器加载。在父加载器中看不到子加载器加载信息

       修改MySample和MyCat的代码如下所示

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

public class MySample {

    public MySample(){
        System.out.println("MySample loaded by : "+this.getClass().getClassLoader());

        // 由加载MySample的类加载器去加载MyCat
        new MyCat();

        System.out.println("from Sample : " + MyCat.class);
    }
}

       重新编译后运行,输出如下

	class: 21029277
	MySample loaded by : sun.misc.Launcher$AppClassLoader@b4aac2
	MyCat loaded by : sun.misc.Launcher$AppClassLoader@b4aac2
	from Sample : class com.wangzhao.jvm.classloader.MyCat

       将MySample从类路径下删除,放到桌面后,运行输出如下

	findClass
	class: 21685669
	MySample loaded by : com.wangzhao.jvm.classloader.MyTest11@140e19d
	MyCat loaded by : sun.misc.Launcher$AppClassLoader@b4aac2
	from Sample : class com.wangzhao.jvm.classloader.MyCat

       MySampleloader1加载,MyCatAppClassloader加载。当在loader1中获取MyCatclass对象时,子加载器可以访问到父加载器的加载信息,所以可以获取成功。

2.7.2 命名空间结论

  • 同一个命名空间内的类是相互可见的
  • 如果两个加载器之间没有直接或间接的父子关系,那么它们各自加载的类是互不可见的
  • 子加载器可以访问到被父加载器加载的类
  • 父加载器无法访问到被子加载器加载的类

2.8 类的卸载

       当某个类被加载、连接和初始化后,它的生命周期就开始了。当代表这个类的Class对象不再被引用,即不可触及时,Class对象就会结束生命周期,这个类在方法区内的数据也会被卸载,从而结束该类的生命周期。

       一个类何时结束生命周期,取决于代表它的Class对象何时结束生命周期

       由Java虚拟机自带的类加载器所加载的类,在虚拟机的生命周期中,始终不会被卸载Java虚拟机自带的类加载器包括根类加载器、扩展类加载器和系统类加载器。Java虚拟机自身会始终引用这些类加载器,而这些类加载器则会始终引用它们所加载的类的Class对象,因此这些Class对象始终是可触及的。

        由用户自定义的类加载器所加载的类是可以被卸载的

// -XX:+TraceClassUnloading输出有哪些类被卸载
public static void main(String[] args) throws IllegalAccessException, InstantiationException, ClassNotFoundException {
        MyTest11 loader1 = new MyTest11("loader1");
        loader1.setPath("/home/wangzhao/Desktop/");
        Class<?> clazz = loader1.loadClass("com.wangzhao.jvm.classloader.MyTest1");
        System.out.println(clazz.hashCode());
        Object obj = clazz.newInstance();
        System.out.println(obj.getClass().getClassLoader());

		// 去除引用,让垃圾回收器回收
        loader1 = null;
        clazz = null;
        obj = null;
        System.gc();

        loader1 = new MyTest11("loader1");
        loader1.setPath("/home/wangzhao/Desktop/");
        clazz = loader1.loadClass("com.wangzhao.jvm.classloader.MyTest1");
        System.out.println(clazz.hashCode());
        obj = clazz.newInstance();
        System.out.println(obj.getClass().getClassLoader());
    }

       输出如下

   findClass
   24324022
   com.wangzhao.jvm.classloader.MyTest11@a14482
   [Unloading class com.wangzhao.jvm.classloader.MyTest1 0xa43e4228]
   findClass
   24079912
   com.wangzhao.jvm.classloader.MyTest11@14ae5a5

       可以看到,findClass输出两次,也就是说Class对象被卸载了一次。

3. 补充说明

3.1 双亲委托模型的好处

  1. 确保Java核心库的类型安全

       所有的Java应用都至少引用java.lang.Object 类,也就是说在运行期,java.lang.Object这个类会被加载到 Java 虚拟机中;如果这个加载过程是由自定义的类加载器去完成的话,那么很可能就会在JVM中存在多个版本的java.lang.Object类,而且这些类之间还是不兼容的,相互之间不可见的(命名空间在发挥的作用)。

       借助于双亲委托机制,Java核心类库中的类的加载工作都是由启动类加载器来统一完成的,从而确保了Java应用程序所使用的都是同一个版本的Java核心类库,他们之间是相互兼容的。

  1. 不同的类加载器可以为相同名称的类创建额外的命名空间

       相同名称的类可以并存在Java虚拟机中,只需要用不同的类加载器来加载他们即可。不同类加载器所加载的类之间是不兼容的,这就相当于在Java虚拟机内部创建了一个又一个相互隔离的Java类空间,这类技术在很多框架中都得到了应用。

3.2 扩展类加载器要点分析

       执行下面的代码

public class MyTest15 {

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

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

}

       相信你能很快知道输出结果,输出如下

sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$AppClassLoader@18b4aac2

       在上文中说到过,我们可以通过更改对应加载器的加载路径,可以改变类加载器。那么,如果我们将拓展类加载器的加载路径,改为我们当前的目录,输出结果会是什么

在这里插入图片描述

       通过上面的输出可以看到,上面的两个类依然是由AppClassLoader加载。在上一篇文章中说过,如果扩展类加载器其类路径下的class文件,需要是类库的形式。所以先进行下面的打包

在这里插入图片描述
       此时可以看到MyTest1是由扩展类加载器加载

3.3 启动类加载器分析

       在OracleHotspot实现中,系统属性sun.boot.class.path如果修改错了,则运行会出错,提示如下信息

在这里插入图片描述

       内建于JVM中的启动类加载器会加载java.lang.ClassLoader以及其他的Java平台类,当JVM启动时,一块特殊的机器码会运行,它会加载扩展类加载器与系统类加载器,这块特殊的机器码叫做启动类加载器(Bootstrap)。

       启动类加载器并不是Java类,而其他的加载器则是Java类,启动类加载器时特定于平台的机器指令,它负责开启整个加载过程。

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

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

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

    // 扩展类加载器与系统类加载器也是由启动类加载器所加载的
    System.out.println(Launcher.class.getClassLoader());

    System.out.println(ClassLoader.getSystemClassLoader());

}

       输出如下

null
null
sun.misc.Launcher$AppClassLoader@18b4aac2

       可以看到ClasssLoader以及Launcher的类加载器都是启动类加载器,因为扩展类加载器与系统类加载器属于Launcher内部类,我们不能直接访问,而Launcher的类加载会尝试加载其成员。同时可以看到系统类加载器默认是AppClassLoader

3.3 更改系统类加载器

       我们知道系统类加载器默认是AppClassLoader系统类加载器是默认的自定义加载器的父加载器

       运行下面的代码

public class MyTest16 {

    public static void main(String[] args) {
        System.out.println(System.getProperty("java.system.class.loader"));

	System.out.println(ClassLoader.getSystemClassLoader());

	System.out.println(new MyTest11("loader").getParent());

    }
}

        输出如下

null
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$AppClassLoader@18b4aac2

在这里插入图片描述

       将系统类加载器改为我们自己定义的加载器出错,这是因为我们需要在自定义类加载器中添加一个ClassLoader的构造器,如下所示:

public MyTest11(ClassLoader parent){
    super(parent);
}

在这里插入图片描述

       可以看到自定义ClassLoader的类加载器的父加载器不再是AppClassLoader

       在我们更改默认的系统类加载器后,其是由AppClassLoader加载

3.4 Launcher源码分析

public static ClassLoader getSystemClassLoader() {
    initSystemClassLoader();
    if (scl == null) {
        return null;
    }
    SecurityManager sm = System.getSecurityManager();
    if (sm != null) {
        checkClassLoaderPermission(scl, Reflection.getCallerClass());
    }
    return scl;
}
// 用于确认系统类加载器有没有被赋值
private static boolean sclSet;

// 系统类加载器
private static ClassLoader scl;

private static synchronized void initSystemClassLoader() {
    if (!sclSet) {
        if (scl != null)
            // 如果没有被赋值,但是系统类加载器却不为空,不符合逻辑,所以出错
            throw new IllegalStateException("recursive invocation");
        // 返回一个Launcer的实例    
        sun.misc.Launcher l = sun.misc.Launcher.getLauncher();
        if (l != null) {
            Throwable oops = null;
            // 将AppClassLoader设置为系统类加载器
            scl = l.getClassLoader();
            try {
                // 系统类加载器需不需要进行改变
                scl = AccessController.doPrivileged(
                    new SystemClassLoaderAction(scl));
            } catch (PrivilegedActionException pae) {
                oops = pae.getCause();
                if (oops instanceof InvocationTargetException) {
                    oops = oops.getCause();
                }
            }
            if (oops != null) {
                if (oops instanceof Error) {
                    throw (Error) oops;
                } else {
                    // wrap the exception
                    throw new Error(oops);
                }
            }
        }
        // 表示系统类加载器设置完毕
        sclSet = true;
    }
}

       获取Launcher实例

public Launcher() { 
    // 扩展类加载器 
    Launcher.ExtClassLoader var1;
    try {
        var1 = Launcher.ExtClassLoader.getExtClassLoader();
    } catch (IOException var10) {
        throw new InternalError("Could not create extension class loader", var10);
    }

    try {
    	// 获取AppClassLoader时,将ExtClassLoader传入,这是为了将loader的父加载器设置为ExtClassLoader
        this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
    } catch (IOException var9) {
        throw new InternalError("Could not create application class loader", var9);
    }
    
    // 将刚创建好的应用类加载器设置为当前线程的上下文类加载器
    Thread.currentThread().setContextClassLoader(this.loader); 

    // 安全管理器相关设置
    String var2 = System.getProperty("java.security.manager");
    if (var2 != null) {
        SecurityManager var3 = null;
        if (!"".equals(var2) && !"default".equals(var2)) {
            try {
                var3 = (SecurityManager)this.loader.loadClass(var2).newInstance();
            } catch (IllegalAccessException var5) {
            } catch (InstantiationException var6) {
            } catch (ClassNotFoundException var7) {
            } catch (ClassCastException var8) {
            }
        } else {
            var3 = new SecurityManager();
        }

        if (var3 == null) {
            throw new InternalError("Could not create SecurityManager: " + var2);
        }

        System.setSecurityManager(var3);
    }

}

       获取ExtClassLoader 实例

static class ExtClassLoader extends URLClassLoader {
    public static Launcher.ExtClassLoader getExtClassLoader() throws IOException {
        final File[] var0 = getExtDirs();

        try {
            return (Launcher.ExtClassLoader)AccessController.doPrivileged(new PrivilegedExceptionAction<Launcher.ExtClassLoader>() {
                public Launcher.ExtClassLoader run() throws IOException {
                    int var1 = var0.length;

                    for(int var2 = 0; var2 < var1; ++var2) {
                        MetaIndex.registerDirectory(var0[var2]);
                    }

                    return new Launcher.ExtClassLoader(var0);
                }
            });
        } catch (PrivilegedActionException var2) {
            throw (IOException)var2.getException();
        }
    }

       获取系统的java.ext.dirs路径下的所有文件

private static File[] getExtDirs() {
    String var0 = System.getProperty("java.ext.dirs");
    File[] var1;
    if (var0 != null) {
        StringTokenizer var2 = new StringTokenizer(var0, File.pathSeparator);
        int var3 = var2.countTokens();
        var1 = new File[var3];

        for(int var4 = 0; var4 < var3; ++var4) {
            var1[var4] = new File(var2.nextToken());
        }
    } else {
        var1 = new File[0];
    }

    return var1;
}

       通过代码,我么便可以知道,为什么扩展类的加载路径是java.ext.dirs

       在Launcher的构造器中创建好之后,紧接着开始创建AppClassLoader。创建过程如下

static class AppClassLoader extends URLClassLoader {
    final URLClassPath ucp = SharedSecrets.getJavaNetAccess().getURLClassPath(this);

    public static ClassLoader getAppClassLoader(final ClassLoader var0) throws IOException {
        final String var1 = System.getProperty("java.class.path");
        final File[] var2 = var1 == null ? new File[0] : Launcher.getClassPath(var1);
        return (ClassLoader)AccessController.doPrivileged(new PrivilegedAction<Launcher.AppClassLoader>() {
            public Launcher.AppClassLoader run() {
                URL[] var1x = var1 == null ? new URL[0] : Launcher.pathToURLs(var2);
                return new Launcher.AppClassLoader(var1x, var0);
            }
        });
    }

       通过代码,可以看到AppClassLoader的加载路径同样读取java.class.path下的所有文件。

3.5 自定义系统类加载器源码分析

class SystemClassLoaderAction
    implements PrivilegedExceptionAction<ClassLoader> {
    private ClassLoader parent;

    SystemClassLoaderAction(ClassLoader parent) {
        this.parent = parent;
    }

    public ClassLoader run() throws Exception {
        String cls = System.getProperty("java.system.class.loader");
        if (cls == null) {
            // 如果我们没有设置过java.system.class.loader,那么系统类加载器为AppClassLoader
            return parent;
        }

		// 通过反射调用一个带ClassLoader参数的构造方法
		// 这就是为什么我们自定义类加载器设置为SystemClassLoader时,需要有一个ClassLoader参数的构造方法
        Constructor<?> ctor = Class.forName(cls, true, parent)
            .getDeclaredConstructor(new Class<?>[] { ClassLoader.class });
            
        // 系统类加载器的父加载器设置为parent
        ClassLoader sys = (ClassLoader) ctor.newInstance(
            new Object[] { parent });
        // 将用户自定义的类加载器设置为上下文类加载器    
        Thread.currentThread().setContextClassLoader(sys);
        return sys;
    }
}

3.6 forName方法剖析

// name 全限定类名
// initialize 是否进行初始化
// loader 加载指定类的类加载器
public static Class<?> forName(String name, boolean initialize,
                               ClassLoader loader)
    throws ClassNotFoundException
{
    Class<?> caller = null;
    SecurityManager sm = System.getSecurityManager();
    if (sm != null) {
        // 获取调用forName()方法的类的class对象        
        caller = Reflection.getCallerClass();
        if (sun.misc.VM.isSystemDomainLoader(loader)) {
            // 获取caller的classLoader对象
            ClassLoader ccl = ClassLoader.getClassLoader(caller);
            if (!sun.misc.VM.isSystemDomainLoader(ccl)) {
                sm.checkPermission(
                    SecurityConstants.GET_CLASSLOADER_PERMISSION);
            }
        }
    }
    return forName0(name, initialize, loader, caller);
}

       该方法的作用是返回使用给定的类加载器加载给定字符串名称的类或接口的Class对象。

       Class.forName(“Foo”) = Class.forName(“Foo”, true, this.getClass().getClassLoader())

3.7 线程上下文类加载器

public class MyTest17 {

    public static void main(String[] args) {
        System.out.println(Thread.currentThread().getContextClassLoader());
        // sun.misc.Launcher$AppClassLoader@18b4aac2
        System.out.println(Thread.class.getClassLoader());
        // null
    }

}
  • 当前类加载器

       每个类都会使用自己的类加载器(即加载自身的类加载器)来去加载其他类(指的是所依赖的类),如果ClassX引用了ClassY,那么ClassX的类加载器就会去加载ClassY(前提是ClassY尚未被加载)。

  • 线程上下文类加载器

       线程上下文类加载器从JDK1.2开始引入的,类Thread中的getContextClassLoader()setContextClassLoader(ClassLoader cl)分别用来获取和设置上下文类加载器。

       如果没有通过setContextClassLoader(cl) 设置的话,线程将继承其父线程的上下文类加载器。

       Java应用运行时的初始化线程的上下文类加载器就是系统类加载器。在线程中运行的代码可以通过该类加载器来加载类于资源

       线程上下文类加载器的重要性:SPI(Service Provider Interface)

  • ClassLoader可以使用当前线程Thread.currentThread().getContextClassLoader()所指定的classloader加载的类。这就改变了父Classloader不能使用子ClassLoader或是其他没有直接父子关系的classLoader加载的类的情况。即改变了双亲委托模型。

  • 在双亲委托模型下,类加载器是由下至上的,即下层的类加载器会委托上层进行加载。但是对于SPI来说,有些接口时Java核心库所提供的,而Java核心库是由启动类加载器来加载的,而这些接口的实现却来自于不同的jar包(厂商提供),Java的启动类加载器是不会加载其他来源的jar包,这样传统的双亲委托模型就无法满足SPI的要求(这样导致接是启动类加载,而实现是由系统类加载器或应用类加载器加载)。而通过给当前线程设置上下文类加载器,就可以由设置的上下文类加载器来实现对于接口实现类的加载。

       以JDBC为例,JDBC的接口是启动类加载器加载,但是JDBC的接口的实现却是由数据库厂商所提供。因为JDBC的接口和其实现存在依赖关系,而其实现的jar包被我们放到了classpath,这样便导致了启动类加载器无法加载其实现,从而我们无法使用JDBC,所以传统的双亲委托机制便失效了。

public class MyTest18 implements Runnable{

    private Thread thread;

    public MyTest18(){
        thread = new Thread(this);
        thread.start();
    }

    @Override
    public void run() {
        ClassLoader classLoader = this.thread.getContextClassLoader();

        this.thread.setContextClassLoader(classLoader);

        System.out.println("Class: " + classLoader.getClass());
        System.out.println("Parent: " + classLoader.getParent().getClass());
    }

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

       该程序的输出通过前面的分析,想必很快能知道答案,输出如下

Class: class sun.misc.Launcher$AppClassLoader
Parent: class sun.misc.Launcher$ExtClassLoader

3.7.1 线程上下文类加载器的使用

       线程上下文类加载的一般使用模式 (获取 - 使用 - 还原

       伪代码如下

      try{
          // targetThreadContextClassLoader将要使用的ClassLoader设置为线程上下文类加载器
         Thread.currentThread().setContextClassLoader(targetThreadContextClassLoader);
          doSomething();
     }finally{
          Thread.currentThread().setClassLoader(classLoader);
     }

       doSomething()里面调用了Thread.currentThread.getContextClassLoader(),获取当前线程的上下文类加载器做某些事情。如果一个类由类加载器A加载,那么这个类的类加载器会加载这个类的依赖类(前提依赖类没有被加载)

       ContextClassLoader的作用是为了破坏Java的类加载器委托机制

       当高层提供了统一的接口让低层去实现,同时又要在高层加载(或实例化)低层的类时,就必须要通过线程上下文类加载器来帮助高层的ClassLoader找到并加载该类

3.7.2 SPI

       SPI

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值