您的当前位置:首页正文

Java类加载机制解析

来源:华拓网

了解我们写的代码是怎么被编译、加载、执行、卸载对我们提升代码能力很重要。
我们写的java代码首先被编译器编译成字节码(class文件),在执行代码的时候必须将字节码加载到虚拟机中,虚拟机进行一系列操作后才能被执行。而虚拟机合适如何加载字节码?java虚拟机对加载的字节码会进行怎样的操作?这就是这篇博客中要解释的内容。

什么是java的类加载机制

虚拟机把描述类的数据从class文件加载到内存,并对数据进行校、转换解析和初始化,最终形成可以被虚拟机直接使用的java类型,这就是虚拟机的类加载机制。

什么时候加载类

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


image.png

其中,加载、验证、准备、初始化、卸载这5个阶段的顺序是确定的,而解析步骤开始的时机不一定:某种情况下们可以在初始化之后再进行解析,从而实现java的动态绑定和动态连接机制。

java虚拟机规范没有规定什么进行类加载,但是java虚拟机规范严格规定了有且只有5中情况必须对类进行初始化(加载、验证、准备自然需要在这之前完成)。这五种情况是:(1)遇到new、getstatic、putstatic、invokestatic这四条字节码时,如果类没有进行过初始化,则需要先触发其初始化。生成这四条指令的最常见的java代码场景是:使用new实例化对象、读取或设置一个类的静态字段(final修饰的除外,final修饰变量在类编译的时候已经把结果放入常量池中)、调用一个类的静态方法。(2)对类进行反射调用的时候,如果类没有进行初始化,则需要先触发其初始化。(3)当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。(4)当虚拟机启动时,用户需要指定一个执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。(5)当使用JDK 1,7的动态语言支持时,如果一个java.lang.invoke.MathodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic方法的句柄,并且这个方法对应的类没有经过过厨师哈,则需要先触发其初始化。

类加载过程

类加载的过程,分为加载、验证、准备、解析、初始化这5个阶段。

加载
验证

验证时连接阶段的第一步,这一阶段的目的是为了确保class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
验证阶段大致完成以下四个阶段的验证动作:
(1)文件格式验证。目的是验证字节流是否符合class文件格式的规范,包括:魔数、主次版本号、常量池类型是否合法等。
(2)元数据雁阵。主要对类信息进行验证(是否有父类、是不是继承了不能被继承的类、如果不是抽象类是否实现了父类的所有抽象方法,类中字段、方法是否与父类有矛盾)。
(3)字节码验证。目的是通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。
(4)符号引用验证。这个阶段发生在虚拟机将符号引用转化为直接引用的时候。

准备

准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都在方法区中分配。
这里的类变量指的是被static修饰的变量。
如果一个类变量被final修饰,那么在准备阶段就会给他赋值,否则赋值操作在类构造器<clinit>()方法中,初始化阶段才会赋值(现在为默认的值)。

解析

解析阶段是虚拟机将常量池中的符号引用替换为直接引用的过程。其中包括对类和接口的解析、对字段的解析、对类方法的解析、接口方法的解析。
我认为解析的过程就是一个从模糊到确定的过程。比如一个变量, 在class文件中只有描述这个变量信息的符号引用,并没有直接指向内存地址的引用,解析的过程就是确定变量在内存中引用地址的过程。

初始化

在准备阶段,我们已经为final类型的变量赋值了,也为类变量设置了默认值。初始化阶段根据java程序来为类变量和其他资源赋值。
初始化的过程就是执行类构造器<clinit>()的过程,<clinit>()由java虚拟机生成,包含类中类变量的赋值操作和静态代码块。如果一个类没有类变量赋值操作和静态代码块,虚拟机不会为其生成<clinit>()函数。虚拟机会保证<clinit>()在多线程环境下的同步,如果多个线程同时去初始化一个类,只有一个线程拥有锁,其他线程都处于阻塞状态。因此,如果<clinit>()方法比较耗时,就可能造成多个线程长时间阻塞。

类加载器

类加载器负责类加载的加载阶段。类加载的验证,准备,解析,初始化都是虚拟机自动完成的,而加载工作交给了类加载器,我们可以自定义类加载器,这样就有了很大的灵活性。

双亲委派机制

双亲委派机制:如果一个类加载器收到类加载的请求,他首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此。只有父类加载器反馈自己无法完成这个加载请求时,子类加载器才会尝试自己去加载。
双亲委派机制并不是java虚拟机规范中规定的类加载方式,但是从JDK1.2开始,双亲委派机制已经是一种约定俗成的加载方式,几乎所有的java都是通过双亲委派机制进行加载的。
双亲委派机制中各个类加载器形成一个数状结构,每个类在他的应用范围内只会被加载一次。
双亲委派机制模型的伪代码如下

 protected synchronized Class<?> loadClass(String name, boolean resolve) throws **ClassNotFoundException** {

        // 首先,检查请求的类是否已经被加载过了
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            try {
                if (parent != null) {
                    // 调用父加载器的loadClass()方法
                    c = parent.loadClass(name, false);
                } else {
                    // 若父加载器为空则默认使用启动类加载器作为父加载器(只有启动类加载器父类为null)
                    c = findBootstrapClassOrNull(name);
                }
            } catch (**ClassNotFoundException** e) {
                // 如果父类加载器抛出ClassNotFoundException 说明父类加载器无法完成加载请求
            }

            if (c == null) {
                // 在父类加载器无法加载的时候再调用自身的findClass方法进行类加载
                c = findClass(name);

            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
不同的类加载器

双亲委派机制使得类加载器形成一种树状结构。根据层级关系,可以将类加载器分为以下三种
(1)启动类加载器(Bootstrap ClassLoader)
这个类加载器由c++语言实现,是虚拟机自身的一部分。这个类加载器负责将存放在<JAVA_HOME>\lib目录中,或者被-Xbootclasspath参数所指定的路径中,并且是虚拟机识别的类加载到内存中。用户在java程序中不可直接应用这个类,如果需要把加载请求委派给这个加载器,直接使用null代替。
(2)扩展类加载器(Extension ClassLoader)
负责加载<JAVA_HOME>\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类。
(3)应用程序类加载器(Application ClassLoader)
这个类是ClassLoader中的getSystemClassLoader方法的返回值,也称为系统类加载器。它负责加载用户目录上的类。
类加载器的层级关系如下所示


image.png