JVM成神之路-类加载机制-双亲委派,破坏双亲委派

  • Post author:
  • Post category:其他



概述


概念

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


类的生命周期

类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载、验证、准备、解析、初始化、使用、卸载。其中验证、准备、解析统称为连接

上图中,加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的,类的加载过程必须严格按照这种顺序开始。

解析阶段则不一定,它在某些情况下,可以在初始化阶段之后再开始,这是为了支持Java语言的运行时绑定(动态绑定|晚期绑定)


类加载-时机


主动引用

Java虚拟机规范中并没有进行强制约束什么时候开始类加载过程的第一个阶段-加载,可以交给虚拟机具体实现来自由把握。但对于初始化阶段,虚拟机规范严格规定

有且只有

5种情况必须立即对类进行初始化(加载、验证、准备自然要在此之前开始)

  • 遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发初始化操作。

4条指令最常见Java代码场景:用new关键字实例化对象的时候、读取或设置一个类的静态字段(被final修饰、已在编译器把结果放入常量池的静态字段除外)的时候、调用一个类的静态方法的时候。

  • 用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要触发初始化操作。
  • 初始化一个类的时候,发现其父类还有进行过初始化,则需要触发先其父类的初始化操作。

注意这里和接口的初始化有点区别,,一个接口在初始化时,并不要求其父接口全部都完成了初始化,只要在真正使用到父接口的时候(如引用接口中定义的常量)才会初始化。

  • 虚拟机启动时,需要指定一个执行的主类(包含main方法的类),虚拟机会先初始化这类。
  • 用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化操作。


被动引用

以上5种场景均有一个必须的限定:“有且只有”,这5种场景中的行为称为对一个类进行主动引用。除此之外,所有引用类的方式都不会触发初始化,称为被动引用。


示例1

    /**
     * 通过子类引用父类的静态字段,不会导致子类初始化
     */
    public class SuperClass {
        static {
            System.out.println("SuperClass init....");
        }

        public static int value = 123;
    }

    package com.xdwang.demo;

    public class SubClass extends SuperClass {
        static {
            System.out.println("SubClass init....");
        }
    }

    package com.xdwang.demo;

    public class Test {
        public static void main(String[] args) {
            System.out.println(SubClass.value);
        }
    }

运行结果:

SuperClass init....
123

结论:

对于静态字段,只有直接定义这个字段的类才会被初始化,因此通过其子类来引用父类中定义的静态字段,只会触发父类的初始化而不会触发子类的初始化。(是否触发子类的加载和验证,取决于虚拟机具体的实现,对于HotSpot来说,可以通过-XX:+TraceClassLoading参数观察到此操作会导致子类的加载)


示例2

package com.xdwang.demo;

public class Test2 {
    public static void main(String[] args) {
        //
        SuperClass[] superClasses = new SubClass[10];
    }
}

无任何输出

结论:

通过数组定义来引用类,不会触发此类的初始化

这里其实会触发另一个类的初始化


示例3

    public class ConstClass {
        static {
            System.out.println("ConstClass init....");
        }

        public static final String MM = "hello Franco";
    }

    package com.xdwang.demo;

    public class Test3 {
        public static void main(String[] args) {
            System.out.println(ConstClass.MM);
        }
    }

运行结果:

hello Franco

并没有ConstClass init….,这是因为虽然Test3里引用了ConstClass类中的常量,但其实在编译阶段通过常量传播优化,已经将此常量存储到Test3类的常量池中。两个类在编译成class之后就不存在任何联系了。


类加载-过程


加载

加载阶段(可参考java.lang.ClassLoader的loadClass()方法),虚拟机要完成以下3件事情:

  1. 通过一个类的全限定名来获取定义此类的二进制字节流(并没有指明要从一个Class文件中获取,可以从其他渠道,譬如:网络、动态生成、数据库等);
  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构;
  3. 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口;

加载阶段和连接阶段(Linking)的部分内容(如一部分字节码文件格式验证动作)是交叉进行的,加载阶段尚未完成,连接阶段可能已经开始,但这些夹在加载阶段之中进行的动作,仍然属于连接阶段的内容,这两个阶段的开始时间仍然保持着固定的先后顺序。


验证

验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

验证阶段是非常重要的,这个阶段是否严谨,直接决定了Java虚拟机是否能承受恶意代码的工具,从执行性能的角度上讲,验证阶段的工作量在虚拟机的类加载子系统中又占了相当大一部分。

验证阶段大致会完成4个阶段的检验动作:

  1. 文件格式验证:验证字节流是否符合Class文件格式的规范,并且能够被当前版本的虚拟机处理

    • 是否以魔术0xCAFEBABE开头
    • 主次版本号是否在当前虚拟机的处理范围之内
    • 常量池中的常量是否有不被支持的类型。
    • ….
  2. 元数据验证:对字节码描述的信息进行语义分析(注意:对比javac编译阶段的语义分析),以保证其描述的信息符合Java语言规范的要求;

    • 这个类是否有父类。(除了java.lang.Object之外)
    • 这个类的父类是否集继承了不允许被继承的类(被final修饰的类)
    • 如果这个类不是抽象类,是否实现了其父类或接口中要求实现的所有方法
    • ….
  3. 字节码验证:整个验证过程最复杂的一个阶段。主要目的是通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。在第二阶段对元数据信息中的数据类型做完校验后,这个阶段将对类的方法体进行校验分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的事件

    • 保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作,例如不会出现类似在操作栈放int型数据,使用却按long行加载如本地变量表中。
    • 保证跳转指令不会跳转到方法体意外的字节码指令上
    • ….
  4. 符号引用验证:目的是确保解析动作能正常执行,发生在虚拟机将符号引用转换为直接引用的时候,这个转化动作将在连接的第三阶段-解析阶段中发生。符号引用验证可以看做是对类自身以外(常量池中的各种符号引用)的信息进行匹配性校验。

    • 符号引用中通过字符串描述的全限定名是否能够找到对应的类。
    • 在指定类中是否存在符号方法的字段描述符以及简单名称所描述的方法和字段
    • 符号引用中的类、字段、方法的访问性(private、protected、public、default)是否可被当前类访问。
    • ….

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


准备

准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。这时候进行内存分配的仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在堆中。其次,这里所说的初始值“通常情况”下是数据类型的零值,假设一个类变量的定义为:

public static int value=123;

那变量value在准备阶段过后的初始值为0而不是123.因为这时候尚未开始执行任何java方法,而把value赋值为123的putstatic指令是程序被编译后,存放于类构造器<clinit>()方法之中,所以把value赋值为123的动作将在初始化阶段才会执行。

至于“特殊情况”是指:

public static final int value=123

即当类字段的字段属性是ConstantValue时,会在准备阶段初始化为指定的值,所以标注为final之后,value的值在准备阶段初始化为123而非0.


解析

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


初始化

类初始化阶段是类加载过程的最后一步,到了初始化阶段,才真正开始执行类中定义的java程序代码。在准备阶段,变量已经赋过一次系统要求的初始值,而在初始化阶段,则根据程序猿通过程序制定的主观计划去初始化类变量和其他资源,或者说:初始化阶段是执行类构造器<clinit>()方法的过程。

  • <clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块static{}中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,

    静态语句块只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问

    。如下:
  • public class Test
    {
        static
        {
            i=0;//给变量赋值可以正常编译通过
            System.out.println(i);//这句编译器会报错:Cannot reference a field before it is defined(非法向前应用)
        }
        static int i=1;
    }

  • <clinit>()方法与实例构造器<init>()方法不同,它不需要显示地调用父类构造器,虚拟机会保证在子类<init>()方法执行之前,父类的<clinit>()方法已经执行完毕,一次虚拟机中第一个被执行的<clinit>()方法的类肯定是java.lang.Object。
  • 由于父类的<clinit>()方法先执行,也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作。(下面的例子,B=2)
static class Parent{
    public static int A=1;
    static{
        A=2;
    }
}
static class Sub extends Parent{
    public static int B=A;
}
public class Test{
    public static void main(String[] args){
        System.out.println(Sub.B);
    }
}
  • <clinit>()方法对于类或者接口来说并不是必需的,如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生产<clinit>()方法。
  • 接口中不能使用静态语句块,但仍然有变量初始化的赋值操作,因此接口与类一样都会生成<clinit>()方法。但接口与类不同的是,执行接口的<clinit>()方法不需要先执行父接口的<clinit>()方法。只有当父接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类在初始化时也一样不会执行接口的<clinit>()方法。
  • 虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确的加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其他线程都需要阻塞等待,直到活动线程执行<clinit>()方法完毕。如果在一个类的<clinit>()方法中有好事很长的操作,就可能造成多个线程阻塞,在实际应用中这种阻塞往往是隐藏的。

public class DealLoopTest {
    static class DeadLoopClass {
        static {
            if (true)// 如果不加上这个if语句,编译器将提示“Initializer does not complete normally”错误
            {
                System.out.println(Thread.currentThread() + "init DeadLoopClass");
                while (true) {
                }
            }
        }
    }

    public static void main(String[] args) {
        Runnable script = new Runnable() {
            public void run() {
                System.out.println(Thread.currentThread() + " start");
                DeadLoopClass dlc = new DeadLoopClass();
                System.out.println(Thread.currentThread() + " run over");
            }
        };

        Thread thread1 = new Thread(script);
        Thread thread2 = new Thread(script);
        thread1.start();
        thread2.start();
    }
}

运行结果:(即一条线程在死循环以模拟长时间操作,另一条线程在阻塞等待)

Thread[Thread-1,5,main] start
Thread[Thread-0,5,main] start
Thread[Thread-1,5,main]init DeadLoopClass

需要注意的是,其他线程虽然会被阻塞,但如果执行<clinit>()方法的那条线程退出<clinit>()方法后,其他线程唤醒之后不会再次进入<clinit>()方法。同一个类加载器下,一个类型只会初始化一次。

将上面代码中的静态块替换如下:

static {
    System.out.println(Thread.currentThread() + "init DeadLoopClass");
    try {
        TimeUnit.SECONDS.sleep(10);
    }
    catch (InterruptedException e) {
        e.printStackTrace();
    }
}

运行结果:

Thread[Thread-0,5,main] start
Thread[Thread-1,5,main] start
Thread[Thread-0,5,main]init DeadLoopClass
Thread[Thread-0,5,main] run over
Thread[Thread-1,5,main] run over

原因在类加载-时机的主动引用中已经解释了。


类加载器(class loader)


概念

类加载器(class loader)用来加载 Java 类到 Java 虚拟机中。一般来说,Java 虚拟机使用 Java 类的方式如下:Java 源程序(.java 文件)在经过 Java 编译器编译之后就被转换成 Java 字节代码(.class 文件)。类加载器负责读取 Java 字节代码,并转换成 java.lang.Class类的一个实例。每个这样的实例用来表示一个 Java 类。通过此实例的 newInstance()方法就可以创建出该类的一个对象。

类加载器应用在很多方面,比如类层次划分、OSGi、热部署、代码加密等领域。

基本上所有的类加载器都是 java.lang.ClassLoader类的一个实例


java.lang.ClassLoader



java.lang.ClassLoader类的基本职责就是根据一个指定的类的名称,找到或者生成其对应的字节代码,然后从这些字节代码中定义出一个 Java 类,即 java.lang.Class类的一个实例。除此之外,ClassLoader还负责加载 Java 应用所需的资源,如图像文件和配置文件等。

为了完成加载类的这个职责,ClassLoader提供了一系列的方法


方法


说明

getParent()

返回该类加载器的父类加载器。

loadClass(String name)

加载名称为name的类,返回的结果是java.lang.Class类的实例。

findClass(String name)

查找名称为name的类,返回的结果是java.lang.Class类的实例。

findLoadedClass(String name)

查找名称为name的已经被加载过的类,返回的结果是java.lang.Class类的实例。

defineClass(String name, byte[] b, int off, int len)

把字节数组 b中的内容转换成 Java 类,返回的结果是 java.lang.Class类的实例。这个方法被声明为final的。

resolveClass(Class c)

链接指定的 Java 类。


类与类加载器

类加载器虽然只用于实现类的加载动作,但它在java程序中起到作用却远远不限于类加载阶段。对于任意一个类,都需要由

加载它的类加载器和这个类本身一起确立其在Java虚拟机中的唯一性

,每一个类加载器,都拥有一个独立的类名称空间。(比较两个类是否相等,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则即使这两个类来源于同一个Class文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类肯定不会相等)

这里说的相等,包括代表类的Class对象的equals()方法、isAssignableFrom()方法、isInstance()方法的返回结果,也包括使用instanceof关键字做对象所属关系判定等情况。


双亲委派模型


类加载器分类

在虚拟机的角度上,只存在两种不同的类加载器:

  • 启动类加载器(Bootstrap ClassLoader),这个类加载器使用C++语言实现,是虚拟机自身的一部分;
  • 其它所有的类加载器,这些类加载器都由Java语言实现,独立于虚拟机外部,并且全部继承自java.lang.ClassLoader

从Java开发人员的角度看,类加载器还可以划分得更细一些,如下:

  • 启动类加载器(Bootstrap ClassLoader)

这个类加载器负责将放置在<JAVA_HOME>\lib目录中的,或者被-Xbootclasspath参数所指定路径中的,并且是虚拟机能识别的(仅按照文件名识别,如rt.jar,名字不符合的类库即使放置在lib目录中也不会被加载)类库加载到虚拟机内存中。启动类加载器无法被Java程序直接使用。程序员在编写自定义类加载器时,如果需要把加载请求委派给引导类加载器,直接使用null代替即可。

  • 扩展类加载器(Extension ClassLoader)

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

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

这个类加载器由sum.misc.Launcher.$AppClassLoader来实现。由于这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般也被称为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

应用程序由这三种类加载器互相配合进行加载的,如果有必须,还可以加入自己定义的类加载器。这些类加载器之间的关系一般如下图

https://img-blog.csdnimg.cn/20181224103307101


双亲委派模型概念

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

类加载器的双亲委派模型在JDK1.2期间被引入并广泛用于之后几乎所有的Java程序中,但它并不是一个强制性的约束模型,而是Java设计者推荐给开发者的一种类加载实现方式。

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


双亲委派模型优点

Java类随着它的类加载器一起具备了一种带有优先级的层次关系,例如类java.lang.Object,它存在在rt.jar中,无论哪一个类加载器要加载这个类,最终都会委派给出于模型最顶端的启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。相反,如果没有使用双亲委派模型,由各个类加载器自行去加载的话,如果用户自己编写了一个称为java.lang.Object的类(该类具有系统的Object类一样的功能,只是在某个函数稍作修改。比如equals函数,这个函数经常使用,如果在这这个函数中,黑客加入一些“病毒代码”。并且通过自定义类加载器加入到JVM中,哈哈,那就热闹了),并放在程序的ClassPath中,那系统中将会出现多个不同的Object类,java类型体系中最基础的行为也就无法保证了,应用程序也将变得一片混乱。


双亲委派模型实现

双亲委派模型对于保证Java程序的稳定运作很重要,但它的实现却非常简单,实现代码都集中在ClassLoader类默认的loadClass方法中。

loadClass默认实现如下:

public Class<?> loadClass(String name) throws ClassNotFoundException {
        return loadClass(name, false);
}

再看看loadClass(String name, boolean resolve)函数:

protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        // 1、检查请求的类是否已经被加载过了
        Class c = findLoadedClass(name);
        if (c == null) {
            try {
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // 如果父类加载器抛出ClassNotFoundException,说明父类加载器无法完成加载请求
            }
            if (c == null) {
                // 在父类加载器无法加载的时候,再调用本身的findClass方法来进行类加载
                c = findClass(name);
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}
  1. 检查一下指定名称的类是否已经加载过,如果加载过了,就不需要再加载,直接返回。
  2. 如果此类没有加载过,那么,再判断一下是否有父加载器;如果有父加载器,则由父加载器加载(即调用parent.loadClass(name, false);).或者是调用bootstrap类加载器来加载。
  3. 如果父加载器及bootstrap类加载器都没有找到指定的类,那么调用当前类加载器的findClass方法来完成类加载。

换句话说,如果自定义类加载器,就必须重写findClass方法!

findClass的默认实现如下:

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

可以看出,抽象类ClassLoader的findClass函数默认是抛出异常的。而前面我们知道,loadClass在父加载器无法加载类的时候,就会调用我们自定义的类加载器中的findeClass函数,因此我们必须要在loadClass这个函数里面实现将一个指定类名称转换为Class对象.

如果是读取一个指定的名称的类为字节数组的话,这很好办。但是如何将字节数组转为Class对象呢?很简单,Java提供了defineClass方法,通过这个方法,就可以把一个字节数组转为Class对象啦~

defineClass主要的功能是:

将一个字节数组转为Class对象,这个字节数组是class文件读取后最终的字节数组。如,假设class文件是加密过的,则需要解密后作为形参传入defineClass函数。

defineClass默认实现如下:

protected final Class<?> defineClass(String name, byte[] b, int off, int len)
        throws ClassFormatError  {
        return defineClass(name, b, off, len, null);
}

函数调用过程:

https://img-blog.csdnimg.cn/20181224103307123


示例

首先,我们定义一个待加载的普通Java类:Test.java。放在com.xdwang.demo包下:

package com.xdwang.demo;

public class Test {
    public void hello() {
        System.out.println("恩,是的,我是由 " + getClass().getClassLoader().getClass() + " 加载进来的");
    }
}

如果你是直接在当前项目里面创建,待Test.java编译后,请把Test.class文件拷贝走,再将Test.java删除。因为如果Test.class存放在当前项目中,根据双亲委派模型可知,会通过sun.misc.Launcher$AppClassLoader 类加载器加载。为了让我们自定义的类加载器加载,我们把Test.class文件放入到其他目录。

接下来就是自定义我们的类加载器:

import java.io.FileInputStream;
import java.lang.reflect.Method;

public class Main {
    static class MyClassLoader extends ClassLoader {
        private String classPath;
        public MyClassLoader(String classPath) {
            this.classPath = classPath;
        }
        private byte[] loadByte(String name) throws Exception {
            name = name.replaceAll("\\.", "/");
            FileInputStream fis = new FileInputStream(classPath + "/" + name
                    + ".class");
            int len = fis.available();
            byte[] data = new byte[len];
            fis.read(data);
            fis.close();
            return data;
        }

        protected Class<?> findClass(String name) throws ClassNotFoundException {
            try {
                byte[] data = loadByte(name);
                return defineClass(name, data, 0, data.length);
            } catch (Exception e) {
                e.printStackTrace();
                throw new ClassNotFoundException();
            }
        }

    };

    public static void main(String args[]) throws Exception {
        MyClassLoader classLoader = new MyClassLoader("D:/test");
        //Test.class目录在D:/test/com/xdwang/demo下
        Class clazz = classLoader.loadClass("com.xdwang.demo.Test");
        Object obj = clazz.newInstance();
        Method helloMethod = clazz.getDeclaredMethod("hello", null);
        helloMethod.invoke(obj, null);
    }
}

运行结果:

恩,是的,我是由 class Main$MyClassLoader 加载进来的


破坏双亲委派模型

上面提到过双亲委派模型并不是一个强制性的约束模型,而是java设计者推荐给开发者的类加载器实现方式,在java的世界中大部分的类加载器都遵循这个模型,但也有例外,到目前为止,双亲委派模型主要出现过三次较大规模的“被破坏”情况。

  • 双亲委派模型的第一次“被破坏”其实发生在双亲委派模型出现之前–即JDK1.2发布之前。由于双亲委派模型是在JDK1.2之后才被引入的,而类加载器和抽象类java.lang.ClassLoader则是JDK1.0时候就已经存在,面对已经存在的用户自定义类加载器的实现代码,Java设计者引入双亲委派模型时不得不做出一些妥协。为了向前兼容,JDK1.2之后的java.lang.ClassLoader添加了一个新的proceted方法findClass(),在此之前,用户去继承java.lang.ClassLoader的唯一目的就是重写loadClass()方法,因为虚拟在进行类加载的时候会调用加载器的私有方法loadClassInternal(),而这个方法的唯一逻辑就是去调用自己的loadClass()。JDK1.2之后已不再提倡用户再去覆盖loadClass()方法,应当把自己的类加载逻辑写到findClass()方法中,在loadClass()方法的逻辑里,如果父类加载器加载失败,则会调用自己的findClass()方法来完成加载,这样就可以保证新写出来的类加载器是符合双亲委派模型的。
  • 双亲委派模型的第二次“被破坏”是这个模型自身的缺陷所导致的,双亲委派模型很好地解决了各个类加载器的基础类统一问题(越基础的类由越上层的加载器进行加载),基础类之所以被称为“基础”,是因为它们总是作为被调用代码调用的API。但是,如果基础类又要调用用户的代码,那该怎么办呢?

    这并非是不可能的事情,一个典型的例子便是JNDI服务,JNDI现在已经是Java的标准服务,它的代码由启动类加载器去加载(在JDK1.3时放进rt.jar),但JNDI的目的就是对资源进行集中管理和查找,它需要调用独立厂商实现部部署在应用程序的classpath下的JNDI接口提供者(SPI, Service Provider Interface)的代码,但启动类加载器不可能“认识”之些代码,该怎么办?

    为了解决这个困境,Java设计团队只好引入了一个不太优雅的设计:线程上下文件类加载器(Thread Context ClassLoader)。这个类加载器可以通过java.lang.Thread类的setContextClassLoader()方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个;如果在应用程序的全局范围内都没有设置过,那么这个类加载器默认就是应用程序类加载器。有了线程上下文类加载器,JNDI服务使用这个线程上下文类加载器去加载所需要的SPI代码,也就是父类加载器请求子类加载器去完成类加载动作,这种行为实际上就是打通了双亲委派模型的层次结构来逆向使用类加载器,已经违背了双亲委派模型,但这也是无可奈何的事情。Java中所有涉及SPI的加载动作基本上都采用这种方式,例如JNDI,JDBC,JCE,JAXB和JBI等。
  • 双亲委派模型的第三次“被破坏”是由于用户对程序的动态性的追求导致的,例如OSGi的出现。在OSGi环境下,类加载器不再是双亲委派模型中的树状结构,而是进一步发展为网状结构。

Class.forName()和ClassLoader.loadClass()的区别

Class.forName(className)方法,内部实际调用的方法是  Class.forName(className,true,classloader);

第2个boolean参数表示类是否需要初始化,  Class.forName(className)默认是需要初始化。

一旦初始化,就会触发目标对象的 static块代码执行,static参数也也会被再次初始化。

ClassLoader.loadClass(className)方法,内部实际调用的方法是  ClassLoader.loadClass(className,false);

第2个 boolean参数,表示目标对象是否进行链接,false表示不进行链接,由上面介绍可以,

不进行链接意味着不进行包括初始化等一些列步骤,那么静态块和静态对象就不会得到执行


参考与扩展

《深入理解Java虚拟机》


链接:


Java类的加载、链接和初始化-HollisChuang’s Blog


链接:


深度分析Java的ClassLoader机制(源码级别)-HollisChuang’s Blog


链接:


双亲委派模型与自定义类加载器 – ImportNew


链接:


Java双亲委派模型及破坏 – CSDN博客

球友



版权声明:本文为w372426096原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。