从JVM层面谈一谈类的加载过程

26 篇文章 6 订阅
17 篇文章 0 订阅

从JVM层面谈一谈类的加载过程

之前在Java内存区域详解(万字总结!一篇入魂!点赞收藏!)这篇文章中详细讲解了Java内存区域,我们下面再放一张图,回顾一下Java内存区域都划分为了哪几部分

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4FX6Y5x4-1631870473991)(C:\Users\Jian\AppData\Roaming\Typora\typora-user-images\image-20210916230054934.png)]

通过JVM整体架构来看,Java内存区域,也就是运行时数据区,是JVM的核心。我们知道Java内存区域是用来存放程序运行过程中产生的数据的区域,如果我们要向将Java程序运行起来,首先必须把编译出来class文件加载到内存中,而负责加载的就是JVM中的类加载子系统,它负责将编译好的class文件加载到内存中。这篇文章,我们就来讲一讲类加载子系统是怎么把一个类加载到内存中的。

我们先来细化一下类加载子系统,看看类的加载过程都分为哪几个阶段

  • Loading加载阶段
  • Linking链接阶段
  • Initialization初始化阶段

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bJl0r6k9-1631870474000)(C:\Users\Jian\AppData\Roaming\Typora\typora-user-images\image-20210916234328913.png)]

加载阶段

首先你得清楚这个加载的概念,所谓的加载,就是将Java类编译的class字节码文件加载到机器内存中,并在内存中构建出Java类的原型,即类模板对象。那么你可能想问这个类模板对象又是什么东西?类模板它其实就是Java类在JVM内存中的一个快照,是一个存放在方法区中用来描述这个的数据结构。JVM将从class字节码文件中解析出的常量池、类字段、类方法等相关信息存储到类模板中,这样JVM在运行期间就可以能够通过类模板来获取Java类中的任意信息,能够对Java类的成员变量进行变量,也能够调用Java的方法。我们说Java语言万物皆对象,我们平时使用一个类,都是创建这个类的对象来进行使用,而这个类其实也可以看做一个对象!这个类相当于类模板的对象,但是我们说了类模板是存放在方法区中的快照,它只是描述了这个类的所有相关信息,不能直接使用类模板来获取类的相关信息,而真正可以使用的是在堆中创建的这个类的Class实例对象。Class对象用来封装位于方法区中类模板的数据结构,Class对象实在加载类的过程中创建的,每一个类都对应唯一一个Class类型的对象,既然Class是在堆中的对象,我们就可以使用这个类的Class对象,来获取想要的类的相关信息,比如都有哪些成员变量啊,可以使用的方法啊,等等。还记不记得Java中的反射机制,反射机制就是基于类模板对象实现的,如果JVM没有将Java类的有关信息存储起来,那么JVM在运行期间也就无法实现反射。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ll4EyzDa-1631870474004)(C:\Users\Jian\AppData\Roaming\Typora\typora-user-images\image-20210916233908408.png)]

所以在整个加载阶段,JVM需要完成:

  1. 通过类的全名,从class字节码文件中获取二进制数据流
  2. 解析类的二进制数据流为方法区的数据结构,即Java类模板
  3. 在堆区创建Class对象,表示该类型,同时作为方法区这个类的各种数据的访问入口

以上过程需要类加载子系统作为桥梁来完成二进制数据到Class对象的转换,这里我们所说的二进制数据其实不只是来自class字节码文件,只要是类加载子系统读取的字节码符合JVM规范即可。

二进制数据流可以来源于:

  • jar包、zip包等数据包
  • 事先存放在数据库中的类的二进制数据
  • 使用类似于http之类的协议通过网络传输二进制数据流

在获取到类的二进制数据信息之后,JVM就会通过类加载子系统读取并处理这些数据,在方法区创建这个类对应的数据结构,并在堆空间中创建这个类的Class对象来封装方法区中的数据结构,通过Class对象就可以访问到这个类的相关信息。

需要注意的是数组类的加载,因为数组类本身并不是由类加载器负责创建,而是由JVM在运行时根据需要而直接创建的,但是数组的元素类型仍然需要依赖类加载器去创建。

链接阶段

当类加载到系统内存中之后,就开始了链接阶段。在整个链接阶段又被分为了3个环节:验证、准备、解析。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZWHy7Y5f-1631870474008)(C:\Users\Jian\AppData\Roaming\Typora\typora-user-images\image-20210917093350902.png)]

  • 验证环节

    验证目的是保证加载的字节码是合法的,是符合JVM规范的,不能随便一个二进制文件就能被JVM加载。

    • 格式检查

      格式的检查实际上是和加载阶段一起执行的,只有验证通过之后,类加载器才会成功将类的二进制数据信息加载到方法区中。格式之外验证操作是在方法区中进行的。我们说过class字节码文是由规定的格式的,不符合格式的字节码是无法被JVM识别并解析的,那么如何判断一个字节码文件符合规范呢?实际上符合规范的字节码文件都是以0xCAFEBABE开头的,称为魔数,是用来标识这是一个可被JVM加载的Class字节码文件。由于JDK版本更新迭代很快,有些类可以是新的加,有些类可能已经过期被删除了,所以考虑到版本的兼容性,还会检查字节码文件中的主版本号和副版本号是否在当前JVM的支持范围内。字节码文件中是一大长串儿的二进制数据,不像我们写文章一样,用标点符号来区分每一段话,在字节码文件中每一个数据项都是采取长度+数据来分割每一个数据项的,所以格式检查还要检查每一项数据中长度是否正确,否则就会导致数据分割混乱。

    • 语义检查

      所谓的语义检查,就是看看这段二进制数据是想干什么的,表达的意思是什么。

      JVM规定了一些行为规范,比如:

      • 是否所有的类都有父类
      • 是否一些被定义为final的方法或者类被重写或继承了
      • 非抽象类是否实现了所有抽象方法或者接口方法
      • 是否在不兼容的方法
    • 字节码验证

      JVM还会视图通过对字节码流的分析,判断字节码是否可以被正确的执行,比如:

      • 在字节码的执行过程中,是否会跳转到一条不存在的指令
      • 函数的调用是否传递了正确类型的参数
      • 变量的赋值是否符合数据类型
    • 符号引用验证

      在排除了文件格式错误、语义错误以及字节码本身的错误之后,仍然不能保证加载的类没有问题,此时还会进行符号引用的检查。Class字节码文件在常量池会以字符串的形式记录自己将要使用的其它类或者方法,因此,JVM就需要检查这些类和方法是否存在,是否当前类有权限访问这些类和方法。实际上这是在检查符号引用是否能转换为直接引用,这个检查在解析环节才真正的执行。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-v5SthI6S-1631870474009)(C:\Users\Jian\AppData\Roaming\Typora\typora-user-images\image-20210917100527537.png)]

  • 准备环节

    通过了验证环节,说明加载的这个类没有什么问题。我们说静态变量是随着类的加载而加载的,所以接下来准备环节就是为类的静态变量分配内存,并将其初始化为默认值。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yG5Ebp74-1631870474012)(C:\Users\Jian\AppData\Roaming\Typora\typora-user-images\image-20210917100909658.png)]

    准备环节不会为实例对象的变量分配内存并初始化,只有类变量才会在方法区中分配内存并初始化,而实例变量是随着对象一起分配到堆空间中。在准备环节并不会像初始化阶段中那样会有初始化代码执行,这里所说的初始化只是赋值了变量类型的默认值。

    需要注意的是:

    • 在准备环节分配内存并初始化默认值,**不包含基本数据类型用Static final修饰的情况,**因为final修饰的变量在编译的时候就已经分配内存了,准备环节只是会显式赋默认值。
    • 非final修饰的静态变量会在准备环节分配内存并初始化为默认值,然后在初始化环节中的< clint >类的初始化方法中显式赋值
    • 基本数据类型、String字面量的静态常量,在编译阶段中就会分配内存并赋默认值,然后在准备环节就会显式赋值,不需要在初始化环节再进行显式赋值。
    • 对于引用类型的静态常量(new String(“XXX”))都是在初始化环节中的< clint >类的初始化方法中进行显式赋值的。
    • 静态代码块的赋值操作是在初始化环节中的< clint >类的初始化方法中进行的显式赋值
  • 解析环节

    在准备环节结束之后,就进入到了解析环节,而解析环节就是将类、接口、字段、方法的符号引用转为直接引用

    我们说过class字节码文件中的常量池保存的都是一些符号引用,这些符号引用只是一些字面量的引用,并没有什么实际作用,和JVM内部的数据结构以及内存布局都没有什么关系。但是在程序实际运行过程中,只有符号引用并不会起到什么作用,我们需要根据符号引用找到真正在程序运行中起到实质性作用的引用对象,比如一些字段、接口、方法等。以类的方法为例,JVM为每一个类都准备了一张方法表,将类的所有方法都保存在方法表中,当需要调用一个类的方法的时候,只要知道这个方法在方法表中的偏移量就可以直接调用该方法,通过解析操作,符号引用就可以转换为目标方法在类中方法表中的位置,从而使得方法被成功调用。

    解析环节往往在JVM执行完初始化阶段之后再执行!

初始化阶段

类的初始化阶段是类完成加载的最后一个阶段,这个阶段就是**为了给类的静态变量赋初始值。**到了初始化阶段,才真正开始执行类中定义的Java程序代码。

初始化阶段最重要的工作就是执行类的初始化方法< clinit >方法。< clinit >方法只能由Java编译器生成并且由JVM调用,它是由类静态成员的赋值语句以及static语句块合并产生的。在加载一个类之前,JVM会先试图加载这个类的父类,因此父类的< clinit >方法总是在子类的< clinit >方法之前被调用,即父类的静态代码块优先级高于子类的静态代码块。

Java编译器并不是会为所有类都产生< clinit >初始化方法,比如以下几种情况就不会产生初始化方法

  • 一个类中没有声明任何的类变量,也没有任何静态代码块
  • 一个类中声明的了类变量,但是没有明确使用类变量的初始化语句以及静态代码块来执行初始化操作
  • 一个类中包含static final修饰的基本数据类型、String字面量的字段,这些字段初始化语句采用编译时常量表达式,直接赋值常量,而非调用方法,通常在链接阶段就完成了。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SnbmInb1-1631870474013)(C:\Users\Jian\AppData\Roaming\Typora\typora-user-images\image-20210917152359175.png)]

所以说对于static final修饰的类变量,并且显式赋值中不涉及到方法或者构造器调用的基本数据类型或String字面量类型的显式赋值,是在链接阶段的准备环节进行的,其它显式赋值中涉及到方法或者构造器调用的基本数据类型、引用数据类型的显式赋值,都会产生< clinit >初始化方法

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uYlyIRei-1631870474015)(C:\Users\Jian\AppData\Roaming\Typora\typora-user-images\image-20210917153429821.png)]

对于< clinit >方法的调用,JVM会在内部保证多线程操作的安全性,< clinit >方法是隐式加锁,没有sync描述符,如果多个线程同时去初始化一个类的时候,那么只会有一个线程对于< clinit >方法加锁成功,其它线程都需要阻塞等待。如果之前已经成功加载了一个类,也就是说已经初始化过了,那么其它线程就不会再执行< clinit >方法了,需要使用这个类的时候,JVM会直接返回这个类的相关信息。

我们说初始化阶段可以说是类的加载最后一个过程,加载完成的类就可以使用了,Java程序对类的使用分为主动使用和被动使用两种

  • 主动使用

    类只有在首次使用的时候才会被加载。JVM不会无条件的加载一个类,当一个类或者接口在首次主动使用前,必须要进行初始化。主动使用一个类时,会调用< clinit >初始化方法进行类的初始化操作。

    主动使用包括以下几种情况:

    1. 当创建一个类的实例时,比如使用new关键字或者通过反射、克隆、反序列化等
    2. 当调用类的静态方法时
    3. 当使用类、接口的静态字段时
    4. 当使用反射机制获取类的方法时
    5. 当初始化子类,父类还没有初始化的话,必须先初始化父类。但是初始化一个类时,并不会初始化它实现的接口,也不会初始化它的父接口,如果接口中定义了default方法则会初始化这个接口。
    6. JVM启动是会先初始化包含main方法的类
  • 被动使用

    **被动使用不会引起类的初始化!**也就是说被动使用一个类,不会调用< clinit >方法进行类的初始化操作。所以在代码中出现的类并不是都会加载,只有那些需要的的使用或者等待使用时,才会主动加载进来。

    以下几种情况都属于被动使用,并不会导致类的初始化:

    1. 当访问一个静态字段时,只有真正证明这个字段的类才会被初始化,也就是说通过子类引用父类的静态变量,不会导致子类的初始化
    2. 通过数组定义类引用,不会触发这个类的初始化
    3. 引用常量不会触发这个类或者接口的初始化,因为常量在链接阶段的准备环节就已经被显式赋值了。
    4. 调用ClassLoader类的loadClass()方法加载一个类,并不是对类的主动使用,所以不会导致类的初始化。

类的使用

经过了上面的加载、链接(验证/准备/解析)、初始化三大阶段,可以说是一个类就被JVM成功的加载到系统中了,也就是说Java程序中可以正常使用这个加载成功的类了。加载成功后就可以访问这个类的静态变量、静态方法等相关信息,或者使用new关键字创建出这个类的实例对象来使用。

类的卸载

我们之前说过,一个类的正常的加载过程是由类加载子系统通过类加载器加载到系统中的,只有正常加载完成这个类,才可以使用这个类创建出该类的实例对象,那么类、类加载器、类的实例对象之间的引用关系是什么样子的呢?

在类加载器的内部实现中,使用一个Java集合来存放这个类加载器加载过的类的引用,而且Class对象总是会引用它的类加载器,因此我们可以通过调用getClassLoader()方法来知道这个类是由哪个类加载器加载进来的。一个类加载器知道它自己都加载过哪些类,而一个类的Class对象知道它是由哪个类加载器加载进来的,所以Class对象和类加载器之间是双向关联的关系

一个类的实例对象总是引用代表这个类的Class对象,实例通过getClass()方法可以获取到这个Class对象,进而获取到这个实例所属类型的相关信息。每一个类只有唯一一个Class对象。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-61jVoyel-1631870474018)(C:\Users\Jian\AppData\Roaming\Typora\typora-user-images\image-20210917170440877.png)]

当一个类被加载、链接、初始化之后,我们说这个类的生命周期也就开始了,当这个类的Class对象不再被引用的时候,也就代表这个类的生命周期也就结束了。所以说一个类什么时候结束生命周期,取决于代表这个类的Class对象什么时候不再被引用。

我们说过Class对象和类加载器之间双向引用,并且这个类的实例对象也引用Class对象,启动类加载器不可能被卸载,扩展类、系统类加载器也很难被卸载,只有自定义的类加载器才有极小可能被卸载,但是一般来说类加载器都不会被卸载的,因为很有可能影响到程序的运行,进而我们也就可以推断出Class对象想要不被任何对象引用也很难。

而判定一个类不再使用要满足以下3个条件:

  1. 该类的所有实例都已经被回收(GC垃圾回收,这个很容易)
  2. 加载该类的类加载器已经被回收(很难)
  3. 该类对应的Class对象没有任何地方被引用(也很难)

**所以说,卸载一个已经加载的类的概率是很小的!**所以说GC垃圾回收一般不会回收方法区!

综上所述,类的加载过程主要有三大阶段:加载——》链接——》初始化

而类的完整的生命周期:三大阶段+类的使用和卸载

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zaGHylI2-1631870474019)(C:\Users\Jian\AppData\Roaming\Typora\typora-user-images\image-20210917172007688.png)]

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值