深入理解java虚拟机笔记Chapter7

虚拟机类的加载机制

概述

虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类的加载机制。

类加载的时机

JVM 会在程序第一次主动引用类的时候,加载该类,被动引用时并不会引发类加载的操作。也就是说,JVM 并不是在一开始就把一个程序就所有的类都加载到内存中,而是到不得不用的时候才把它加载进来,而且只加载一次。那么什么是主动引用,什么是被动引用呢?

  • 主动引用
    • 遇到 new、getstatic、putstatic、invokestatic 字节码指令,例如:
      • 使用 new 实例化对象;
      • 读取或设置一个类的 static 字段(被 final 修饰的除外);
      • 调用类的静态方法。
    • 对类进行反射调用;
    • 初始化一个类时,其父类还没初始化(需先初始化父类);
      • 这点类与接口具有不同的表现,接口初始化时,不要求其父接口完成初始化,只有真正使用父接口时才初始化,如引用父接口中定义的常量。
    • 虚拟机启动,先初始化包含 main() 函数的主类;
    • JDK 1.7 动态语言支持:一个 java.lang.invoke.MethodHandle 的解析结果为 REF_getStaticREF_putStaticREF_invokeStatic
  • 被动引用
    • 通过子类引用父类静态字段,不会导致子类初始化;
    • Array[] arr = new Array[10]; 不会触发 Array 类初始化;
    • static final VAR 在编译阶段会存入调用类的常量池,通过 ClassName.VAR 引用不会触发 ClassName 初始化。

也就是说,只有发生主动引用所列出的 5 种情况,一个类才会被加载到内存中,也就是说类的加载是 lazy-load 的,不到必要时刻是不会提前加载的,毕竟如果将程序运行中永远用不到的类加载进内存,会占用方法区中的内存,浪费系统资源。


类的生命周期


类的加载过程

加载

加载(Loading)阶段,虚拟机需要完成以下三件事:

  • 通过一个类的全限定名来获取定义这个类对应的二进制字节流
  • 将这个类的二进制字节流所代表的静态存储结构转换为方法区的运行时数据结构
  • 在Java堆中生成一个代表这个类的 java.lang.Class 对象,作为方法区这些数据的访问入口。

分类

  • 非数组类
    • 系统提供的引导类加载器
    • 用户自定义的类加载器
  • 数组类
    • 不通过类加载器,由 Java 虚拟机直接创建
    • 创建动作由 newarray 指令触发,new 实际上触发了 [L全类名 对象的初始化
    • 规则
      • 数组元素是引用类型
        • 加载:递归加载其组件
        • 可见性:与引用类型一致
      • 数组元素是非引用类型
        • 加载:与引导类加载器关联
        • 可见性:public

类的显式加载和隐式加载

  • 显示加载:
    • 调用 ClassLoader#loadClass(className)Class.forName(className)
    • 两种显示加载 .class 文件的区别:
      • Class.forName(className) 加载 class 的同时会初始化静态域,ClassLoader#loadClass(className) 不会初始化静态域;
      • Class.forName 借助当前调用者的 class 的 ClassLoader 完成 class 的加载。
  • 隐式加载:
    • new 类对象;
    • 使用类的静态域;
    • 创建子类对象;
    • 使用子类的静态域;
    • 其他的隐式加载,在 JVM 启动时:
      • BootStrapLoader 会加载一些 JVM 自身运行所需的 Class;
      • ExtClassLoader 会加载指定目录下一些特殊的 Class;
      • AppClassLoader 会加载 classpath 路径下的 Class,以及 main 函数所在的类的 Class 文件。

验证

目的: 确保 .class 文件中的字节流信息符合虚拟机的要求。

4 个验证过程:

  • 文件格式验证:是否符合 Class 文件格式规范,验证文件开头 4 个字节是不是 “魔数” 0xCAFEBABE
    • 魔数:每个Class文件的头4个字节称为魔数,它的唯一作用是确定这个文件是否为一个能被虚拟机接收的class文件。
  • 元数据验证:保证字节码描述信息符号 Java 规范(语义分析)
  • 字节码验证:程序语义、逻辑是否正确(通过数据流、控制流分析)
  • 符号引用验证:对类自身以外的信息(常量池中的符号引用)进行匹配性校验

这个操作虽然重要,但不是必要的,可以通过 -Xverify:none 关掉。

准备

描述: 为 static 变量在方法区分配内存。

  • static 变量准备后的初始值:
    • public static int value = 123;
      • 准备后为 0,value 的赋值指令 putstatic 会被放在 <clinit>() 方法中,<clinit>()方法会在初始化时执行,也就是说,value 变量只有在初始化后才等于 123。
    • public static final int value = 123;
      • 准备后为 123,因为被 static final 赋值之后 value 就不能再修改了,所以在这里进行了赋值之后,之后不可能再出现赋值操作,所以可以直接在准备阶段就把 value 的值初始化好。

解析

描述: 将常量池中的 “符号引用” 替换为 “直接引用”。

在此之前,常量池中的引用是不一定存在的,解析过之后,可以保证常量池中的引用在内存中一定存在。

什么是 “符号引用” 和 “直接引用” ?

  • 符号引用:以一组符号描述所引用的对象(如对象的全类名),引用的目标不一定存在于内存中。
  • 直接引用:直接指向被引用目标在内存中的位置的指针等,也就是说,引用的目标一定存在于内存中。

初始化

描述: 执行类构造器 () 方法的过程。

  • <clinit>() 方法
    • 包含的内容:
      • 所有 static 的赋值操作;
      • static 块中的语句;
    • <clinit>() 方法中的语句顺序:
      • 基本按照语句在源文件中出现的顺序排列;
      • 静态语句块只能访问定义在它前面的变量,定义在它后面的变量,可以赋值,但不能访问。
    • <init>() 的不同:
      • 不需要显示调用父类的 <clinit>() 方法;
      • 虚拟机保证在子类的 <clinit>() 方法执行前,父类的 <clinit>() 方法一定执行完毕。也就是说,父类的 static 块和 static 字段的赋值操作是要先于子类的。
    • 接口与类的不同:
      • 执行子接口的 <clinit>() 方法前不需要先执行父接口的 <clinit>() 方法(除非用到了父接口中定义的 public static final 变量);
    • 执行过程中加锁:
      • 同一时刻只能有一个线程在执行 <clinit>() 方法,因为虚拟机要保证在同一个类加载器下,一个类只被加载一次。
    • 非必要性:
      • 一个类如果没有任何 static 的内容就不需要执行 () 方法。

本小节的补充:<clinit><init> 方法

概述

在编译生成class文件时,会自动产生两个方法,一个是类的初始化方法<clinit>, 另一个是实例的初始化方法<init>

<clinit>:在jvm第一次加载class文件时调用,包括静态变量初始化语句和静态块的执行

<init>:在实例创建出来的时候调用,包括调用new操作符;调用Class或java.lang.reflect.Constructor对象的newInstance()方法;调用任何现有对象的clone()方法;通过java.io.ObjectInputStream类的getObject()方法反序列化。

<clinit>方法

先理解 类初始化阶段 的含义: 该阶段负责为类变量赋予正确的初始值, 是一个类或接口被首次使用前的最后一项工作

<clinit>方法的执行时期: 类初始化阶段(该方法只能被jvm调用, 专门承担类变量的初始化工作)

<clinit>方法的内容: 所有的类变量初始化语句和类型的静态初始化器

类的初始化时机: 即在java代码中首次主动使用的时候, 包含以下情形:

  • (首次)创建某个类的新实例时--new, 反射, 克隆 或 反序列化;
  • (首次)调用某个类的静态方法时;
  • (首次)使用某个类或接口的静态字段或对该字段(final 字段除外)赋值时;
  • (首次)调用java的某些反射方法时;
  • (首次)初始化某个类的子类时;
  • (首次)在虚拟机启动时某个含有 main() 方法的那个启动类

注意: 并非所有的类都会拥有一个方法, 满足下列条件之一的类不会拥有方法:

  1. 该类既没有声明任何类变量,也没有静态初始化语句;

  2. 该类声明了类变量,但没有明确使用类变量初始化语句或静态初始化语句初始化;

  3. 该类仅包含静态 final 变量的类变量初始化语句,并且类变量初始化语句是编译时常量表达式;

<init>方法

<init>方法的执行时期: 对象的初始化阶段

实例化一个类的四种途径:

  1. 调用 new 操作符
  2. 调用 Class 或 java.lang.reflect.Constructor 对象的newInstance()方法
  3. 调用任何现有对象的clone()方法
  4. 通过 java.io.ObjectInputStream 类的 getObject() 方法反序列化

类加载器

类加载过程中的“通过一个类的全限定名来获取描述这个类的二进制字节流”这个动作是放在Java虚拟机的外部来实现的,以便于让应用程序自己来决定如何去获取所需要的类,实现这个动作的代码模块被称为“类加载器”

类加载器虽然只用于实现类的加载动作,但是它的作用却远远不限于此,比较两个类是否“相等”,不仅仅要确认这两个类是否来源于同一个class文件,还需要加载这两个类的类加载器相同。

如何判断两个类 “相等”

  • “相等” 的要求
    • 同一个 .class 文件
    • 被同一个虚拟机加载
    • 被同一个类加载器加载
  • 判断 “相等” 的方法
    • instanceof 关键字
    • Class 对象中的方法:
      • equals()
      • isInstance()
      • isAssignableFrom()

类加载器的分类

站在虚拟机的角度,只存在两种类加载器:

启动类加载器(Bootstrap ClassLoader),使用C++实现,是虚拟机的一部分
其他类加载器,由Java语言实现,独立于虚拟机之外的,全部继承自抽象类 java.lang.ClassLoader

从开发人员的角度,类加载器可以划分得更细致一些:

  • 启动类加载器(Bootstrap):负责将存放在 \lib 目录中的,或者被 -Xbootclasspath 参数所指定的路径中的,并且是虚拟机识别的类库加载到虚拟机内存中。
    • <JAVA_HOME>/lib
    • -Xbootclasspath 参数指定的路径
  • 扩展类加载器(Extension):负责加载 <JAVA_HOME>\lib\ext 目录下的,或者被 java.ext.dirs 系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。
    • <JAVA_HOME>/lib/ext
    • java.ext.dirs 系统变量指定的路径
  • 应用程序类加载器(Application):负责加载用户类路径(ClassPath)上所指定的类库,一般情况下这个就是程序中默认的类加载器。
    • -classpath 参数

以上加载器互相配合来加载我们自己的应用程序,如果有必要,我们还可以加入自己定义的加载器。这些加载器之间的关系一般如下图示:

双亲委派模型

类加载器的双亲委派模型(Parent Delegation Model):要求除了顶层的启动类加载器外,其余的类加载器都必须有自己的父类加载器。(注意!这里类加载器之间的父子关系一般不会以继承(Inheritance)来实现,而是使用组合(Composition)来复用父加载器的代码)。这种模型被广泛使用于几乎所有的Java程序中,但是它并不是一个强制性的约束,只是Java设计者推荐给开发者使用的一种类加载器实现方式。

  • 工作过程
    • 当前类加载器收到类加载的请求后,先不自己尝试加载类,而是先将请求委派给父类加载器。因此,所有的类加载请求,都会先被传送到启动类加载器。
    • 只有当父类加载器加载失败时,当前类加载器才会尝试自己去自己负责的区域加载
  • 实现
    • 检查该类是否已经被加载
    • 将类加载请求委派给父类
      • 如果父类加载器为 null,默认使用启动类加载器
      • parent.loadClass(name, false)
    • 当父类加载器加载失败时
      • catch ClassNotFoundException 但不做任何处理
      • 调用自己的 findClass() 去加载
        • 我们在实现自己的类加载器时只需要 extends ClassLoader,然后重写 findClass() 方法而不是 loadClass() 方法,这样就不用重写 loadClass() 中的双亲委派机制了
  • 优点
    • 自己写的类库同名类不会覆盖类库的类
    • java类随着它的类加载器一起具备了一种带有优先层级的层次关系,保证了Java程序的稳定运行。
posted @ 2020-10-07 19:15  猫坚果NutCat  阅读(159)  评论(0编辑  收藏  举报