Java,JVM类加载机制

JVM类加载机制

        • 一. 前言
        • 二. 什么是类的加载
        • 三. 类的生命周期及加载过程
          • 3.1 类的生命周期
          • 3.2 类的加载过程
        • 四. 类的加载过程详解
          • 4.1 加载
          • 4.2 验证
          • 4.3 准备(重要)
          • 4.4 解析
          • 4.5 初始化(重要)
        • 五. 整3个例子,直观理解下
          • 5.1 例1
          • 5.2 例2(进阶版)
          • 5.3 例3(启动究极变化形态)
          • 5.4 如何分析一个类的执行顺序
        • 六. 类加载器(简单说下)
        • 七. 双亲委派机制(重点)(从下往上找,从上往下使用)

一. 前言

       贴一下大佬的链接,感觉写的非常好,参考(即抄写)了很多:https://baijiahao.baidu.com/s?id=1636309817155065432&wfr=spider&for=pc
       我们知道,我们写的java文件是不能直接运行的,我们可以在IDEA中直接运行它,这中间参杂了一些列的复杂处理过程。这篇文章,讨论下代码在运行之前的一个环节,叫做类的加载。

二. 什么是类的加载

       在介绍类的加载机制之前,先来看看类的加载机制在整个java程序运行期间处于一个什么样的环节,如下图红色区域。
Java,JVM类加载机制_第1张图片
        从上图可以看出,java的文件通过编译器变成了.class文件,接下来类加载器又将这些.class文件加载到JVM中。其中类加载器的作用就是类的加载。
        即类的加载就是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后再堆区创建一个java.lang.class对象,用来封装类在方法区的数据结构。
        (详情,可见我这篇博客最后的图解:https://blog.csdn.net/weixin_42146993/article/details/106663897)

三. 类的生命周期及加载过程

        类加载器并不需要等到某个类被“首次主动使用”时才加载它,JVM规范允许类加载器在某个类将要被使用时就预先加载它。

3.1 类的生命周期

        一个类型从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期将会经历 加载,验证,准备,解析,初始化,使用和卸载这七个阶段。其中验证,准备,解析这3个部分统称为连接。

3.2 类的加载过程

        类的加载包括了加载,验证,准备,解析,初始化这五个阶段。在这五个阶段中,加载,验证,准备和初始化这四个阶段的发生顺序时确定的,而解析阶段则不一定,它在某些情况下可以在初始化阶段之后开始。另外,这里的几个阶段是按顺序开始,而不是按顺序进行或完成。因为这些阶段通常都是相互交叉混合进行的。
        a) 装载:根据查找路径找到对应的class文件,然后导入。
        b) 验证:验证待加载的class文件的正确性(看看.class文件堆虚拟机有没有害)
        c) 准备:给类中的静态变量分配存储空间
        d) 解析:将符号引用转换成直接引用
        e) 初始化:堆静态变量和静态代码块执行初始化工作

四. 类的加载过程详解

4.1 加载

        加载时类加载的一个阶段,不要混淆
        加载过程完成以下3件事:
        a) 通过一个类的全限定名来获取定义此类的二进制字节流。
        b) 将这个字节流代表的静态存储结构转化为方法区的运行时存储结构。
        c) 在堆中生成一个代表这个类的class对象,作为方法区这个类的各种数据的访问入口。
        相对于类加载的其它阶段而言,加载阶段是可控性最强的阶段,因为程序员可以使用系统的类加载器加载,还可以使用自己的类加载器加载。关于类加载器后面介绍。

4.2 验证

        验证的主要作用就是确保被加载的类的准确性,也是连接阶段的第一步。说白了就是看看加载好的.class文件是不是对虚拟机有害的。验证阶段主要有4个,这里不详细介绍了。

4.3 准备(重要)

        准备阶段主要为类变量分配内存并设置初始值。这些内存都在方法区分配。在这个阶段我们需要注意类变量和初始值这两个关键词:
        a)类变量会分配内存,但是实例变量不好,实例变量主要随着对象的实例化一块分配到java堆中。
        b)这里的初始指指的是数据类型默认值,而不是代码中被现实赋予的值。比如public static int value=1;//在这里准备阶段过后的value值为0,而不是1。赋值为1的动作在初始化阶段。 但是如果是public static final int value=1;准备阶段过后value的值为1,因为final在Java中代表不可改变的意思,因此被final修饰的类变量在准备阶段就会被赋予想要的值。
        关于默认值表如下:

Java,JVM类加载机制_第2张图片

4.4 解析

        解析阶段主要是虚拟机将常量池中的符号引用转化为直接引用的过程。详细解释这里也不作说明了。

4.5 初始化(重要)

        在初始化阶段,主要为类的静态变量赋予正确的初始值,JVM负责对类进行初始化,主要对类变量进行初始化,在java中对类变量进行初始值的设定有两种方式:声明类变量指定初始值,使用静态代码块为类变量指定初始值。

        初始化时类加载机制的最后一步,在这个阶段,java程序代码才开始真正执行。在这个阶段,JVM会根据语句执行顺序对类对象进行初始化,一般来说当JVM遇到下面5种情况的时候会触发初始化:(看一看,知道就好,后面用到的时候能帮助理解)

        a)使用new关键字实例对象的时候、读取或设置一个类的静态字段(被final修饰)的时候,以及调用一个类的静态方法的时候。
        b)使用反射的时候
        c) 初始化某个子类的时候,若其父类还没有被初始化,先触发父类的初始化
        d)虚拟机启动,用户指定一个要执行的主类,主类没有初始化。
        e)java.lang.invoke.MethodHandle实例解析结果为REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,句柄对应的类没有初始化

五. 整3个例子,直观理解下

来源于博文:https://mp.weixin.qq.com/s/YTa0h4FSjqvbKDuGYjHjHw

5.1 例1
class Grandpa
{
     
    static
    {
     
        System.out.println("爷爷在静态代码块");
    }
}
class Father extends Grandpa
{
     
    static
    {
     
        System.out.println("爸爸在静态代码块");
    }
    public static int factor = 25;
    public Father()
    {
     
        System.out.println("我是爸爸~");
    }
}
class Son extends Father
{
     
    static
    {
     
        System.out.println("儿子在静态代码块");
    }
    public Son()
    {
     
        System.out.println("我是儿子~");
    }
}
public class InitializationDemo
{
     
    public static void main(String[] args)
    {
     
        System.out.println("爸爸的岁数:" + Son.factor);    //入口
    }
}

        输出结果是:

爷爷在静态代码块
爸爸在静态代码块
爸爸的岁数:25

        **解析:**首先从程序main()方法开始,使用标准化输出的Son类中的factor类成员变量,但是Son类中没有定义这个类成员变量,于是往父类找,找到Father,在Father中找到了对应的类成员变量,于是触发了Father的初始化,然后Father要初始化,就必须先初始化Father的父类(如初始化第3种情况)。于是乎,我们初始化Grandpa类,输出【爷爷在静态代码块】,再初始化Father类,输出【爸爸在静态代码块】,最后,所以父亲都初始化完成之后,Son类调用父类的静态变量,输出【爸爸岁数:25】。

        那么为什么没有输出【儿子在静态代码块】呢?因为对于静态字段,只有直接定义这个字段的类才会被初始化(执行静态代码块),因此通过其子类来引用父类中定义的静态字段,只会触发父类的初始化而不会触发子类的初始化。即Son类没有初始化。

5.2 例2(进阶版)
class Grandpa
{
     
    static
    {
     
        System.out.println("爷爷在静态代码块");
    }
    public Grandpa() {
     
        System.out.println("我是爷爷~");
    }
}
class Father extends Grandpa
{
     
    static
    {
     
        System.out.println("爸爸在静态代码块");
    }
    public Father()
    {
     
        System.out.println("我是爸爸~");
    }
}
class Son extends Father
{
     
    static
    {
     
        System.out.println("儿子在静态代码块");
    }
    public Son()
    {
     
        System.out.println("我是儿子~");
    }
}
public class InitializationDemo
{
     
    public static void main(String[] args)
    {
     
        new Son();     //入口
    }
}

        输出结果:

爷爷在静态代码块
爸爸在静态代码块
儿子在静态代码块
我是爷爷~
我是爸爸~
我是儿子~

        解析:首先在程序入口这里实例化一个Son对象,因此触发Father,Grandpa的初始化,从而执行对应类种的静态代码块。因此会输出:【爷爷/爸爸/儿子在静态代码块种】。当Son类完成初始化后,便会调用Son类的构造方法,而Son类构造方法的调用又会触发Father,Grandpa的构造方法的调用,最后输出:【我是爷爷/爸爸/儿子】

5.3 例3(启动究极变化形态)

没搞定,直接一个传送门:https://mp.weixin.qq.com/s/YTa0h4FSjqvbKDuGYjHjHw

5.4 如何分析一个类的执行顺序

        通过上述例子,可以看出类的执行顺序如下:
        确定类变量的初始值。在类加载的准备阶段,JVM 会为类变量初始化零值,这时候类变量会有一个初始的零值。如果是被 final 修饰的类变量,则直接会被初始成用户想要的值。
        初始化入口方法。当进入类加载的初始化阶段后,JVM 会寻找整个 main 方法入口,从而初始化 main 方法所在的整个类。当需要对一个类进行初始化时,会首先初始化类构造器,之后初始化对象构造器。
        初始化类构造器。初始化类构造器是初始化类的第一步,其会按顺序收集类变量的赋值语句、静态代码块,最终组成类构造器由 JVM 执行。
        初始化对象构造器。初始化对象构造器是在类构造器执行完成之后的第二部操作,其会按照执行类成员变成赋值、普通代码块、对象构造方法的顺序收集代码,最终组成对象构造器,最终由 JVM 执行。
        如果在初始化 main 方法所在类的时候遇到了其他类的初始化,那么继续按照初始化类构造器、初始化对象构造器的顺序继续初始化。如此反复循环,最终返回 main 方法所在类。

六. 类加载器(简单说下)

        把类加载阶段的 “通过一个类的全限定名来获取描述此类的二进制字节流” 这个动作交给虚拟机之外的类加载器来完成。这样的好处在于,我们可以自选实现类加载器来加载其他格式的类(即自定义类加载器),只要是二进制字节流就行,这就大大增加了加载器的灵活性。
        系统自带的加载器有:启动类加载器,扩展类加载器,应用类加载器。
        其层次关系如下图(箭头即父加载器的意思):

Java,JVM类加载机制_第3张图片

七. 双亲委派机制(重点)(从下往上找,从上往下使用)

        双亲委派机制的goon工作流程:如果一个类加载器收到了类加载的请求,它首先不好尝试去加载这个类,而是把这个请求委派给父类加载器去完成。依次往上。所以所有的加载请求最终都会传送到Bootstrap类加载器种,只要父类加载反馈自己无法加载这个请求时(在它的搜索范围内没有找到所需的类),子加载器才会尝试自己去加载。

        我们写程序在获取ExtClassLoader的父加载器时,会显示null,因为ExtClassLoader的父加载器BootstrapClassLoader是用c++写的。

        这样加载的好处:避免混乱,比如用户自定义了个String的类,在使用字符串时时,在双亲委派机制下,会先往父类找,最终找到Object类,然后一看Object类里有个String类,就使用它;而不是直接就找到我们自定义的String类并使用,因为它会一直往上找父类。

        具体解释如下:例如类java.lang.Object,它存放在rt.jart之中.无论哪一个类加载器都要加载这个类.最终都是双亲委派模型最顶端的Bootstrap类加载器去加载.因此Object类在程序的各种类加载器环境中都是同一个类.相反.如果没有使用双亲委派模型.由各个类加载器自行去加载的话.如果用户编写了一个称为“java.lang.Object”的类.并存放在程序的ClassPath中.那系统中将会出现多个不同的Object类.java类型体系中最基础的行为也就无法保证.应用程序也将会一片混乱.

你可能感兴趣的:(jvm,java)