JVM 类加载过程

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

与那些在编译时需要进行连接的语言不同,在 Java 语言里面,类型的加载、连接和初始化过程都是在程序运行期间完成的,这种策略让 Java 语言进行提前编译会面临额外的困难,也会让类加载时稍微增加一些性能开销, 但是却为 Java 应用提供了极高的扩展性和灵活性,Java 天生可以动态扩展的语言特性就是依赖运行期动态加载和动态连接这个特点实现的。例如,编写一个面向接口的应用程序,可以等到运行时再指定其实际的实现类,用户可以通过 Java 预置的或自定义类加载器,让某个本地的应用程序在运行时从网络或其他地方上加载一个二进制流作为其程序代码的一部分。这种动态组装应用的方式目前已广泛应用于Java 程序之中,从最基础的 Applet、JSP 到相对复杂的 OSGi 技术,都依赖着 Java 语言运行期类加载才得以诞生。

之前写的 Java 实战案例:Java 类隔离应用:多 Jar 包支持,就是充分利用了 Java 这个特性实现的。


在正式学习下面内容之前,避免因表达而造成的歧义,约定如下内容:



对于类加载过程,包括加载、连接、初始化三个阶段,每个阶段的作用简单概括如下:

类加载过程三个阶段

要注意类加载过程和类加载阶段两个名词的区别:

类加载过程:是类加载的整个流程,包括加载阶段、连接阶段、初始化阶段

类加载阶段:只是类加载过程的第一个阶段

加载阶段

在加载阶段,JVM 需要完成下面三件事:

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

类加载的最终产物就是堆内存中的 Class 对象,对于同一个 ClassLoader 来说,不管某个类被加载多少次,对应到堆内存中,只有一个 Class 对象。如下图所示:

虚拟机规范要求类的加载通过类的全限定类名来获取二进制字节流,并没有严格规范获取的途径,这就给开发者很大的想象空间,除了我们平时常见的 Class 文件以外,还会有如下几种形式:

连接阶段 - 验证

验证是连接阶段的第一步,这一阶段的目的是确保 Class 文件的字节流中包含的信息符合《Java虚 拟机规范》的全部约束要求,保证这些信息被当作代码运行后不会危害虚拟机自身的安全。

验证阶段大致会完成下面四个阶段的验证动作:

该阶段开发者可控性弱,而且对我们学习 Java 开发意义不大,再次不多赘述,想要更详细的学习具体每阶段验证的内容,可以阅读《深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)周志明》

连接阶段 - 准备

在准备阶段,会为类中定义的类变量(即静态变量,使用 static 修饰的变量)分配内存并设置类变量初始值,从概念上讲,这些变量所使用的内存都应当在方法区中进行分配,但必须注意到方法区本身是一个逻辑上的区域,在JDK 7 及之前,HotSpot 使用永久代来实现方法区时,实现是完全符合这 种逻辑概念的;而在 JDK 8 及之后,类变量则会随着 Class 对象一起存放在 Java 堆中,这时候“类变量在方法区”就完全是一种对逻辑概念的表述了。

需要注意区分类变量实例变量两个概念:

在此阶段,类变量设置初始值包括两种情况:

1、编译后 value 具有 ConstantValue 属性

当类变量是使用 static 和 final 修饰,并且代码中赋值类型为基本数据类型或字符串(这里的字符串是双引号赋值,不是 new String()),如下所示:

public class Demo {
	// 基本数据类型
    private static final int NUM = 10;
	// 字符串
    private static final String STR = "字符串";
}

使用命令 javap -v -p Demo.class 查看编译后的 Class 文件,如下所示:

// 省略
{
  private static final int NUM;
    descriptor: I
    flags: (0x001a) ACC_PRIVATE, ACC_STATIC, ACC_FINAL
    ConstantValue: int 10

  private static final java.lang.String STR;
    descriptor: Ljava/lang/String;
    flags: (0x001a) ACC_PRIVATE, ACC_STATIC, ACC_FINAL
    ConstantValue: String 字符串

  // 省略
}
SourceFile: "Demo.java"

可以看到两个变量均生成了 ConstantValue 属性,对于这种变量,设置初始值就是当前代码中设置的值。

2、不具有 ConstantValue 属性

当类类变量只使用 static 修饰,或使用 static 和 final 修饰,但代码中赋值类型为引用数据类型,如下所示:

public class Demo {
	// 只有 static 修饰
    private static int num = 10;
	// 赋值为引用数据类型
    private static final String STR = new String("字符串");
}

使用命令 javap -v -p Demo.class 查看编译后的 Class 文件,如下所示:

// 省略
{
  private static int num;
    descriptor: I
    flags: (0x000a) ACC_PRIVATE, ACC_STATIC

  private static final java.lang.String STR;
    descriptor: Ljava/lang/String;
    flags: (0x001a) ACC_PRIVATE, ACC_STATIC, ACC_FINAL

  // 省略
}
SourceFile: "Demo.java"

这类类变量没有生成 ConstantValue 属性,对于这类变量,设置初始值就是对应类型的默认初始值,比如类变量 num 初始值为 0,STR 初始值为 null,具体如下:

各种数据类型初始化零值

连接阶段 - 解析

该阶段开发者可控性弱,而且对我们学习 Java 开发意义不大,再次不多赘述,想要更详细的学习具体每阶段验证的内容,可以阅读《深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)周志明》

初始化阶段

类的初始化阶段是类加载过程的最后一个步骤,之前介绍的几个类加载的动作里,除了在加载阶段用户应用程序可以通过自定义类加载器的方式局部参与外,其余动作都完全由Java虚拟机来主导控制。直到初始化阶段,Java 虚拟机才真正开始执行类中编写的 Java 程序代码,将主导权移交给应用程序。

在初始化阶段,最主要做一件事,就是执行类构造器 () 方法(clinit:class initialize 前几个字母的简写),该方法是由编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并产生的,如下所示:

public class InitStageTest {

    private static final String STATIC_CONSTANT = "静态常量";

    private static String CLASS_VARIABLE = "类变量";
    
    static {
        CLASS_VARIABLE = "类变量重新赋值";
    }
}

对上面的类进行编译,执行命令 javap -v -p InitStageTest.class 查看字节码,如下所示:

// 省略上面内容
{
  private static final java.lang.String STATIC_CONSTANT;
    descriptor: Ljava/lang/String;
    flags: ACC_PRIVATE, ACC_STATIC, ACC_FINAL

  private static java.lang.String CLASS_VARIABLE;
    descriptor: Ljava/lang/String;
    flags: ACC_PRIVATE, ACC_STATIC

  public com.haichun.jvm.InitStageTest();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."":()V
         4: return
      LineNumberTable:
        line 6: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/haichun/jvm/InitStageTest;

  static {};
    descriptor: ()V
    flags: ACC_STATIC
    Code:
      stack=3, locals=0, args_size=0
         0: new           #7                  // class java/lang/String
         3: dup
         4: ldc           #9                  // String 静态常量
         6: invokespecial #11                 // Method java/lang/String."":(Ljava/lang/String;)V
         9: putstatic     #14                 // Field STATIC_CONSTANT:Ljava/lang/String;
        12: ldc           #20                 // String 类变量
        14: putstatic     #22                 // Field CLASS_VARIABLE:Ljava/lang/String;
        17: ldc           #25                 // String 类变量重新赋值
        19: putstatic     #22                 // Field CLASS_VARIABLE:Ljava/lang/String;
        22: return
      LineNumberTable:
        line 8: 0
        line 10: 12
        line 13: 17
        line 14: 22
}
SourceFile: "InitStageTest.java"

上面输出的信息中 static {} 其实就是 方法

为了更加直观的看到,可以使用 IDEA 插件 jclasslib Bytecode Viewer 查看类的字节码信息,如下所示:

根据 方法的字节码可以看到,在该方法中有如下内容:

通过插件可以看到另外一个方法 (对应 javap 命令打印的 public om.haichun.jvm.InitStageTest()),该方法为实例构造器,该方法是在类实例化时调用。要区别 方法, 方法为类构造器,是在类加载过程初始化阶段调用。

实例构造器,其实就是我们代码中定义的构造函数,如果有多个构造函数,就会有多个 方法。

方法与 方法不同的是,不需要显示的调用父类类构造器( 方法首先会调用父类实例构造器,如 public om.haichun.jvm.InitStageTest() 命令中 1: invokespecial #1 调用 Object 的构造器),而是由 JVM 保证子类 方法执行前,父类的先执行完成,因此,JVM 第一个执行的 方法的类型一定是 Object。

由于父类 方法先执行,也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作,因此下面示例中字段 B 的值将会是 2 不是 1:

static class Parent {
    public static int A = 1;
    
    static {
    	A = 2;
    }
}

static class Sub extends Parent {
    public static int B = A;
}

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

方法对于类或接口来说并不是必需的,如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生成 方法。


接口中不能使用静态语句块,但仍然有变量初始化的赋值操作,因此接口与类一样都会生成 方法。但接口与类不同的是,执行接口的 方法不需要先执行父接口的 方法,因为只有当父接口中定义的变量被使用时,父接口才会被初始化。此外,接口的实现类在初始化时也一样不会执行接口的 方法。



参考文献

深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)周志明

Java高并发编程详解:多线程与架构设计

展开阅读全文

页面更新:2024-03-02

标签:加载   赋值   初始化   变量   字节   静态   虚拟机   接口   阶段   过程   方法

1 2 3 4 5

上滑加载更多 ↓
推荐阅读:
友情链接:
更多:

本站资料均由网友自行发布提供,仅用于学习交流。如有版权问题,请与我联系,QQ:4156828  

© CopyRight 2008-2024 All Rights Reserved. Powered By bs178.com 闽ICP备11008920号-3
闽公网安备35020302034844号

Top