2虚拟机如何加载类

三大步骤:加载、链接、初始化
基本数据类型由Java虚拟机原先定义好,引用数据类型,泛型会被擦除,数组由虚拟机直接生成,类和接口对应字节流。

加载

定义:查找字节流并创建类结构
数据源:jar文件、class文件、网络数据源等

数组没有字节流由Java虚拟机直接生成,其他类需借助类加载器完成查找字节流过程。

类加载器

功能

功能:加载类,提供命名空间
类的唯一性是由类加载器实例以及类的全名确定。
同一串字节流由不同的类加载器加载,会得到两个不同的类。可借此,来运行同一个类的不同版本。

分类

  • 启动类加载器boot class loader
    最为基础、最为重要的类,如JRE lib目录下jar包中的类以及Xbootclasspath制定的类
  • 扩展类加载器 extension class loader
    boot class loader的子类
    负责相对次要又通用类的加载,如JRE lib/ext目录下jar包中的类以及由系统变量java.ext.dirs制定的类
    Java 9 改名为平台类加载器 platform class loader
  • 应用加载器 application class loader
    extension class loader的子类
    加载应用程序路径下的类,(应用程序路径指虚拟机参数 -cp/-classpath、系统变量 java.class.path 或环境变量 CLASSPATH 所指定的路径)
    默认应用程序中包含的类由应用类加载器加载
  • 自定义类加载器
    如实现class文件加密,加载时解密

双亲委派模型

类加载器接收到加载请求时,先将请求转发给父类加载器,附加在其没有找到该类时,它再尝试加载。避免重复加载。

链接

三个阶段:验证、准备、解析

  • 验证
    确保加载类满足JVM约束
  • 准备
    1. 为加载类的静态字段分配内存,静态字段初始化在初始化阶段进行
    2. 构造跟其他和类层次相关的数据结构,如动态绑定方法表
  • 解析
    将常量池中的符号引用替换为直接引语

.class文件加载到JVM前,类不知道其他类及其他类的方法、字段对应的具体地址,若引用其他类成员时,JAVA编译器会生成一个符号引用指代目标方法类名、方法名、参数类型、返回值类型
解析目的就是将指代符号解析为实际引用,若引用的类未加载则触发加载此类(但未必链接及初始化它)

JVM规范要求在执行前完成引用符号的解析,没有要求连接过程中完成解析

初始化

为常量值的字段赋值,执行clinit方法
clinit方法:Java编译器会把所有final静态字段赋值和静态代码块中的代码置于clinit方法中

初始化触发时机

  • 虚拟机启动时,初始化用户指定的主类;
  • 初始化 new 指令的目标类;
  • 调用静态方法的指令时,初始化该静态方法所在的类;
  • 访问静态字段的指令时,初始化该静态字段所在的类;
  • 子类的初始化会触发父类的初始化;
  • 如果一个接口定义了 default 方法,那么直接实现或者间接实现该接口的类的初始化,会触发该接口的初始化;
  • 使用反射API对某个类进行反射调用时,初始化这个类;
  • 当初次调用 MethodHandle 实例时,初始化该 MethodHandle 指向的方法所在的类。

总结

加载是指查找字节流,并且据此创建类的过程。加载需要借助类加载器,在Java虚拟机中,类加载器使用了双亲委派模型,即接收到加载请求时,会先将请求转发给父类加载器。

链接,是指将创建成的类合并至Java虚拟机中,使之能够执行的过程。链接还分验证、准备和解析三个阶段。其中,解析阶段为非必须的。

初始化,则是为标记为常量值的字段赋值,以及执行 < clinit > 方法的过程。类的初始化仅会被执行一次,这个特性被用来实现单例的延迟初始化。

问题

1
2
3
4
public static Object getInstance(boolean flag) {
if (flag) return new LazyHolder[2]; //会加载、链接、初始化LazyHolder吗?
return LazyHolder.INSTANCE;
}

会加载,不会链接、初始化。flag=true时链接、初始化。