JVM之类的加载过程详解

JVM之类的加载过程详解

  1. 类的生命周期概述
    Java中数据类型分为基本数据类型和引用数据类型,基本数据类型有虚拟机预先定义,引用数据类型需要进行类的加载
    (1) 类的整个生命周期如下图所示:JVM之类的加载过程详解_第1张图片
    (2)从类的使用过程看
    JVM之类的加载过程详解_第2张图片
  2. Loading(加载)阶段
    (1)加载完成的操作及二进制的获取
    ① 加载的理解
    所谓加载就是将Java类的字节码文件加载到机器内存中,并在内存中构建出java 类的原型-类模板对象
    所谓类模板对象就是Java类在JVM内存中的一个快照,JVM把从字节码中解析出的常量池,类字段,类方法等信息存储类模板中,这样JVM在运行期便能通过类模板获取Java类中的任意信息,能够对Java类的成员变量进行遍历,也能进行Java方法的调用
    ② 加载完成的操作
    加载阶段,简而言之就是查找并加载类的二进制数据,生成Class的实例
    在加载类时,Java虚拟机必须完成以下3件事:
    通过类的全名,获取类的二进制数据流
    解析类的二进制数据流为方法区内的数据结构(Java类模型)
    创建java.lang.Class类的实例,表示该类型,作为方法区这个类的各个数据的访问入口

    (2)类模型与Class实例的位置
    ① 类模型的位置
    加载的类在JVM中创建相应的类结构,类结构会储存在方法区中(JDK8前是永久代,之后是元空间)
    ② Class实例的位置
    类将.Class文件加载至元空间后,会在堆中创建一个Java.lang.Class 对象,用来封装类似于方法区内的数据结构,该Class对象是在加载类的过程中创建的,每个类都有对应的Class类型对象
    ③ 如图所示
    JVM之类的加载过程详解_第3张图片
    外部可以通过访问代表Order类的Class对象来获取Order的类数据结构
    Class类的构造方法是私有的,只有JVM可以创建
    java.lang.Class实例是访问类型元数据的接口,也是实现反射的关键数据,入口,通过Class类提供的接口,可以获得目标类所关联的 .class文件中具体的数据结构,方法字段等信息
    ⑤ 数组类的加载
    因为数组类本身并不是由类加载器负责创建,而是由JVM运行时根据需要而直接创建的,但数组的元素类型仍然需要依靠类加载器去创建。
    创建数组类的过程:
    如果数组的元素类型是引用类型,那么就遵循定义的加载过程递归加载和创建数组的元素类型,数组类的可访问性由元素类型的可访问性决定,否则数组类的可访问性将被缺省定义为public
    JVM使用指定的元素类型和数组维度来创建新的数组类
  3. Linking(链接)阶段
    (1)验证环节(Verification)
    该阶段的目的是保证加载的字节码是合法合理并符合规范的
    大体上Java虚拟机需要做以下检查,如图所示:
    JVM之类的加载过程详解_第4张图片
    整体说明:
    验证的内容则涵盖了类数据信息的格式验证,语义检查,字节码验证,以及符号引用验证
    ① 其中格式验证会和加载阶段一起执行,验证通过之后,类加载器才会成功将类的二进制数据信息加载到方法区中
    格式验证之外的验证操作将会在方法区中进行
    具体说明:
    格式验证:是否以魔数0xCAFEBABE开头,主版本和副版本号是否在当前Java虚拟机的支持范围内,数据中每一个项是否都拥有正确的长度
    语义检查:是否所有的类都有父类的存在;是否一些被定义为final的方法或者接口方法;非抽象类是否实现所有抽象方法或者接口方法;是否存在不兼容的方法
    字节码验证:在字节码的执行过程中,是否会跳转到一条不存在的指令;函数的调用是否传递了正确类型的参数;变量的赋值是否给了正确的数据类型
    栈映射帧就是在这个阶段,用于检查在特定的字节码处,其局部变量表和操作数栈是否有着正确的数据类型,该过程只能尽可能的检查出可以预知的明显的问题,如果该阶段无法通过检查,虚拟机也不会正确装载这个类
    通过以上三个阶段,以及排除了文件格式错误,语义错以及字节码的不正确性,但依然不能确保类是没有问题的
    符号引用验证:Class文件在其常量池会通过字符串记录自己将要使用的其他类或者方法,在验证阶段,虚拟机会检查这些类或者方法确实是存在的,并且当前类有权限访问这些数据,如果一个需要使用类无法在系统中找到,会抛出NoClassDefFoundError,如果一个方法无法被找到,则会抛出NoSuchMethodError此阶段在解析环节才会执行
    (2)准备环节(Preparation)
    准备环节,简而言之就是为类的静态变量分配内存,并将其初始化为默认值
    JVM之类的加载过程详解_第5张图片
    注意:
    这里不包含基本数据类型的字段用staitic final修饰的情况,因为final在编译时就会分配,准备阶段会显式赋值
    ② 这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量会随着对象一起分配在Java堆中
    ③ 这个阶段不会像初始化阶段中那样会有初始化或者代码被执行
    (3)解析环节(Resolution)
    解析阶段,简而言之就是将类,接口,字段和方法的符号引用转为直接引用
    ① 具体描述
    符号引用就是一些字面量的引用,和虚拟机的内部数据结构和内存布局无关,在Class类文件中,通过常量池进行大量的符号引用,但是在程序实际运行时,只有符号引用是不够的,系统需要做的其具体的位置
    例如:输出操作System.out.println()对应的字节码
    JVM之类的加载过程详解_第6张图片
    ② 以方法为例,Java虚拟机为每个类都准备了一张方法表,将其所有的方法都列在表中,当需要调用一个类的方法的时候,只要知道这个方法在方法表中的偏移量就可以直接调用该方法,通过解析操作,符号引用就可以转变为目标方法在类中方法表中的位置,从而使得方法被成功调用
    ③ 小结
    所谓解析就是将符号引用转为直接引用,也就是得到类,字段,方法在内存中的指针或者偏移量,因此,可以说,如果直接引用存在,那么可以肯定系统中赶存在该类,方法或者字段,但只存在符号引用,不能确定系统中一定存在该结构
  4. 初始化阶段(initialization)
    为类的静态变量赋予正确的初始值
    (1)具体描述
    如果前面的阶段都没问题,类才会开始执行Java字节码,才开始真正的执行类中定义的Java程序代码
    初始化阶段的重要工作是执行类的初始化方法:()方法
    该方法仅能有Java编译器生成并由JVM调用,程序开发者无法自定义一个同名的方法,更无法直接在Java程序中调用该方法,虽然该方法也是有字节码指令所组成
    ② 它是由类静态成员的赋值语句以及static语句块合并产生
    (2)说明
    ① 在加载一个类之前,虚拟机总会试图加载该类的父类,因此父类的总是在子类之前被调用,也就是说,父类的static块优先级高于子类,由父及子,静态先行
    ② Java编译器并不会为所有的类都产生()初始化方法,例如
    一个类中没有声明任何的类变量,也没有静态代码块时
    一个类中声明类变量,但是没有明确使用类变量的初始化语句以及静态代码块初始化操作时

    JVM之类的加载过程详解_第7张图片
    JVM之类的加载过程详解_第8张图片
    (3)()方法的线程安全
    对于()方法的调用,也就是类的初始化,虚拟机会在内部确保其多线程环境中安全性
    虚拟机会保证一个类的()方法在多线程环境中被正确的加锁,同步,如果多个线程同时去初始化一个类,那么,只会有一个线程去执行这个类的()方法,其他线程都需要阻塞等待,直到活动线程执行()方法完毕
    正是因为函数()带锁线程安全的,因此,如果在一个类的()方法中有耗时很长的操作,就可能造成多个线程阻塞,引起死锁
    如果之前的线程成功加载了类,则等在队列中线程就没有机会在执行方法了,那么,当需要使用这个类时,虚拟机会直接返回给它已经准备好的信息
  5. 类的主动使用和被动使用
    (1) 主动使用
    ① 当创建一个类的实例时,比如使用new关键字,或者通过反射,克隆,反序列化
    ② 当调用一个类的静态方法时,即当使用了字节码invokestatic指令
    ③ 当使用类,接口的静态字段时(final特殊考虑),比如使用getstatic或putstatic指令
    ④ 当使用java.lang.reflect包中的方法反射类的方法时,比如Class.forName()
    ⑤ 当初始化子类时,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化
    补充说明:
    当Java虚拟机初始化一个类时,要求它的所有父类都已经被初始化,但是这条规则不适用于接口
    在初始化一个类时,并不会先初始化它所实现的接口
    在初始化一个接口时,并不会先初始化它的父接口
    因此,一个父接口不会因为它的子接口或者实现类的初始化而初始化,只有当程序首次使用特定接口的静态字段时才会导致该接口的初始化
    ⑥ 如果一个接口定义了default方法,那么直接实现或间接实现该接口的类的初始化,该接口要在其之前被初始化
    ⑦ 当虚拟机启动时,用户需要指定一个要执行的主类,虚拟机会先初始化这个主类
    ⑧ 当初次调用MethodHandle实例时,初始化该MethodHandle指向的方法所在的类
    代码说明:
/**

 * 

* 4. 当使用java.lang.reflect包中的方法反射类的方法时。比如:Class.forName("com.atguigu.java.Test") * 5. 当初始化子类时,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。 * 6. 如果一个接口定义了default方法,那么直接实现或者间接实现该接口的类的初始化,该接口要在其之前被初始化。 * 7. 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。 * 8. 当初次调用 MethodHandle 实例时,初始化该 MethodHandle 指向的方法所在的类。 * (涉及解析REF_getStatic、REF_putStatic、REF_invokeStatic方法句柄对应的类) *

*

* 针对5,补充说明: * 当Java虚拟机初始化一个类时,要求它的所有父类都已经被初始化,但是这条规则并不适用于接口。 * >在初始化一个类时,并不会先初始化它所实现的接口 * >在初始化一个接口时,并不会先初始化它的父接口 * 因此,一个父接口并不会因为它的子接口或者实现类的初始化而初始化。只有当程序首次使用特定接口的静态字段时, * 才会导致该接口的初始化。 */ public class ActiveUse3 { static{ System.out.println("ActiveUse3的初始化过程"); } @Test public void test1() { try { Class clazz = Class.forName("com.java1.Order"); } catch (ClassNotFoundException e) { e.printStackTrace(); } } @Test public void test2() { System.out.println(Son.num); } @Test public void test3(){ System.out.println(CompareC.NUM1); } @Test public void test4() { System.out.println(Son.num); } public static void main(String[] args) { System.out.println("hello"); } } class Father { static { System.out.println("Father类的初始化过程"); } } class Son extends Father implements CompareB{ static { System.out.println("Son类的初始化过程"); } public static int num = 1; } interface CompareB { public static final Thread t = new Thread() { { System.out.println("CompareB的初始化"); } }; public default void method1(){ System.out.println("你好!"); } } interface CompareC extends CompareB { public static final Thread t = new Thread() { { System.out.println("CompareC的初始化"); } }; public static final int NUM1 = new Random().nextInt(); }

(2)被动使用
除了以上的情况外,其他都是被动使用,被动使用不会引起类的初始化,也就是说,并不是在代码中出现的类,就一定会被加载或初始化,如果不符合主动使用的条件,类就不会初始化
代码说明:

/**
 *
 * 关于类的被动使用,即不会进行类的初始化操作,即不会调用()
 *
 * 1. 当访问一个静态字段时,只有真正声明这个字段的类才会被初始化。
 *     > 当通过子类引用父类的静态变量,不会导致子类初始化
 * 2. 通过数组定义类引用,不会触发此类的初始化
 * 3. 引用常量不会触发此类或接口的初始化。因为常量在链接阶段就已经被显式赋值了。
 * 4. 调用ClassLoader类的loadClass()方法加载一个类,并不是对类的主动使用,不会导致类的初始化。
 *
 * 说明:没有初始化的类,不意味着没有加载!
 */
public class PassiveUse1 {
    @Test
    public void test1(){
        System.out.println(Child.num);
    }

    @Test
    public void test2(){
        Parent[] parents = new Parent[10];
        System.out.println(parents.getClass());
        System.out.println(parents.getClass().getSuperclass());

        parents[0] = new Parent();
        parents[1] = new Parent();
    }
     @Test
    public void test3(){
        try {
            Class clazz = ClassLoader.getSystemClassLoader().loadClass("com.java1.Person");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

class Parent{
    static{
        System.out.println("Parent的初始化过程");
    }

    public static int num = 1;
}

class Child extends Parent{
    static{
        System.out.println("Child的初始化过程");
    }
}
  1. 类的使用(Using)
    在程序中访问和调用它的静态类成员信息(比如静态字段,静态方法),或者使用new关键字为其创建对象实例
  2. 类的卸载(Unloading)
    (1)启动类加载器加载的类型在整个运行期间是不可能被卸载的
    (2)被系统类加载器和扩展类加载器加载的类型在运行期间不太可能被卸载,因为西永类加载器实例或扩展类的实例基本上在整个运行期间总能直接或者间接的访问的到,器达到unreachable的可能性极小
    (3)被自定义的类加载器实例加载的类型只有在很简单的上下文环境汇总才能卸载,而且需要借助强制调用虚拟机的垃圾收集功能才能做到,被加载的类型在运行期间也是机会不太可能被卸载的
    (4)具体例子JVM之类的加载过程详解_第9张图片
    说明:
    ① loader1变量和obj变量间接应用代表Sample类的Class对象,而objClass变量则直接引用它
    ② 如果运行过程中,将三个引用变量置为null,此时Sample对象结束生命周期,MyClassLoader对象结束生命周期,代表Sample类的Class对象也结束生命周期,Sample类再方法区内的二进制数据被卸载
    ③ 当再次有需要时,会检查Sample类的Class对象是否存在,如果存在会直接使用,不在重新加载,如果不存在Sample类会被重新加载,在Java虚拟机的堆区中会生成一个新的代表Sample类的Class实例(可以通过查看哈希吗判断是否是同一个对象)

你可能感兴趣的:(JVM学习笔记,JVM,类的加载过程)