关于后端:深入浅出JVM三之HotSpot虚拟机类加载机制

42次阅读

共计 6018 个字符,预计需要花费 16 分钟才能阅读完成。

HotSpot 虚拟机类加载机制

类的生命周期

什么叫做类加载?

类加载的定义: JVM 把形容类的数据从 Class 文件加载到内存, 并对数据进行校验, 解析和初始化, 最终变成能够被 JVM 间接应用的 Java 类型(因为能够动静产生, 这里的 Class 文件并不是具体存在磁盘中的文件, 而是二进制数据流)

一个类型被加载到内存应用 到 完结卸载出内存, 它的生命周期分为 7 个阶段: 加载 -> 验证 -> 筹备 -> 解析 -> 初始化 -> 应用 -> 卸载

其中重要阶段个别的 开始程序 : 加载 -> 验证 -> 筹备 -> 解析 -> 初始化

验证, 筹备, 解析合起来又称为连贯所以也能够是 加载 -> 连贯 -> 初始化

留神这里的程序是个别的开始程序, 并不一定是执行完某个阶段完结后才开始执行下一个阶段, 也能够是执行到某个阶段的中途就开始执行下一个阶段

还有种非凡状况就是 解析可能在初始化之后(因为 Java 运行时的动静绑定)

根本数据类型不须要加载, 援用类型才须要被类加载

类加载阶段

接下来将对这五个阶段进行具体介绍

Loading

加载

  • 加载的作用
  1. 通过这个类的全限定名来查找并加载这个类的二进制字节流

    • JVM 通过文件系统加载某个 class 后缀文件
    • 读取 jar 包中的类文件
    • 数据库中类的二进制数据
    • 应用相似 HTTP 等协定通过网络加载
    • 运行时动静生成 Class 二进制数据流
  2. 将这个类所代表的动态存储构造 (动态常量池) 转化为办法区运行时数据结构(运行时常量池)
  3. 在堆中创立这个类的 Class 对象, 这个 Class 对象是对办法区拜访数据的 ” 入口 ”

    • 堆中实例对象中对象头的类型指针指向它这个类办法区的类元数据
  • 对于加载能够由 JVM 的自带类加载器来实现, 也能够通过开发人员自定义的类加载器来实现(实现 ClassLoader, 重写 findClass())

留神

  1. 数组类是间接由 JVM 在内存中动静结构的, 数组中的元素还是要靠类加载器进行加载
  2. 反射正是通过加载创立的 Class 对象能力在运行期应用反射

Verification

验证

  • 验证的作用

    确保要加载的字节码符合规范, 避免危害 JVM 平安

  • 验证的具体划分

    • 文件格式验证

      目标: 保障字节流能正确解析并存储到办法区之内, 格局上合乎 Java 类型信息

      验证字节流是否合乎 Class 文件格式标准(比方 Class 文件主, 次版本号是否在以后虚拟机兼容范畴内 …)

    • 元数据验证

      目标: 对类的元数据信息进行语义验证

      元数据:简略的来说就是形容这个类与其余类之间关系的信息

      元数据信息验证(举例):

      1. 这个类的父类有没有继承其余的最终类(被 final 润饰的类, 不可让其余类继承)
      2. 若这个类不是抽象类, 那这个类有没有实现 (形象父类) 接口的所有办法
    • 字节码验证(验证中最简单的一步)

      目标: 对字节码进行验证, 保障校验的类在运行时不会做出对 JVM 危险的行为

      字节码验证举例:

      1. 类型转换无效: 子类转换为父类(平安, 无效) 父类转换为子类(危险)
      2. 进行算术运算, 应用的是否是雷同类型指令等
    • 符号援用验证

      产生在解析阶段前: 符号援用转换为间接援用

      目标: 保障符号援用转为间接援用时, 该类不短少它所依赖的资源(外部类), 确保解析能够实现

验证阶段是一个十分重要的阶段, 但又不肯定要执行(因为许多第三方的类, 本人封装的类等都被重复 ” 试验 ” 过了)

在生产阶段能够思考敞开 -Xverify:none以此来缩短类加载工夫

Preparation

筹备

筹备阶段为类变量 (动态变量) 分配内存并默认初始化

  • 分配内存

    • 逻辑上应该调配在办法区, 然而因为 hotSpot 在 JDK7 时将 字符串常量, 动态变量 挪出永恒代(放在堆中)
    • 实际上它应该在堆中
  • 默认初始化

    • 类变量个别的默认初始化都是初始化该类型的 零值

      类型 零值
      byte (byte)0
      short (short)0
      int 0
      long 0L
      float 0.0F
      double 0.0
      boolean false
      char ‘\u0000’
      reference null
    • 非凡的类变量的字段属性中存在 ConstantValue 属性值, 会初始化为 ConstantValue 所指向在常量池中的值
    • 只有被 final 润饰的根本类型或字面量且要赋的值在常量池中才会被加上 ConstantValue 属性

Resolution

解析

  • 解析的作用

    将常量池中的常量池中 符号援用替换为间接援用(把符号援用代表的地址替换为实在地址)

    • 符号援用

      • 应用一组符号形容援用(为了定位到指标援用)
      • 与虚拟机内存布局无关
      • 还是符号援用时指标援用不肯定被加载到内存
    • 间接援用

      • 间接执行指标的指针, 绝对偏移量或间接定位指标援用的句柄
      • 与虚拟机内存布局相干
      • 解析间接援用时指标援用曾经被加载到内存中
  • 并未规定解析的工夫

    能够是类加载时就对常量池的符号援用解析为间接援用

    也能够在符号援用要应用的时候再去解析(动静调用时只能是这种状况)

  • 同一个符号援用可能会被解析屡次, 所以会有缓存(标记该符号援用曾经解析过), 屡次解析动作都要保障每次都是雷同的后果(胜利或异样)
类和接口的解析

当咱们要拜访一个未解析过的类时

  1. 把要解析的类的符号援用 交给以后所在类的类加载器 去加载 这个要解析的类
  2. 解析前要进行符号援用验证, 如果以后所在类没有权限拜访这个要解析的类, 抛出异样IllegalAccessError
字段的解析

解析一个从未解析过的字段

  1. 先对此字段所属的类 (类, 抽象类, 接口) 进行解析
  2. 而后在此字段所属的类中查找该字段简略名称和描述符都匹配的字段, 返回它的间接援用

    • 如果此字段所属的类有父类或实现了接口, 要自下而上的寻找该字段
    • 找不到抛出 NoSuchFieldError 异样
  3. 对此字段进行权限验证 (如果不具备权限抛出IllegalAccessError 异样)

确保 JVM 取得字段惟一解析后果

如果同名字段呈现在父类, 接口等中, 编译器有时会更加严格, 间接回绝编译 Class 文件

办法的解析

解析一个从未解析过的办法

  1. 先对此办法所属的类 (类, 抽象类, 接口) 进行解析
  2. 而后在此办法所属的类中查找该办法简略名称和描述符都匹配的办法, 返回它的间接援用

    • 如果此办法所属类是接口间接抛出 IncompatibleClassChangeError 异样
    • 如果此办法所属的类有父类或实现了接口, 要自下而上的寻找该办法(先找父类再找接口)
    • 如果在接口中找到了, 阐明所属类是抽象类, 抛出 AbstractMethodError 异样 (本身找不到, 父类中找不到, 最初在接口中找到了, 阐明他是抽象类), 找不到抛出NoSuchMethodError 异样
  3. 对此办法进行权限验证 (如果不具备权限抛出IllegalAccessError 异样)
接口办法的解析

解析一个从未解析过的接口办法

  1. 先对此接口办法所属的接口进行解析
  2. 而后在此接口办法所属的接口中查找该接口办法简略名称和描述符都匹配的接口办法, 返回它的间接援用

    • 如果此接口办法所属接口是类间接抛出 IncompatibleClassChangeError 异样
    • 如果此办法所属的接口有父接口, 要自下而上的寻找该接口办法
    • 如果多个不同的接口中都存在这个接口办法, 会随机返回一个间接援用(编译会更严格, 这种状况应该会回绝编译)
  3. 找不到抛出NoSuchMethodError

Initializtion

初始化

执行类结构器 <clinit> 的过程

  • 什么是 <clinit> ?

    • <clinit> 是javac 编译器 在编译期间主动收集类变量赋值的语句和动态代码块合并 主动生成的
    • 如果没有对类变量赋值动作或者动态代码块 <clinit> 可能不会生成 (带有 ConstantValue 属性的类变量初始化曾经在筹备阶段做过了, 不会在这里初始化)
  • 类和接口的类结构器

    • <clinit> 又叫类结构器, 与 <init> 实例结构器不同, 类结构器不必显示父类类结构器调用

      然而 父类要在子类之前初始化, 也就是实现类结构器

    • 接口

      执行接口的类结构器时, 不会去执行它父类接口的类结构器, 直到用到父接口中定义的变量被应用时才执行

  • JVM 会保障执行 <clinit> 在多线程环境下被正确的加锁和同步(也就是只会有一个线程去执行 <clinit> 其余线程会阻塞期待, 直到 <clinit> 实现)

     public class TestJVM {
         static class  A{
             static {if (true){System.out.println(Thread.currentThread().getName() + "<clinit> init");
                     while (true){​}
                 }
             }
         }
         @Test
         public void test(){Runnable runnable = new Runnable() {
                 @Override
                 public void run() {System.out.println(Thread.currentThread().getName() + "start");
                     A a = new A();
                     System.out.println(Thread.currentThread().getName() + "end");
                 }
             };
     ​
             new Thread(runnable,"1 号线程").start();
             new Thread(runnable,"2 号线程").start();}
     ​
     }
     ​
     /*
     1 号线程 start
     2 号线程 start
     1 号线程 <clinit> init
     */

JVM 规定 6 种状况下必须进行初始化(被动援用)

被动援用
  • 遇到 new,getstatic,putstatic,invokestatic 四条字节码指令

    • new
    • 读 / 写 某类动态变量(不包含常量)
    • 调用 某类静态方法
  • 应用 java.lan.reflect 包中办法对类型进行反射
  • 父类未初始化要先初始化父类 (不适用于接口)
  • 虚拟机启动时, 先初始化 main 办法所在的类
  • 某类实现的接口中有默认办法(JDK8 新退出的), 要先对接口进行初始化
  • JDK7 新退出的动静语言反对, 局部 ….
被动援用
  1. 当拜访动态字段时, 只有真正申明这个字段的类才会被初始化

(子类拜访父类动态变量)

 public class TestMain {
     static {System.out.println("main 办法所在的类初始化");
     }
 ​
     public static void main(String[] args) {System.out.println(Sup.i);
     }
 }
 ​
 class Sub{
     static {System.out.println("子类初始化");
     }
 }
 ​
 class Sup{
     static {System.out.println("父类初始化");
     }
     static int i = 100;
 }
 ​
 /*
 main 办法所在的类初始化
 父类初始化
 100
 */

子类调用父类动态变量是在父类类加载初始化的时候赋值的, 所以子类不会类加载

  1. 实例数组
 public class TestArr {
     static {System.out.println("main 办法所在的类初始化");
     }
     public static void main(String[] args) {Arr[] arrs = new Arr[1];
     }
 }
 ​
 class Arr{
     static {System.out.println("arr 初始化");
     }
 }
 ​
 /*
 main 办法所在的类初始化
 */

例子里包名为:org.fenixsoft.classloading。该例子没有触发类 org.fenixsoft.classloading.Arr 的初始化阶段,但触发了另外一个名为“[Lorg.fenixsoft.classloading.Arr”的类的初始化阶段,对于用户代码来说,这并不是一个非法的类名称,它是一个 由虚拟机主动生成的、间接继承于 Object 的子类,创立动作由字节码指令 anewarray 触发. 这个类 代表了一个元素类型为 org.fenixsoft.classloading.Arr 的一维数组 ,数组中应有的属性和办法(用户可间接应用的只有被润饰为 public 的 length 属性和 clone() 办法)都实现在这个类里。

创立数组时不会对数组中的类型对象 (Arr) 产生类加载

虚拟机主动生成的一个类, 治理 Arr 的数组, 会对这个类进行类加载

  1. 调用动态常量
 public class TestConstant {
     static {System.out.println("main 办法所在的类初始化");
     }
     public static void main(String[] args) {System.out.println(Constant.NUM);
     }
 }
 ​
 class Constant{
     static {System.out.println("Constant 初始化");
     }
     static final int NUM = 555;
 }
 ​
 /*
 main 办法所在的类初始化
 555
 */

咱们在连贯阶段的筹备中阐明过, 如果动态变量字段表中有 ConstantValue(被 final 润饰) 它在筹备阶段就曾经实现初始默认值了, 不必进行初始化

  1. 调用 classLoader 类的 loadClass()办法加载类不导致类初始化

卸载

办法区的垃圾回收次要有两局部: 不应用的常量和类

回收办法区性价比比拟低, 因为不应用的常量和类比拟少

不应用的常量

没有任何中央援用常量池中的某常量, 则该常量会在垃圾回收时, 被收集器回收

不应用的类

成为不应用的类须要满足以下要求:

  1. 没有该类的任何实例对象
  2. 加载该类的类加载器被回收
  3. 该类对应的 Class 对象没在任何中央被援用

留神: 就算被容许回收也不肯定会被回收, 个别只会回收自定义的类加载器加载的类

总结

本篇文章围绕类加载阶段流程的加载 - 验证 - 筹备 - 解析 - 初始化 - 卸载 具体开展每个阶段的细节

加载阶段次要是类加载器加载字节码流,将动态构造(动态常量池)转换为运行时常量池,生成 class 对象

验证阶段验证平安确保不会危害到 JVM,次要验证文件格式,类的元数据信息、字节码、符号援用等

筹备阶段为类变量分配内存并默认初始化零值

解析阶段将常量池的符号援用替换为间接援用

初始化阶段执行类结构器(类变量赋值与类代码块的合并)

  • 参考资料

    • 《深刻了解 Java 虚拟机》
    • 局部图片起源网络

最初(不要白嫖,一键三连求求拉 \~)

本篇文章笔记以及案例被支出 gitee-StudyJava、github-StudyJava 感兴趣的同学能够 stat 下继续关注喔 \~

有什么问题能够在评论区交换,如果感觉菜菜写的不错,能够点赞、关注、珍藏反对一下 \~

关注菜菜,分享更多干货,公众号:菜菜的后端私房菜

本文由博客一文多发平台 OpenWrite 公布!

正文完
 0