jvm 类加载机制

本文是在学习周志明先生《深入理解 Java 虚拟机》一书时所作的总结笔记,在此对周先生表示诚挚的感谢。

1. 概述

虚拟机的类加载机制:

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

2. 类加载的时机

类被加载的整个生命周期

加载 Loading --> 验证 Verification --> 准备 Preparation --> 解析 Resolution --> 初始化 Initialization --> 使用 Using --> 卸载 Unloading

其中加载,验证,准备,初始化和卸载这 5 个阶段的顺序是确定的。

由于在初始化之前,必须要先完成类加载的过程,并且在虚拟机规范中严格规定了有且只有 5 种情况必须对类进行初始化,因此这些情况下也必须对类加载。

这 5 种情况分别为

  1. 遇到 new、getstatic、putstatic、invokestatic 这4 条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。场景如下
    1. 使用 new 关键字实例化对象的时候
    2. 读取 getstatic 或者设置 putstatic 一个类的静态字段时
    3. 调用一个类的静态方法时
  2. 使用 java.lang.reflect 包的方法对类进行反射调用时,如果类没有进行过初始化,则需要先触发其初始化
  3. 当触发一个类的时候,如果发现父类还没有进行过初始化,则需要先触发其父类的初始化
  4. 当 jvm 启动时,用户需要指定一个要执行的主类,也就是包含 main 方法的哪个类,jvm 会先初始化这个主类
  5.  java.lang.invoke.MethodHandle

这里引入了两个关键词,主动引用和被动引用。

  • 主动引用: 上面的场景就称为对一个类的主动引用
  • 被动引用: 除了上面的场景,所有引用的方法都不会触发初始化,称为被动引用

关于主动引用和被动引用的测验请看下面例子(为了集中展示,作者将这几个 class 的定义都放在了一起,如果读者需要调试运行,请将其拆开,放在不同的文件下)

public class SuperClass {
   static {
        System.out.println("SuperClass init");
   }

   public static int value = 123;
}

public class SubClass extends SuperClass {

   static {
      System.out.println("SubClass init!");
   }
   
}

public class NotInitialization {

    public static void main(String[] args) {
        System.out.println(SubClass.value);
    }
}

其中 value 是在 SuperClass 中定义的静态变量,当在运行时只有定义到该字段的类才能初始化。

在本例中,subClass 不会初始化,因此其输出应该为

而如果将 value 值的定义挪到 SubClass 中

public class SubClass extends SuperClass {

   static {
      System.out.println("SubClass init!");
   }
   
   public static int value = 123;
}

得到的结果为

super 和 sub class 都初始化了,这是因为在主动引用中有一条:

当初始化一个类时,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。

 因此 subClass 在初始化时,会将其父类 SuperClass 也一并初始化。

此时如果将 NotInitialization 的 main 方法修改如下

package com.reycg.jvm;


public class NotInitialization {

    public static void main(String[] args) {
        SuperClass[] superClassArray = new SuperClass[10];
    }
}

在运行时会发现程序没有输出,也就是说没有触发 SuperClass 的初始化。

使用 javap 来查看 NotInitialization 会获取main 方法如下

main 方法触发了另外一个名为  [Lcom/reycg/jvm/SuperClass 的类的初始化,它是一个由虚拟机自动生成的,直接继承自 java.lang.Object 的子类,创建动作由 newarray 触发。

 继续看第 3 个被动引用的例子,定义一个 ConstClass 如下

package com.reycg.jvm;

public class ConstClass {
    
    static {
        System.out.println("ConstClass init");
    }
    
    public static final String HELLOWORLD = "helloworld";

}

public class NotInitialization {

    public static void main(String[] args) {
        System.out.println(ConstClass.HELLOWORLD);
    }
}

运行得到的结果为

可见,在运行时并没有初始化 ConstClass, 这是因为 HELLOWORLD 的值在编译阶段经过常量传播优化,会存储在 NotInitialization 类的常量池中,之后 NotInitialization  对常量 ConstClass.HELLOWORLD 的引用实际都转化为了 NotInitialization  常量池的引用。

这一点我们可以通过使用 javap 命令对 NotInitialization 的字节码计算看出

3. 类加载的过程

这部分是对类加载的全过程进行详细的学习。

3.1 加载

在加载阶段,jvm 需要完成下面 3 件事情

  1. 通过一个类的全限定名来获取定义此 class 的二进制字节流
  2. 将该字节流代表的静态存储结构转化为方法区的运行时结构数据
  3. 在内存中生成一个代表该类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口

在加载完成后,jvm 外部的 class 的二进制字节流 就会存储在方法区,也就是 HotSpot 的永久代中。对 HotSpot 来说,接下来就会在方法区,也就是永久代中实例化一个 java.lang.Class 类的对象。这个对象就会作为程序访问方法区中的这些类型数据的外部接口。

3.2 验证

验证的目的是为了确保 Class 文件的字节流中包含的信息符合 jvm 的要求,并且不会危害 jvm 自身的安全。

从整体来看,验证阶段大致会完成下面 4 个阶段的检验动作

3.2.1 文件格式验证

这一阶段会直接对 class 字节流进行操作,只有通过这部分验证,才能进入内存的方法区中存储。因此后面的 3 个验证阶段全部基于方法区的存储结构进行,不会再操作字节流。

3.2.2 元数据验证

3.2.3 字节码验证

3.2.4 符号引用验证

对虚拟机的类加载机制来说,验证阶段是一个非常重要,但不是一定必要的阶段。如果所运行的全部代码都已经被反复使用和验证过,那么在实施阶段就可以考虑使用 -Xverify:none 参数来关闭大部分的类验证措施,以缩短 jvm 类加载的时间

3.3 准备

准备阶段是正式为类变量分配内存并设置类变量初始值的阶段。所谓类变量都是添加 static 修饰符的变量。

需要注意以下几点

  • 类变量使用的内存在方法区中分配
  • 为类变量所赋的初始值,“通常情况下”是数据类型的零值。

假设一个类变量的定义为

public static int value = 123;

那么在准备阶段过后,其初始值就是 0。而将 value 赋值为 123 的动作是在类构造器 <clint>() 方法中进行的。

但是对于 static final 同时修饰的基本类型或者 String 类型的数据,也就是通常所说的 ConstantValue 属性,在准备阶段会直接赋值。

 3.4 解析

 所谓解析就是 jvm 将符号引用替换为直接引用的过程。

  • 符号引用就是一组字面量,它与内存布局无关
  • 直接引用可以理解为指针,偏移量,或者能够定位到目标的句柄。它和内存布局直接相关。

通过上面的定义可看出,所谓解析就是将 class 文件中的如常量,变量,方法等在机器内存中存放的过程。

解析动作主要分为下面 4 种

  1. 类或者接口的解析
  2. 字段解析
  3. 接口方法解析
  4. 类方法解析

3.5 初始化

这个阶段才开始真正执行类中定义的 Java 程序代码,其实就是执行类构造器 <clint>() 方法的过程。

<clint>() 类构造器是怎么产生的呢?

在定义类中,如果包含 static 变量或者语句块,编译器就会收集这些语句,并对这些语句进行合并,从而产生类构造器方法。

既然 <clint>() 与 static 变量直接相关,如果类或者接口中没有 static 变量,也就不会有 <clint>() 方法。

父类,子类 <clint>() 类构造器的执行顺序?

父类的 <clint>() 一定会比子类的 <clint>() 方法先执行。映射到 java 类中,也就是说父类的静态语句一定比子类的静态语句先执行。

<clint> 在多线程环境中需要加锁来确保类构造函数只初始化一次。

原文地址:https://www.cnblogs.com/reycg-blog/p/7832293.html