• Java虚拟机--类加载的过程


    1. 类加载

    类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载这7个阶段,类加载包含了前五个,具体如图:

     一个 .java 文件在编译后会形成一个或多个class文件(若有内部类,则编译后会产生多个.class文件),但这些class文件中的信息,只有被加载到虚拟机中才能被运行和使用。

    虚拟机把类的数据从class文件加载到内存,并对class文件中的数据进行校验、转换、解析、初始化等操作后,最终形成可以被虚拟机识别并使用的过程就叫做“虚拟机的类加载”.

    1.1 加载

    “加载”是类加载中的一个过程,并且是通过类加载器来完成的。但是什么时候会进行“加载”,虚拟机规范中并没有强制约束,而是交给具体的虚拟机实现来完成。

    在此阶段中,虚拟机需要完成以下步骤:

    • 将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内。
    • 在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。

    在虚拟机中,当程序主动使用某个类时,如果该类还未被加载到内存中,JVM虚拟机会进行加载操作,直至初始化完成。

    1.2 验证

    在完成加载阶段后,JVM虚拟机开始验证阶段,此阶段的目的很简单,很纯粹,就是为了保证class文件中的内容符合虚拟机的规范要求,在实际运行时不会威胁到虚拟机自身的安全。

    1.2.1 文件格式验证

    文件格式验证,就是验证class文件字节流是否符合class文件格式规范。其中包含字节流文件是否以魔数0xCAFEBABE开头; 主、次版本号是否在虚拟机处理范围之内; class常量池中
    是否有不支持的常量类型....等等。该阶段验证的主要目的是为了保障输入的class文件字节流能正确地解析, 格式上符合要求。在通过该阶段验证后, class文件字节流会进入内存方法区
    中存储,后续的三种验证都是基于方法区的.

    1.2.2 元数据验证

    元数据验证, 是对字节码描述的信息进行语义化分析, 保证其描述的内容符合java语言的语法规范。验证点如下: 这个类是否有父类; 这个类的父类是否继承了不允许被继承的
    类(final修饰的类); 这个类是否实现了其父类或者接口中所要求实现的所有方法(抽象类可不实现); 类中的字段、方法是否与父类产生矛盾(不符合规则的重载、覆盖了父类中的final字段
    等). 该阶段的验证类似于我们编译期间的校验, 对java语法上的检查.

    1.2.3 字节码验证

    通过数据流和控制流分析, 确定程序语义是合法的、符合逻辑的.

    1.2.4 符号引用验证

    主要来验证一些引用的真实性与可行性, 比如代码里面引用了其他类, 这时就需要去检测一下类是否存在. 或者说代码中访问了其他类的一些属性, 就对那些属性是否可以访问进行检验.
    如果无法通过符号引用验证, 那么就会抛出异常. 我们在编写程序时经常会遇到 java.lang.IllegalAccessError、java.lang.NoSuchFieldError、java.lang.NoSuchMethodError,这就是符
    号引用验证所产生的. 对于虚拟机类加载阶段来说, 符号引用验证可以省略, 如果你所运行的代码都已经被反复使用和验证过, 那么你可以考虑省略此步骤, 虚拟机也提供了一个参数供我
    们修改, 使用 -Xverify:none 参数可关闭验证措施, 以缩短类加载阶段耗时.

    1.3 准备

    验证阶段完成后就到了准备阶段。准备阶段,说直白点就是对类变量设置初始值。而准备阶段的初始值,是数据类型的零值。例如:

    public static int value = 123;
    

    对于变量value来说,变量value的初始值是0,并不是123,123在初始化阶段才会被赋值给value。

    private static final int VALUE= 123;

    值得注意的是初始值也会存在一些特殊情况,如果类的静态变量被final所修饰,在编译阶段会为VALUE生成ConstantValue属性,在准备阶段虚拟机会根据ConstantValue属性将VALUE赋值为123。

    1.4 解析

    解析阶段主要是将常量池内的符号引用替换为直接引用的过程。Java在编译阶段,会将.java文件编译成.class文件,在生成的.class文件中,static修饰的静态变量就是我们常说的符号引用,但是在
    编译阶段该符号引用并不知道引用类的实际内存地址(虚拟机还没运行)。直到解析阶段,虚拟机加载了该类才能真正解析到具体的内存地址。

    1.5 初始化

    初始化,是类加载阶段的最后一步。在这步中,才真正开始执行类中定义的Java代码。

    在Java虚拟机规范中,如果有以下几种情况时必须立即对类进行“初始化”操作:

    1. 遇到new、getstatic、putstatic或invokestatic这四条字节码指令时,如果类没有进行过初始化,则需要先对其进行初始化。生成这四条指令的最常见的Java代码场景是:使用new关键字实例化
      对象的时候;读取或设置一个类的静态字段(被final修饰除外)的时候;调用一个类的静态方法的时候。
    2. 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
    3. 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
    4. 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
    5. 当一个接口中定义了JDK 8新加入的默认方法(被default关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化。

    在之前的准备阶段,类中定义的static静态变量已经被赋过一次零值。而在初始化阶段,则会调用类构造器<clinit>来完成初始化操作,为静态变量赋原始值。

    此处,需要注意的是类构造器<clinit>和类构造方法<init>区别。

    类构造方法<init>就不用多说了,每创建一次对象,都会自动调用一次。

    类构造器<clinit>类似于一个无参的构造函数,只不过该函数是静态修饰,只会初始化静态所修饰的代码。

    类构造器<clinit>是由编译器自动收集类中的所有静态变量的赋值动作和静态语句块static{}中的代码合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的。

    类构造器<clinit>和类构造方法<init>不同的是,类构造器不需要显式地调用父类类构造器,虚拟机会保证子类类构造器执行之前,父类类构造器已经执行完毕。

    此外,虚拟机会保证类构造器<clinit>在多线程环境中被正确执行,如果有多个线程同时去初始化一个类,那么只会有一个线程去执行该类的类构造器<clinit>,其他线程都需要阻塞的等待。并且,类构造器<clinit>只会执行一次。
  • 相关阅读:
    1856: [Scoi2010]字符串(Catalan数)
    11.6NOIP模拟赛
    bzoj1257[CQOI2007]余数之和(除法分块)
    11.5NOIP模拟赛
    bzoj1048(记忆化搜索)
    置顶公告+更新日志
    CF585F Digits of Number Pi
    [SHOI2007]善意的投票
    [HEOI2015]最短不公共子串
    树形背包复杂度+P3177 [HAOI2015]树上染色
  • 原文地址:https://www.cnblogs.com/xxoome/p/13215873.html
Copyright © 2020-2023  润新知