类装载器

  类加载就是虚拟机将java的Class文件加载到内存,并对数据进行验证,准备,解析,初始化的一个过程。将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。类的加载的最终产品是位于堆区中的Class对象,Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。

一、类的生命周期

  首先附上一张类的生命周期的一张图:

  类的生命周期分为 加载、链接、初始化、使用、和卸载五个阶段。其中链接阶段又分为验证、准备和解析,使用阶段分为对象初始化、垃圾搜集和对象终结。在这些阶段中发生的顺序基本都是确定的,唯有解析阶段不确定,有可能发生在初始化之前,也有可能发生在初始化之后,这是为了支持Java语言的运行时绑定(也成为动态绑定或晚期绑定)。另外注意这里的几个阶段是按顺序开始,而不是按顺序进行或完成,因为这些阶段通常都是互相交叉地混合进行的,通常在一个阶段执行的过程中调用或激活另一个阶段。

  • 加载

  加载为类装载的第一阶段,它会获取类的的二进制流,它会将calss文件中的类的信息转换为方法区的数据结构,在java堆中生成相对应的java.lang.Class对象,作为对方法区中这些数据的访问入口。相对于类加载的其他阶段而来说,加载阶段(准确地说,是加载阶段获取类的二进制字节流的动作)是可控性最强的阶段,因为开发人员既可以使用系统提供的类加载器来完成加载,也可以自定义自己的类加载器来完成加载。
  获取类的二进制流的方法:
    ①. 读取本地系统中直接加载
    ②. 将java源文件动态编译为.class文件
    ③. 读取jar文件中的.class文件
    ④. 从网络上下载.class文件

  • 链接
    ①. 验证(为了保证class流的格式是正确的)

  验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。其中包含文件格式验证(是否以0xCAFEBABE开头、版本号是否合理);元数据验证(是否有父类、是否继承了final类、非抽象类是否实现了所有的抽象方法);字节码验证(运行检查、栈数据类型和操作码数据参数是否吻合、跳转指令是否指定到合适的位置);符号引用验证(常量池中描述的类是否存在、访问的方法或字段是否存在并且有足够的权限)。

   ②. 准备(分配内存,并为类设置初始值)

    在方法区中分配内存,并为类设置初始值。例如:public static int v = 1 这段代码在准备阶段v会被设置为默认值0,而不是1,只有在初始化阶段v才会被置为1,而常量则会在初始化阶段直接置为设置的值

   ③. 解析(把类中的符号引用转换为直接引用)

    符号引用是一组符号来描述目标,可以是任何字面量。直接引用是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。

  • 初始化

   ①. 执行类的构造器 static变量赋值,执行static语句 
   ②. 子类的调用前要保证父类被调用
   ③. 是线程安全的

  下面简单看一个例子:

/**
 * 
 * @author Herrt灬凌夜
 *
 */
class SuperClass {
    static {
        System.out.println("SuperClass init!");
    }
    public static int value=123;
}
class SubClass extends SuperClass{
    static {
        System.out.println("SubClass init!");
    }
}
public class NotInitialization {
    public static void main(String[] args) {
        System.out.println(SubClass.value);
    }
}

   执行结果为:

SuperClass init!
123

  从上例可以看出,在调用SubClass.value时首先初始化了SubClass的父类SuperClass,并且执行了SuperClass类中的静态代码块,初始化了value的值,但是并没有去触发SubClass类的初始化。只有当程序”首次”并且是”主动使用”类的时候,才会执行初始化。

  主动使用:
    ①. 创建类的实例
    ②. 访问类的静态变量、或给该类的静态变量赋值
    ③. 调用类的静态方法
    ④. 反射调用类的静态方法、或反射创建类实例
    ⑤. JVM启动时被标明为启动类
    ⑥. 初始化一个类的子类

  • 使用

    ①. 对象实例化:当我们去实例化一个对象时,首先虚拟机会去常量池定位这个类的符号引用,并检查这个类是否被加载,解析和初始化过,如果没有的话就需要首先执行这几个过程,如果已经执行过这几个过程,那么虚拟机就会为新生对象分配内存(一般是指针碰撞或者空闲列表方式),将内存空间进行对象初始化(初始值全部为对应0值),设置对象头信息,将对象引入栈,执行构造器
    ②. 垃圾收集:当对象不再被引用的时候,就会被虚拟机标上特别的垃圾记号,在堆中等待GC回收(在前面的文章中有写到GC回收算法)
    ③. 对象的终结:对象被GC回收后,对象就不再存在,对象的生命也就走到了尽头

  • 卸载

  即类的生命周期走到了最后一步,程序中不再有该类的引用,该类也就会被JVM执行垃圾回收,从此生命结束…(满足下面三个条件时该类就会被回收)
    ①. 当一个类在java堆中没有任何实例时
    ②. 该类的ClassLoader已经被回收
    ③. 该类对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法。

 

二、什么是类装载器 ClassLoader

  Java类加载器(Java Classloader)是Java运行时环境(Java Runtime Environment)的一部分,负责动态加载Java类到Java虚拟机的内存空间中。类通常是按需加载,即第一次使用该类时才加载。由于有了类加载器,Java运行时系统不需要知道文件与文件系统。Classloader是一个抽象类,它的实例将读入的java字节码装载到JVM中,可以单独定制,满足不同的字节码流获取方式,主要负责类装载过程中的加载阶段。
大部分java程序会使用以下3中系统提供的类加载器:

  • 启动类加载器(Bootstrap ClassLoader): 这个类加载器负责将存放在<java_home>lib目录中的,或者被 -Xbootclasspath参数所指定的路径中的,并且在巡检识别的(仅仅按照文件名识别,如rt.jar,名字不符合的类库即使放在lib目录下也不会被加载)类库加载到虚拟机中。此类加载器并不继承于java.lang.ClassLoader,由原生代码(如C语言)编写,不能被java程序直接调用。
  • 扩展类加载器(Extendsion ClassLoader):此类负责加载<java_home>libext目录中的,或者被java.ext.dirs系统变量所指定的路径的所有类库,开发者可以直接使用扩展类加载器。
  • 应用程序类加载器(Application ClassLoader): 这个类加载器负责加载用户类路径(CLASSPATH)下的类库,一般我们编写的java类都是由这个类加载器加载,这个类加载器是CLassLoader中的getSystemClassLoader()方法的返回值,所以也称为系统类加载器.一般情况下这就是系统默认的类加载器.

三、JDK中 ClassLoader默认设计模式(双亲委派模式)

  双亲委派模式:一个类加载器接受到加载类的请求时,首先会去看自己是否加载过这个类,如果加载过则直接返回,如果没有加载过,不会立即去尝试加载这个类,而是把请求委托给父加载器,直到Bootstrap ClassLoader。因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类。

  我们在开发过程中偶尔会出现ClassNotFoundException的异常,那么在什么情况下才会出现这样的异常呢?

  • 当AppClassLoader加载一个class时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器ExtClassLoader去完成。
  • 当ExtClassLoader加载一个class时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给BootStrapClassLoader去完成。
  • 如果BootStrapClassLoader加载失败(例如在$JAVA_HOME/jre/lib里未查找到该class),会使用ExtClassLoader来尝试加载;
  • 若ExtClassLoader也加载失败,则会使用AppClassLoader来加载,如果AppClassLoader也加载失败,则会报出异常ClassNotFoundException。
    自底向上检查类是否加载,自顶向上尝试加载类。

  在classLoader中比较重要的几个方法:

方法名入参出参描述
loadClass String name Class 根据Class的名字去装载这个class,并且返回这个Class
defineClass byte[]b,int off, int len final Class 定义一个class, 给定一个byte数组,偏移量和长度,就是将class的文件以流信息传入,然后转换为一个class
findClass String name Class 在loadClass方法中做调用,查找class,自定义classLoad时推荐是重载这个方法
findLoadedClass String name final Class 查找已经被加载的class,如果查找不到再去加载这个类,如果找到则不去做二次加载

  在loadClass中,首先去找这个class,如果找到则返回,确保一个类只会被加载1次,否则则委托父级类加载器去加载。

   注意:此处的 parent 不是该类的父类,而是父加载器。

 

  我们看一个简单的例子:

package com.wyx.service;

public class FindClassOrder {

    public static void main(String[] args) {
        HelloLoder loder = new HelloLoder();
        loder.print();
    }
}

 

package com.wyx.service;

public class HelloLoder {

    public void print () {
        System.out.println("I am apploader");
    }
}

    我们直接执行main方法,发现输出的是 I am apploader

 

  然后我们将在在其他地方创建相同的类,注意包名也要相同。将此类的class文件放到E盘的tmp目录下,注意包也要一级一级创建,执行时在 VM arguments 加上-Xbootclasspath/a:E: mp

package com.wyx.service;

public class HelloLoder {

    public void print () {
        System.out.println("I am bootloader");
    }
}

 

  此时再次执行,我们发现执行的结果不再是 I am apploader  而是 I am bootloader 了。

 

   上面例子中,首先我们执行时首先去 AppClassLoader 中看这个类是否在这个类加载器中被加载,没有找到就去ExtClassLoader中找,ExtClassLoader中也没有找到,于是去BootStrapClassLoader 中找,此时也没有找到就开始加载,此时没有指定BootStrapClassLoader加载的位置,于是没有找到 HelloLoder.class,所以继续交给ExtClassLoader去加载,ExtClassLoader也没有找到 HelloLoder.class,于是交给AppClassLoader加载,在AppClassLoader中找到的 HelloLoder.class中输出的是 I am apploader 。 当我们指定 BootStrapClassLoader的加载位置为 -Xbootclasspath/a:E: mp 时,在BootStrapClassLoader加载时就找到了 HelloLoder.class 文件,所以BootStrapClassLoader就直接加载了这个类,而不是等到AppClassLoader去加载。而此时这个类中的输出为 I am bootloader 。这个例子说明了类的加载是从上向下的。

   我们再来看一个例子,在这个例子中我们同样指定BootStrapClassLoader加载的位置 :-Xbootclasspath/a:E: mp

public class FindClassOrder {

    public static void main(String[] args) throws NoSuchMethodException, SecurityException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
        //获取类加载器
        ClassLoader cl = FindClassOrder.class.getClassLoader();
        
        //获取要加载的类
        byte[] buffer = null;  
        try {  
            File file = new File("D:\Users\wuyouxin\eclipse-workspace\springBootTest\target\classes\com\wyx\service\HelloLoder.class");
            FileInputStream fis = new FileInputStream(file);  
            ByteArrayOutputStream bos = new ByteArrayOutputStream(1000);  
            byte[] b = new byte[1000];  
            int n;  
            while ((n = fis.read(b)) != -1) {  
                bos.write(b, 0, n);  
            }  
            fis.close();  
            bos.close();  
            buffer = bos.toByteArray();  
        } catch (FileNotFoundException e) {  
            e.printStackTrace();  
        } catch (IOException e) {  
            e.printStackTrace();  
        }  
        Method md_defineClass = ClassLoader.class.getDeclaredMethod("defineClass", 
                byte[].class,int.class,int.class);
        
        //因为 defineClass 是protected final的,如果不设置则不可调用
        md_defineClass.setAccessible(true);
        //强制使用AppClassLoader加载 HelloLoder.class
        md_defineClass.invoke(cl, buffer, 0, buffer.length);
        md_defineClass.setAccessible(false);
        
        HelloLoder loder = new HelloLoder();
        System.out.println(loder.getClass().getClassLoader());
        loder.print();
    }
}

   不同的是我们使用 AppClassLoader 强制加载了 第一个 HelloLoder.class,所以此时的执行结果为:

  我们可以看出加载的类加载器为AppClassLoader ,执行的语句也为 I am apploader,所以当系统需要使用HelloLoder类时,直接去AppClassLoader 找类有没有被加载,现在找到了就不继续向父级类加载器发起委派了。此例说明,类的检查是否被加载是自底向上的。

 四、上下文加载器(Thread.SetContextClassLoader())

   但是,这种双亲委派模式存在一个问题,就是查看类加载都是从下往上的,所以,在顶层的classLoder中无法加载底层classLoder的类,也就是说在 BootStrapClassLoader 中无法加载 应用层的类,而在rt.jar中有些类中的方法可能在我们应用中被重写了,但是正在 BootStrapClassLoader 中是无法加载到重写的方法的。 但是有时候就需要在 BootStrapClassLoader 去访问应用class中加载的类,这种模式就无法做到。为了解决这个问题就提出了一个上下文加载器,他用以解决顶层classLoader无法访问底层classLoader的类的问题,基本思想就是在顶层classLoader中传入底层classLoader的实例。它可以任务是一个角色,因为他不是一个具体的classLoder,他可以由任何一个classLoader来做这个上下文加载器。

  下面这个方法来自于javax.xml.parsers.FactoryFinder类中的getProviderClass 方法,我们可以看出其中加载时将 cl 传入后加载className 而此时的 cl 就作为一个上下文加载器。

 

 

五、双亲模式的破坏

  在jdk中默认的classLoader的加载模式是双亲委派模式,但是并不是必须要这么去做。比如tomcat中的WebappClassLoader就是先加载自己的class,找不到再去委派给上级的 classLoader。 而OSGi(热部署)的classLoader的结构是网状的,它内部有自己的一套算法,根据自己的需要去自由的加载class。

六、自定义 ClassLoader

  下面代码是自定义 OrderClassLoader 中的部分代码:其中就是首先加载自己的类,如果无法加载则在委托上级加载器去加载。

/**
 * 自定义 classLoder
 * @author Herrt灬凌夜
 *
 */
public class OrderClassLoader extends ClassLoader {

    @Override
    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        Class re = findClass(name);
        if (re == null) {
            System.out.println("无法载入类:" + name + "需要委托父加载器");
            return super.loadClass(name, resolve);
        }
        return re;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        //首先去找这个classloder中是否有加载这个类
        Class clazz = this.findLoadedClass(name);
        //如果没有,则去加载
        if (null == clazz) {
            try {  
                File file = new File(name);
                FileInputStream fis = new FileInputStream(file);  
                ByteArrayOutputStream bos = new ByteArrayOutputStream(1000);  
                byte[] b = new byte[1000];  
                int n;  
                while ((n = fis.read(b)) != -1) {  
                    bos.write(b, 0, n);  
                }  
                fis.close();  
                bos.close();  
                byte[] buffer = bos.toByteArray(); 
                //加载类
                clazz = defineClass(name, buffer, 0, buffer.length);
            } catch (FileNotFoundException e) {  
                e.printStackTrace();  
            } catch (IOException e) {  
                e.printStackTrace();  
            }  
        }
        return clazz;
    }
}

-------------------- END ---------------------


最后附上作者的微信公众号地址和博客地址 


公众号:wuyouxin_gzh



 


Herrt灬凌夜:https://www.cnblogs.com/wuyx/

 

原文地址:https://www.cnblogs.com/wuyx/p/9705132.html