类加载机制以及类加载器---Java虚拟机


类加载机制以及类加载器—Java虚拟机

当编写的一个java类文件被编译之后就变成了 类名.class 的字节码文件.但是此时的.class文件还不能马上执行,只有当其被加入到内存中的方法区,并对数据进行校验,转换解析和初始化之后,最终成为可以被java虚拟机直接使用的Java类型.这个过程就被称为是Java虚拟机的类加载机制.下面我们通过两张图来形象的了解,类加载过程在整个JVM架构中的地位.
在这里插入图片描述

大家可以看到,JVM架构中有一个专门的类加载子系统来描述这个过程.这其中就有两个部分值得大家好好了解

  1. 一个.class文件是怎么被加载进来的:这就是类加载机制
  2. 什么东西负责将class文件加载进来:这就是类加载器

下面就从下面这两方面来深入了解一下类加载子系统.

1.类加载机制(Class Loading Mechanism):

在Java语言中,类型的加载,连接和初始化过程都是在程序的运行过程完成的,这样虽然会让java语言产生一点额外的开销,但是却为Java引用提供了极高的扩展性和灵活性,java天生可以动态扩展的语言特性就是依赖运行期动态加载和动态连接来实现的.

一个类型从被加载到虚拟机内存中开始,到卸载出内存为止,整个生命周期会经历7个小阶段:

  1. 加载(Loading)
  2. 验证(Verification)
  3. 准备(Preparation)
  4. 解析(Resolution)
  5. 初始化(Initialization)
  6. 使用(Using)
  7. 卸载(Unloading)

其中验证,准备,解析三个阶段统称为连接(Linking).其中加载,验证,准备,初始化和卸载这5个过程的顺序是确定的.下面是类的生命周期示意图:

在这里插入图片描述

1.1加载(Loading)

加载(loading)是类加载(ClassLoading)的一个过程而已,大家不要搞混淆了.在加载(Loading)阶段,java虚拟机需要完成以下三件事情:

  1. 通过一个类的全限定名来获取定义该类的二进制字节流文件:例如大家常使用的String类的全限定名就是java.lang.String
  2. 将该字节流所代表的静态存储结构转换为方法区(Method Area)中的运行时数据结构:class文件主要就是加载进了方法区
  3. 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口(反射机制):通过反射机制就可以完成各种操作

1.2验证(Verification)

验证是连接阶段的第一步,这一阶段的目的是确保class字节流中的信息符合<java虚拟机规范>的全部要求,确保它运行后不会对JVM造成危害.

1.3准备(Preparation)

准备阶段是正式为**类中定义的变量(用static修饰的静态变量,也称类变量)分配内存并设定零值(用final修饰的除外)的阶段.而实例变量(也就是没有用static修饰的变量)**会等到某个具体的对象被实例化的时候才会在java堆中分配.
在这里插入图片描述

例如:

	private static int a=1;//准备阶段分配内存,并且赋初始值0.初始化阶段初始化为1
    private int b=2;//new 一个新对象的时候才在堆中分配内存并赋值
    private final static int c=3;//准备阶段就会分配内存,并且赋初始值3

也就是说类变量在准备阶段不会按照代码中初始值来赋值,而是会等到初始化阶段.但是如果用final来修饰就不一样了.因为用final修饰的变量在javac进行编译的时候就会为其生成一个ConstantValue属性,于是在准备阶段就会依照ConstantValue来赋值

1.4解析(Resolution)

解析阶段就是JVM将常量池中的符号引用替换为直接引用的过程.

1.5初始化(Initialization)

类的初始化时类加载过程中的最后一个动作.之前地介绍的几个过程中除了在加载(Loading)阶段可以通过用户自定义加载器来局部参与以外,其余的都是JVM在主导控制.直到初始化阶段JVM才会真正开始执行程序员在类中编写的Java程序代码,将主导权交给应用程序.

在准备阶段,类变量已经被赋了零值(常量除外),但是在初始化阶段,就会根据程序员的编码来初始化类变量以及其他资源.也可以从另一个更加直观的角度来表达:初始化阶段就是执行类构造器大的< clinit >()方法的过程.< clinit >()并不是由程序员编写的,而是Javac自动收集类中的所有赋值动作和静态语句块(static{})中的语句合并而成的.< clinit >()中执行顺序就是语句在源文件中出现的顺序.

例如我们编写一段代码,并通过Jclasslib插件查看字节码文件:

public class ClinitTest
{
    
    
    private int a=0;
    private static int b=1;
    static
    {
    
    
        b=2;
    }
}

Jclasslib查看结果如图:

在这里插入图片描述

可以看到,按照顺序先将b的值赋为1,再将b的值赋为2.没有出现实例变量a的身影

下面还有几个关于< clinit>方法的注意事项就介绍一下但是不演示了.

  • 在该类的< clinit >方法调用之前,JVM会保证父类的< clinit >一定已经执行完毕,也就是说Object类的< clinit >一定是最先执行的.
  • 一个类的< clinit >方法并不是必须的,如果一个类中没有类变量的复制和静态代码块的话,就不会为这个类生成< clinit >方法.
  • 接口中不能使用静态代码块,但是可以声明变量,所以也可以生成< clinit >方法,但是执行接口的< clinit >之前不会先执行父接口的< clinit >方法.而且接口的实现类在初始化的过程中也不会执行接口的< clinit >方法.
  • < clinit >会被正确的加锁同步,因为一个类只能被初始化一次.

2.类加载器(ClassLoader)

用来执行加载(Loading)阶段中"通过一个类的全限定名来获取描述该类的二进制字节流"的动作的代码就被称为"类加载器"

类加载器结构示意图:

在这里插入图片描述

很多人一看,类加载器只是做了类加载阶段一个阶段中的一个步骤,是不能再小的一个功能呢,为什么还要把它当做一个专门的部分来大书特书呢?

其实不然,它在java程序中起到的作用远超类加载阶段.对于任意一个类,都必须由加载它的类加载器和这个类本身来一起确定其在JVM中的唯一性,每一个类加载器都拥有一个独立的类名称空间.用更加通俗的语言表示的话:

判断两个类是否相等条件: (来自同一个class文件&&被同一个类加载器加载)

2.1类加载器的种类(JDK1.8)

站在JVM的角度来看,只存在两大类类加载器:

  • 启动类加载器(Bootstrap ClassLoader):用C++语言实现,是JVM自身的一个部分
  • 其他类加载器:由Java语言实现,独立于虚拟机之外,继承自抽象类java.lang.ClassLoader

但是站在java开发人员的角度来看,类加载器就分的更加详细了,java一直保持着三层类加载器.双亲委派的类加载架构.

细致的分类的话,可以分成4种,按照优先级排序如下,上面的加载器是下面的加载器的父类加载器

  1. 启动类加载器(Bootstrap ClassLoader)
  2. 扩展类加载器(Extension ClassLoader)
  3. 应用程序类加载器(Application ClassLoader):又称系统类加载器
  4. 用户自定义类加载器(UserDefined ClassLoader)

在这里插入图片描述

2.2启动类加载器(Bootstrap ClassLoader)

启动类加载器是由c++代码编写的,主要用于加载:

  1. <JAVA_HOME>\lib中的核心类比如rt.jar,tools.jar中类
  2. 或者是通过JVM参数 VM Options:-Xbootclasspath参数所指定的类
ClassLoader classLoader = String.class.getClassLoader();//结果是null,但是不意味着不存在

结果是null,但是不意味着不存在,因为它是由C++编写的,所以在Java层面无法表示出来,结果就是null.在JVM的类加载器中可以直接用null来指代Bootstrap ClassLoader.

2.3扩展类加载器(Extension ClassLoader)

这个类加载器是在类 sun.misc.Launcher$ExtClassLoader 中以java代码实现的,它负责加载:

  1. <JAVA_HOME>\lib\ext目录
  2. 或者被java.ext.dirs系统变量所指定的路径中的所有类库
        ClassLoader classLoader = SunEC.class.getClassLoader();
        //结果是sun.misc.Launcher$ExtClassLoader@72ea2f77
        System.out.println(classLoader);

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

这个类加载器是sun.misc.Launcher$AppClassLoader来实现的,它负责加载用户类路径(ClassPath)上的所有类库,也就是用户自己写的类和第三方的类库.

        //springboot是第三方的类库
        ClassLoader classLoader = SpringBootCondition.class.getClassLoader();
        //结果是sun.misc.Launcher$AppClassLoader@18b4aac2
        System.out.println(classLoader);

        //ClassLoaderTest是我自己定义的一个类类
        ClassLoader classLoader1 = new ClassLoaderTest().getClass().getClassLoader();
        //结果是sun.misc.Launcher$AppClassLoader@18b4aac2
        System.out.println(classLoader1);

        //通过ClassLoader.getSystemClassLoader()可以获取ApplicationClassLoader
        ClassLoader ClassLoader3 = ClassLoader.getSystemClassLoader();
        //结果是sun.misc.Launcher$AppClassLoader@18b4aac2
        System.out.println(ClassLoader3);

又因为它是ClassLoader中静态方法getSystemClassLoader()的返回值,所以又称"系统类加载器".

2.5 用户自定义类加载器(UserDefined ClassLoader)

在默认情况下,java提供了3种类加载器,但是针对某些特殊情况,程序员可以定制属于自己的类加载器,只需要实现ClassLoader接口即可.

3.双亲委派模型(Parent Delegation Model)以及其破坏

3.1双亲委派模型

java中对类的加载采用的是动态加载,既只要当使用到该类的时候才会去加载对应的字节码文件.
当一个类加载器收到了类加载请求的时候并不会立马就去加载,而是将这个请求委派给给它的父类加载器.直到到达顶层的Bootstrap加载器
如果父类加载器可以完成类的加载过程,那么这个类的加载就交给父类完成.否则就会委派给其子类加载器.

下面是双亲委派模型的实现:

protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    
    
        //1 首先检查类是否被加载
        Class c = findLoadedClass(name);
        if (c == null) {
    
    
            try {
    
    
                if (parent != null) {
    
    
                    //2 没有则调用父类加载器的loadClass()方法;
                    c = parent.loadClass(name, false);
                } else {
    
    
                    //3 若父类加载器为空,则默认使用启动类加载器作为父加载器;
                    c = findBootstrapClass0(name);
                }
            } catch (ClassNotFoundException e) {
    
    
                //4 若父类加载失败,抛出ClassNotFoundException 异常后
                c = findClass(name);
            }
        }
        if (resolve) {
    
    
            //5 再调用自己的findClass() 方法。
            resolveClass(c);
        }
        return c;
    }

3.2双亲委派模型的好处

  1. 避免了类的重复加载:Java中的类和它的类加载器一起具备了一种带有优先级的层次关系,例如java.lang.Object类是java中最核心类,无论哪一个类加载器要加载这个类,最终都会交给最顶端的Bootstrap类来加载,从而保证了不同环境下的Object都是同一个类(因为类相同有两个方面).

  2. 保护程序安全,防止核心API被随意篡改:在编程中我们常常会使用第三方库或者是自己定义的类,如果在程序中出现了一个全限定名为

    java.lang.Object的类,在没有双亲委派模型的情况下,就会出现多个Object类,从而导致程序混乱.

3.3 双亲委派模型的破坏

双亲委派模型并不是一个具有强制性约束的模型,而是Java设计者推荐给开发者的类加载器实现方式.在Java的世界中大部分的类加载器都遵循这个模型,但是也有例外的情况,直到java模块化(JDK9)出现之前,它一共出现过3次较大规模的"被破坏"的情况

  1. 第一次"被破坏"出现在双亲委派模型出现(JDK1.2)之前,因为双亲委派模型在JDK1.2才被引入,但是ClassLoader在JDK1.1就已经存在.所以在这之前该模型是被破坏的.JDK1.2引入双亲委派模型之后,面对已经存在的用户自定义的类加载器(主要是重写loadClass方法),不得不做出一些妥协.因为双亲委派模型就是体现在loadClass()方法中,而JDK1.1的用户都重写了loadClass()方法,这样就导致了模型被破坏.所以设计者在java.lang.ClassLoader类中定义了一个新的protected 方法findClass().引导用户编写类加载机制的时候重写该方法,就不会破坏模型.

    下面的代码就是loadClass方法,它的逻辑就是双亲委派模型,大家可以看到第二个if(c==null) 的代码处,当java的三个默认类加载器都没有完成类的加载就会执行用户自定义的findClass(),这样既不影响用户按照自己的意愿去加载类,又不会破坏双亲委派模型.

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
    
    
        synchronized (getClassLoadingLock(name)) {
    
    
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
    
    
                long t0 = System.nanoTime();
                try {
    
    
                    if (parent != null) {
    
    
                        c = parent.loadClass(name, false);
                    } else {
    
    
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
    
    
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
    
    
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    c = findClass(name);//程序员重写findClass方法这样就不会破坏模型

                    // 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;
        }
    }

2.它的第二次破坏是由于它自身的缺陷所导致的.双亲委派模型很好的解决了各个类加载器协作时基础类型的一致性问题(越基础的类型由越上层的类加载器加载),但是如果基础类型又需要调用用户的代码,那该怎么办呢?换句话说:程序员用自己的代码实现了一个非常基础的接口,也就是说这个非常基础的接口肯定是由Bootstrap ClassLoader来加载的,但是类加载器却不认识用户的代码?那该怎么办呢?

了解决这个困境,Java设计团队只好引入了一个不太优雅的设计:线程上下文件类加载器(Thread Context ClassLoader)。这个类加载器可以通过java.lang.Thread类的setContextClassLoader()方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个;如果在应用程序的全局范围内都没有设置过,那么这个类加载器默认就是应用程序类加载器.也就是说高级的类加载器会借助线程上下文件类加载器来加载用户的代码,这其实就已经打破了双亲委派模型.

在这里插入图片描述

3.双亲委派模型的第三次“被破坏”是由于用户对程序的动态性的追求导致的.IBM针对此推出了OSGi.

OSGi实现模块化热部署的核心在于它自定义的类加载器机制的实现。所有的程序模块(Bundle)都有一个自己的类加载器,当需要更换一个Bundle时,就把Bundle连同类加载器一起换掉来实现代码的热替换。在OSGi幻境下,类加载器不再是双亲委派模型中的树状结构,而是进一步发展为更加复杂的网状结构,当受到类加载请求时,OSGi将按照下面的顺序进行类搜索:

  1. 把java.*开头的类委派给父类加载器加载。
  2. 不然,把委派列表名单内的类委派给父类加载器加载。
  3. 不然,把Import列表中的类委派给Export这个类的Bundle的类加载器加载。
  4. 不然,就查找当前Bundle的ClassPath,并使用自己的类加载器加载。
  5. 不然,就查找类是否在自己的Fragment Bundle中,如果在,则委派给Fragment Bundle的类加载器加载。
  6. 不然,就查找Dynamic Import列表的Bundle,委派给对应Bundle的类加载器加载。
  7. 不然,类加载器失败。

码字不易,觉得写得不错的小伙伴可以给个三连吗?蟹蟹

猜你喜欢

转载自blog.csdn.net/qq_44823898/article/details/111239669