基础巩固之图解类加载过程与双亲委派模型

2018-10-13  本文已影响29人  Andy周

类的加载过程是怎样的?

类从被加载到虚拟机中内存开始,到卸载除内存为止,它的生命周期包括如下图所示:

上图中的
加载验证准备初始化卸载这5个步骤的顺序是固定的,类的加载器也必须按这个顺序开始,而解析阶段则不一定,它在某些情况下可以在初始化阶段之后再开始,这是为了支持Java语言的运行时绑定l

在加载阶段,虚拟机需要完成以下三件事情:

但虚拟机规范中并未指明二进制字节流要从哪里获取,应该怎样获取,因此加载阶段是非常灵活的。例如:

为什么加载之后要验证?

我们可以试想一下这种场景,开发人员自定义Classloader来加载指定的类,在要加载的类中插入恶意代码,如果虚拟机加载之后没有进行验证,对其完全信任,很容易导致因为加载了有问题的字节流导致系统崩溃,所以验证可以看作是虚拟机自身的一种保护措施。

验证有哪些步骤?

准备阶段作了哪些工作?

这个过程实际上做的就是正式为类变量分配内存并设置类变量初始值的属性,这些类变量所使用的内存都在方法区中进行分配。
注意:这里的内存分配仅仅只包括类变量(被static修饰的变量),而不包括实例变量,因为实例变量是要在对象实例化的时候分配在java堆中的。

public static int value = 123;

此时完成的工作仅仅是将value初始化为0而不是上述的123,而赋值为123的操作是在初始化阶段才会被执行。

解析中又做了哪些工作呢?

这一阶段实际上是虚拟机将常量池中的符号引用替换为直接引用的过程。

初始化阶段会执行哪些过程?

初始化阶段是类加载过程的最后一步了,前面的步骤中除了开发人员可以自定义类加载器之外,其余动作全部由虚拟机主导和控制,而到了初始化阶段,才开始真正的执行类中定义的字节码
准备阶段是给类变量赋了初始化的值,而在初始化阶段则是按开发人员定制的去初始化类变量。
通俗的讲:初始化阶段就是执行类构造器<clinit>()的过程。
首先要明确<clinit>()方法都干了些什么?
<clint>()方法是由编译器自动收集类中的所有类变量的赋值动作静态语句块(static{}块)

由于编译器收集的顺序是由语句在源文件中出现的顺序决定的,静态语句块中只能访问到定义在语句块之前的变量,定义在它之后的变量,之前的静态语句块可以赋值,但是不能访问,如下图所示:

<clinit>()方法与类构造器方法不同,它不需要显示的调用父类的构造器,虚拟机会保证在子类的<clinit>()方法被执行之前,父类的<clinit>()已经被执行完毕,因此虚拟机中第一个执行<clinit>()方法的类就是java.lang.Object类。

另外,虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确的加锁同步,如果多线程同时去初始化一个类,那么只会有一个线程去执行这个类中的<clinit>()方法,其他线程都需要阻塞等待。

系统有哪些类加载器?

如上图所示

类加载器的加载机制是怎样的呢?

上图所示的是类加载器相互配合进行加载的,也可以加入自定义的类加载器,这种方式就是双亲委派模型,双亲委派模型规定除了最顶层的启动类加载器外,其余的类加载器都应该有自己的父类加载器,但是这里并不是用的继承关系,而是组合关系

双亲委派模型的流程是怎样的?

一个类加载器收到加载类的请求时,它不会首先去自己尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的加载器都是如此,因此所有的加载请求最终都会委派到顶层的启动类加载器中,只有当父类加载器反馈无法加载这个类的时候(搜索范围内没有找到所需要的类),子加载器才会去加载。

为什么要使用双亲委派模型来完成加载?

我们试想一下这种场景,某个开发人员自定义了一个类加载器,然后自定义了一个和系统一样的类java.lang.String,这个类中的某个方法比如equals方法中插入一些恶意代码,这时候通过自定义的类加载器,加载到虚拟机中,系统中就会出现多个不同的java.lang.String类,当触发这些恶意代码,导致系统混乱崩溃。而双亲委派模型,由于在虚拟机启动的时候已经完成了系统的相关的类的加载,自定义的系统同名的相关类,则无法完成加载。

双亲委派模型的实现过程

我们来看看最核心的加载类的方法

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) {
                        //调用父类的ClassLoader来加载
                        c = parent.loadClass(name, false);
                    } else {
                        //查找最顶层的BootstrapClassLoader
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    .....
                }

                if (c == null) {
                    //如果父类加载器都没找到,就直接调用查找类的方法去查找
                    long t1 = System.nanoTime();
                    c = findClass(name);
                    ......
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

上面的代码很清楚,我们每个ClassLoader都会持有一个父类的ClassLoader对象,当调用当前的加载类的方法时候,其实内部会调用父类的ClassLoader来完成加载,如果最顶层的父类加载器抛出异常,说明父类无法完成加载请求,此时就由子类来完成,查找类加载类的过程了。

原文地址:
https://blog.csdn.net/byhook/article/details/83035480
github地址
https://github.com/byhook/blog
参考:
《深入理解Java虚拟机:JVM高级特性与最佳实践》

上一篇 下一篇

猜你喜欢

热点阅读