JVM之Java类加载机制

什么是类加载机制

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

类的生命周期

生命周期简述

类从被加载到虚拟机内存开始,到卸载为止,它的整个生命周期包括:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)、卸载(Unloading)7个阶段,其中验证、准备、解析三个部分被称为连接(Linking),这7个阶段发生的顺序如下:
image
其中加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的,类加载的过程会按照这种顺序执行,而解析阶段不一定,它在某些情况下可以在初始化阶段之后再开始,这是为了支持Java语言的运行时绑定(动态绑定)

类立即初始化的5中情况:

1、遇到new、getstatic、putstatic或invokestatic这4条指令时,如果类没有进行初始化,则需要先触发其初始化。生成这4条指令的Java应用场景是:使用new关键字实例化对象时、读取或设置一个类的静态字段时、调用一个类的静态方法时。

2、使用java.lang.reflect包的方法,对类进行反射调用的时候

3、当初始化一个类的时候,发现其父类还没有进行过初始化,则需要先触发其父类的初始化。

4、当虚拟机启动时,用户需要指定一个要执行的类(包含main方法的类),虚拟机会先初始化这个主类

5、当使用jdk动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getstatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化

类加载过程说明

加载

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

1)通过一个类的全限定名来获取定义此类的二进制字节流

2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构

3)在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口

相对于类加载过程的其它阶段,一个非数组类的加载阶段是开发人员可控性最强的,因为加载阶段既可以使用系统提供的引导类加载器来完成,也可以由用户自定义的类加载器去完成,开发人员可以通过自定义的类加载器去控制字节流的获取方式。

验证

验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。验证阶段大致上会完成4个阶段的校验工作:文件格式校验、元数据校验、字节码校验、符号引用校验。

对于虚拟机的类加载机制来说,验证阶段是一个非常重要的、但不是一定必要(因为对程序运行期没有影响)的阶段。如果所运行的全部代码都已经被反复使用和验证过,那么在实施阶段可以考虑使用-Xverify:none参数关闭大部分的类验证,以缩短虚拟机类加载时间

文件格式校验

该阶段校验的目的是确保输入的字节流能正确地解析并存储到方法区中,格式上符合描述一个Java类型信息的要求。这阶段的验证是基于二进制字节流进行的,只有通过了这个阶段的验证后,字节流才会进入内存的方法区中进行存储,所以后面的3个校验阶段全部是基于方法区的存储结构进行的,不会再直接操作字节流

元数据校验

该阶段的主要目的是对类的元数据信息进行语义校验,保证不存在不符合Java语言规范的元数据信息

字节码校验

该阶段是整个验证阶段最复杂的一个阶段,主要目的是通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。在第二阶段对元数据信息中的数据类型做完校验后看这个阶段将对类的方法体进行校验分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的时间

符号引用校验

最后一个阶段的校验发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在连接的第三阶段——解析阶段中发生。符号引用验证可以看做是对类自身以外的信息进行匹配性校验。符号引用验证的目的是确保解析动作能正常运行,如果无法通过符号引用验证,那么会抛出一个java.lang.IncompatibleClassChangeError异常的子类,如java.lang.IllegalAccessError、java.lang.NoSuchFieldError、java.lang.NoSuchMethodError等。

准备

准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。这个阶段有两个容易混淆的概念需要注意一下:

首先、这个时候进行内存分配的仅包含类变量(被static修饰的变量),不包括实例变量,实例变量是在对象实例化时随着对象一起分配在Java堆中。

其次、这里所说的初始值通常情况下是数据类型的零值,假设一个类变量定义为:
public static int value=123;
那么变量在准备阶段的初始值为0不是123,在初始化阶段变量value的值才是123

最后、特殊情况,如果类字段的字段属性表中存在ConstantValue属性,在准备阶段变量value的值会被初始化为ConstantValue属性所指定的值,将设上面的类变量定义为:
public static final int value=123;
编译时javac将会为value生成ConstantValue属性,在准备阶段变量value就会被初始化为123

解析

该阶段是把类中的符号引用转换为直接引用,解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。符号引用就是一组符号来描述目标,可以是任何字面量。该解析过程又分为4种引用解析过程,类或接口解析、字段解析、类方法解析、接口方法解析

初始化

初始化,为类的静态变量赋予正确的初始值,JVM负责对类进行初始化,主要对类变量进行初始化。在Java中对类变量进行初始值设定有两种方式:

  • [x] 声明类变量是指定初始值。

  • [x] 使用静态代码块为类变量指定初始值。

类什么时候才被初始化:

1)创建类的实例,也就是new一个对象

2)访问某个类或接口的静态变量,或者对该静态变量赋值

3)调用类的静态方法

4)反射(Class.forName())

5)初始化一个类的子类(会首先初始化子类的父类)

6)JVM启动时标明的启动类,即文件名和类名相同的那个类

类的初始化步骤:

1)如果这个类还没有被加载和链接,那先进行加载和链接

2)假如这个类存在直接父类,并且这个类还没有被初始化,那就初始化直接的父类(不适用于接口)

3 ) 假如类中存在初始化语句(如static变量和static块),那就依次执行这些初始化语句。

类加载器

虚拟机设计团队把类加载阶段中的通过一个类的全限定名来获取描述此类的二进制字节流的动作放到了Java虚拟机的外部去实现,以便让应用程序自己决定如何去获取所需要的类,实现这个动作的代码模块称为类加载器

类与类加载器

类的加载器虽然只用于实现类的加载动作,但它在java程序中起到的作用却远远不限于类加载阶段。对于任意一个类,都需要由加载它的类加载器和这个类本身一起确立其在Java虚拟机中的唯一性,每一个类加载器都有一个独立的类名称空间。通俗点就是:比较两个类是否相等,只有这两个类是被同一个类加载器加载的前提下,这个比较才有意义,否则,即便这两个类来源于同一个Class文件,被同一个虚拟机加载,只要加载他们的类加载器不同,那这两个类就必定不相等。

双亲委派模型

image
图中展示了类加载器之间的这种层次关系,称为类加载器的双亲委派模型(Parents Delegation Model)。双亲委派模型要求除了顶层的启动类加载器之外,其余的类加载器都因该有自己的父类加载器。这里的类加载器之间的父子关系不会以继承(Inheritance)的关系来实现,而是使用组合(Composition)关系来复用父加载器的代码

启动类加载器(Bootstrap ClassLoader)

该类加载器负责将java_home\lib目录下或被-Xbootclasspath参数所指定的路径中并且是虚拟机识别的类库加载到虚拟机内存中。启动类加载器无法被Java程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给引导类加载器,那直接使用null代替即可

扩展类加载器(Extension ClassLoader)

该加载器负责加载java_home\lib\ext目录下的或被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器

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

该类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般也称他为系统类加载器。它负责加载用户类路径(ClassPath)下所指定的类库,开发者可以直接使用该类加载器

双亲委派模型的工作流程

如果一个类加载器收到了类加载的请求,它首先不会自己尝试加载这个类,而是把请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的类加载请求最终都会传递给顶层的启动类加载器,只有当父类反馈无法完成这个加载请求时,子加载器才会尝试自己去加载

双亲委派模型的优点

1、Java类随着类加载器具备了带有优先级的层次关系

例如:类java.lang.Object,它存放在rt.jar中,无论哪个加载器要加载这个类都要委派给启动类加载器进行加载,因此Object类在各种类加载器环境中都是同一个类。反之,如果用户自定义了一个称为java.lang.Object类,并放在了程序的ClassPath下,那系统中就会出现多个不同的Object类,应用程序将变得混乱

2、保证Java程序的稳定运行

实现双亲委派的代码都集中在java.lang.ClassLoader的loadClass()方法之中,它会先检查是否已经被加载过,若没有加载则调用父加载器loadClass()方法,如果父容器为空则默认使用启动类加载器作为父加载器。如果父类加载失败,抛出ClassNotFoundException异常后,再调用自己的findClass()方法进行加载

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) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    //如果父类加载器抛出ClassNotFoundException
                    //说明父类加载器无法完成加载请求
                }

                if (c == null) {
                    // 父类加载器无法加载的时候
                    // 再调用本身的findClass()方法进行类加载
                    long t1 = System.nanoTime();
                    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;
        }
    }

参考资料:深入理解Java虚拟机

posted @ 2018-04-01 16:57  IT码客  阅读(240)  评论(0编辑  收藏  举报