一文搞懂ClassLoader类加载机制

ClassLoader类加载机制

文章目录

  • ClassLoader类加载机制
  • 一、什么是ClassLoader?
  • 二、Java内置的3类Classloader
    • 1.Bootstrap ClassLoader
    • 2.Extention ClassLoader
    • 3.AppClassLoader
  • 三、ClassLoader约定的类加载机制---双亲委托模型
  • 四、双亲委托模型
  • 五、类加载过程
    • 1.加载
    • 2.链接
      • a.验证
      • b.准备
      • c.解析
    • 3.初始化
  • 总结


一、什么是ClassLoader?

      ClassLoader类加载器,它的具体作用就是讲Class文件加载到JVM虚拟机中去,程序就可以正常运行了。但是JVM启动的时候并不会一次性加载所有的class文件,而是根据需要去动态加载。

二、Java内置的3类Classloader

1.Bootstrap ClassLoader

      java最顶层的、程序引导启动的类加载器,主要加载核心类库:%JRE_HOME%\lib下的rt.jar、resources.jar、charsets.jar和class等,对应用不可见。

2.Extention ClassLoader

      扩展类的加载器,默认家在目录%JRE_HOME%\lib\ext目录下的jar和class文件。

3.AppClassLoader

      也称SystemAppClassLoader
      加载当前应用的classpath的所有类,即应用程序依赖的jar和本身的class文件中定义的类。

三个类加载器,可以通过指定参数来改变或追加扫描路径

ClassLoader类型 参数类型 说明
Bootstrap ClassLoader -Xbootstrapclasspath: 设置Bootstrap ClassLoader的搜索路径
-Xbootstrapclasspath/a: 把路径添加到已存在搜索路径的后面
-Xbootstrapclasspath/b: 把路径添加到已存在搜索路径的前面
Extention ClassLoader -Djava.ext.dir 设置Bootstrap ClassLoader的搜索路径
AppClassLoader -Djava.class.path=或者-cp-classpath 设置AppClassLoader的搜索路径

三、ClassLoader约定的类加载机制—双亲委托模型

      当一个类加载器接收到一个类加载的任务时,不会立即加载,而是将加载任务委托给它的父类加载器去执行,每一层的类都采用相同的方式,直接委托给最顶层的启动类加载器为止。如果父类加载器无法加载委托给它的类,便将类的加载任务退回给下一级类加载器去执行加载。
      使用双亲委托机制的好处是:能够有效确保一个类的全局唯一性,当程序中出现多个限定名相同的类时,类加载器在执行加载时,始终只会加载其中的某一个类。
      双亲委托模型对于保证JAVA程序的稳定运作很重要,但它的实现却非常简单,实现双亲委托的代码都集中在java.lang.ClassLoader的loadClass()方法中,逻辑清晰易懂:先检查是否已经倍加再过,若没有加载则调用父类加载器的loadClass()方法,若父加载器为空则默认使用启动类加载器作为父加载器。如果父类加载器加载失败,包出ClassNotFoundException异常后,在调用自己的findClass方法进行加载。

一文搞懂ClassLoader类加载机制_第1张图片

时序图解析:
      1.一个APPClassLoader查找资源时,先查看缓存中是否存在,缓存有从缓存中获取,否则委托给父类加载器。
      2.重复第1步的操作。
      3.如果ExtClassLoader也没有加载过,则有BootstrapClassLoader加载,首先查找缓存,没有查到就去扫描自己规定的路径,sun.mic.boot.class下面的路径,找到就返回,否则让子加载器自己加载。
      4.Bootstrap ClassLoader如果没有找到,则ExtClassLoader自己在java.ext.dirs路径中扫描,查到就返回,否则继续向下让子加载器查找。
      5.ExtClassLoader查不到,AppClassLoader就自己扫描,在java.class.path路径下查找,找到就返回,如果没有找到就让子类扫描。如果没有子类了就会抛出异常。

四、双亲委托模型

      双亲委托模型为什么会这样加载?我们还需要了解ClassLoader类,尤其是重要的方法loadClass()、findLoadedClass()、findClass()、defineClass()。
      ClassLoader是所有类加载器(BootstrapClassLoader除外,因为它是JVM层面的类加载器)的最顶级基类,他有一个ClassLoader类型parent字段,及它的父类加载器,注意这里不是父类,一个类加载器和父类加载器是组合关系,不是继承关系。

ClassLoader部分方法定义

public abstract class ClassLoader {
private final ClassLoader parent;
public Class<?> loadClass(String name) throws ClassNotFoundException;
protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException;
protected final Class<?> findLoadedClass(String name);
protected Class<?> findClass(String name) throws ClassNotFoundException;
protected final void resolveClass(Class<?> c);
protected final Class<?> defineClass(String name, byte[] b, int off, int len,ProtectionDomain protectionDomain) throws ClassFormatError;
protected final Class<?> defineClass(String name, java.nio.ByteBuffer b,ProtectionDomain protectionDomain)throws ClassFormatError;
}

      parent:父类加载器,每一个类加载器都有父类加载器,但是严格的从语言层面,ExtClassLoader的父类加载器是null,但是我们认为他的父类加载器是Bootstrap ClassLoader,下面讲解loadClass方法时会解释为什么这样认为。
      loadClass:JDK文档中是这样说明的,通过指定的全限定类名加载class,有两个重载的方法
      loadClass(String name)内部调用loadClass(String name,boolean resolve),resolve参数表示是否解析(或者说是链接)这个类,第一个loadClass方法调用第二个loadClass方法时,resolve传false。loadClass(String name,boolean resolve)方法就是体现双亲委托模型的关键所在。这个方法的关键步骤是:

  • 1.执行findLoadedClass(String)从已加载的类中查找,找到了则返回,否则执行后面的步骤。
  • 2.如果父加载器不为null,则执行父加载器的loadClass方法(而父类加载器的loadClass同样遵循这里的1~4的步骤),否则调用findBootstrapClassOrNull(String)方法,调用jvm内置的加载器去替代,也就是Bootstrap ClassLoader。这也就解释了ExtClassLoader的parent为null,但仍然说Bootstrap ClassLoader是它的父加载器。
  • 3.如果向上委托父加载器没有加载成功,则通过findClass(String)查找。
  • 4.如果class在上面的步骤中找到了,参数resolve又是true的话,那么loadClass()又会调用resolveClass(class)方法来生成最终的class对象。

我们从源码可以看出这个步骤:
一文搞懂ClassLoader类加载机制_第2张图片

protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
		//先从已加载的类中查找
        Class<?> c = findLoadedClass(name);
		//如果没有找到则调用父类加载器加载
        if (c == null) {
            long t0 = System.nanoTime();
            try {
				//父类加载器不为null,则调用父类加载器加载
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
					//否则调用Bootstrap ClassLoader加载
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // ClassNotFoundException thrown if class not found
                // from the non-null parent class loader
            }
			//如果父类加载器还没有找到,则调用findClass方法
            if (c == null) {
                // If still not found, then invoke findClass in order
                // to find the class.
                long t1 = System.nanoTime();
				//这个findClass(String)方法,ClassLoader本身做了报异常的实现,我们自定义ClassLoader需要实现的
                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;
    }
}
不管是Bootstrap ClassLoader还是ExtClassLoader等,这些类加载器都只是加载指定的目录下的jar包或者资源。如果在某种情况下,我们需要动态加载一些东西呢?比如从D盘某个文件夹加载一个class文件,或者从网络上下载class主内容然后进行加载
如果要这样做的话,需要我们自定义一个ClassLoader 步骤:
  • 1.编写一个类继承自ClassLoader抽象类。
  • 2.复写它的findClass()方法。
  • 3.在findClass()方法中调用defineClass()。 defineClass()这个方法在编写自定义classLoader的时候非常重要,它能将class
    二进制内容转换成Class对象,如果不符合要求的会抛出各种异常。
注意:一个ClassLoader创建时如果没有指定parent(指定为null算是指定),那么它的parent默认就是AppClassLoader。 上面说的是,如果自定义一个ClassLoader,默认的parent父加载器是AppClassLoader,因为这样就能够保证它能访问系统内置加载器加载成功的class文件。

五、类加载过程

      类从被加载到JVM中开始,到卸载为止,整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载7个阶段;
      其中类加载过程包括加载、验证、准备、解析和初始化五个阶段;

1.加载

      由类加载器负责根据一个类的全限定名来读取此类的二进制字节流到JVM内部,并存储在运行时内存区的方法区,然后将其转换成一个与目标类对应的java.lang.Class对象实例(Java虚拟机规范没有明确要求一定存储在堆区中,只是hotspot选择将Class存储在方法区中),这个Class对象在之后就会作为方法区中该类的各种数据的访问入口。

2.链接

      链接阶段要做的是将加载到JVM中的二进制字节流的类数据信息合并到JVM的运行时状态中,经由验证、准备和解析三个阶段

a.验证

      验证类数据信息是否符合Java规范,是否是一个有效的字节码文件,验证内容涵盖了类数据信息的格式验证,语义分析、操作验证等。
      格式验证:验证是否符合class文件规范
      语义验证:检查一个被标记为final的类型是否包含子类;检查一个类中的final方法是否被子类进行重写;确保父类和子类之间没有不兼容的一些方法声明(方法签名相同,但方法的返回值不同)
      操作验证:在操作数栈中的数据必须进行正确的操作,对常量池中的各种符号引用执行验证(通过在解析阶段执行,检查是否通过符号引用中描述的全限定名定位到指定类型上,以及类尘缘信息的访问修饰符是否允许访问等)

b.准备

      为类中的所有静态变量分配内存空间,并为其设置一个初始值(由于还没有产生对象,实例变量不在此操作范围内)被final修饰的静态变量,会直接赋予原值;类字段的字段属性表中存在ConstantValue属性,则在准备阶段,其值就是ConstantValue的值

c.解析

      将常量池中的符号引用转为直接引用(得到类或者字段、方法在内存中的指针或者偏移量,以便直接调用该方法),这个可以在初始化之后执行。
      可以认为是一些静态绑定的会被解析,动态绑定则只会在运行时进行解析;静态绑定包括一些final方法(不可以重写),static方法(只会属于当前类)构造器(不会被重写)

3.初始化

      将一个类中所有被static关键字标识的代码统一执行一遍,如果执行的是静态变量,那么就会使用用户指定的值覆盖之前在准备阶段设置的初始值;如果执行的是static代码块,那么在初始化阶段JVM就会执行static代码块中定义的所有操作。
      所有类变量初始化语句和静态代码块都会在编译时被前端编译器放在收集器里头,存放到一个特殊的方法中,这个方法就是方法,即类/接口初始化方法。该方法的作用就是初始化一个类中的变量,使用用户指定的值覆盖之前在准备阶段里设置的初始值。任何invoke之类的字节码都无法调用方法,因为该方法只能在类加载的过程中有JVM调用。
      如果父类还没有被初始化,那么优先对父类初始化,但在方法内部不会显示调用父类的方法,由JVM负责保证一个类的方法执行之前,它的父类方法已经被执行。
      JVM必须确保一个类在初始化的过程中,如果是多线程需要同时初始化它,仅仅只能允许其中一个线程对其初始化操作,其余线程必须等待,只有在活动线程执行完对类的初始化操作之后,才会通知正在等待的其他线程。
      类初始化是类加载过程的最后一个阶段,到初始化阶段,才真正开始执行类中的Java程序代码。虚拟机规范严格规定了有且只有5中情况必须立即对类进行初始化:

  • 第一种:遇到new、getstatic、putstatic、invokestatic这四条字节码指令时,如果类还没有进行过初始化,则需要先触发初始化。生成这四条指令最常见的Java代码场景是:使用new关键字实例化对象时、读取或设置一个类的静态字段(static)时(被static修饰又被final修饰的,已在编译期把结果放入常量池的静态字段除外)、以及调用一个类的静态方法时。
  • 第二种:使用java.long.refect包的方法对类进行反射调用时,如果类还没有进行过初始化,则需要先触发初始化。
  • 第三种:当初始化第一个类的时候,如果发现其父类还没有进行初始化,则需要先触发其父类的初始化。
  • 第四种:当虚拟机启动时,用户需要指定一个要执行的主类,虚拟机会先执行该主类。
  • 第五种:当使用JDK1.5支持时,如果java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。

虚拟机规定有且只有这5种情况才会触发类的初始化,这5中场景中的行为称为对一个类进行主动引用,除此之外所有引用类的方式都不会触发其初始化,称为被动引用。


总结

java类装载方式有两种:
1.隐式装载, 程序在运行过程中当碰到通过new 等方式生成对象时,隐式调用类装载器加载对应的类到jvm中。
2.显式装载, 通过class.forname()等方法,显式加载需要的类

你可能感兴趣的:(java,jvm,class)