01|类加载的时机

类的生命周期:

  • 1. 加载(Loading)
  • 连接(Linking):
    • 2. 验证(Verification)
    • 3. 准备(Preparation)
    • 4. 解析(Resolution)
  • 5. 初始化(Initialization)
  • 6. 使用(Using)
  • 7. 卸载(Unloading)

什么时候执行类加载没有强制约束,但如遇到以下情况必须立即对类进行初始化

    1. 遇到new、get static、put static 或invoke static这四条字节码指令时,如果对应类型没有进行过初始化,则需要先触发其初始化阶段。能够生成这四条指令的典型场景如下:
      • new 关键字创建对象
      • 读取或者设置一个类型的静态字段
      • 调用一个类型的静态方法
    1. 使用java.lang.reflect包的方法对类型进行反射调用的时候,如果类型没有进行过初始化,则需要先触发其初始化
    1. 当初始化类的时候,如果发现其父类还没有过初始化,则需要先触发其父类的初始化
    1. 当虚拟机启动时,用户需要执行一个要执行的主类,虚拟机会先初始化这个主类
    1. 当使用JDK7新加入的动态语言支持时,如果一台java.lang.invoke.MethodHandle实例最后的解析结果为REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial四种类型的方法句柄,并且这个方法句柄对应的类没有进行过初始化,则需要先触发其初始化
    1. 当一个接口中定义了JDK8新加入的默认方法,如果这个接口的实现类发送类初始化,那该接口要在其之前被初始化

以上六种场景中的行为称为对一个类型的主动引用将会出发类的初始化。除此之外的其它所有引用类型的方式都不会初始化,称为被动引用,如下方示例:

  • 1. 通过子类引用父类的静态字段,不会导致子类初始化

    1. public class ClassTest {
    2. static class A{
    3. static int a= 100;
    4. static {
    5. System.out.println("super init");
    6. }
    7. }
    8. static class B extends A{
    9. static {
    10. System.out.println("sub init");
    11. }
    12. }
    13. public static void main(String[] args) throws Exception {
    14. System.out.println(B.a);
    15. // 仅会输出 super init,不会输出 sub init
    16. }
    17. }
  • 2. 通过数组定义来引用类,不会触发此类的初始化 ```java public class ClassTest {

    static class A{

    1. static int a= 100;
    2. static {
    3. System.out.println("super init");
    4. }

    }

    static class B extends A{

    1. static {
    2. System.out.println("sub init");
    3. }

    }

    public static void main(String[] args) throws Exception {

    1. A[] a = new A[10];
    2. B[] b = new B[10];

    } }

  1. - **3. 常量在编译阶段会存入调用类的常量池中,本质上没有直接引用到定义常量的类,因此也不会触发类的初始化**
  2. ```java
  3. public class ClassTest {
  4. static class A{
  5. static {
  6. System.out.println("class init");
  7. }
  8. public static final int a= 100;
  9. }
  10. public static void main(String[] args) throws Exception {
  11. // 不会输出class init
  12. System.out.println(A.a);
  13. }
  14. }

02|类加载过程

加载

加载阶段,Java虚拟机需要完成以下三件事情:

    1. 通过一个类的全限定名类获取定义类的二进制字节流
      • 从ZIP压缩包中读取(Jar)
      • 从网络中获取(web applet)
      • 运行时计算生成(动态代理)
      • 其他文件生成(JSP)
    1. 将这个字节流所代表的静态存储结构转换为方法区的运行时数据结构
    1. 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口

数组加载

数组类本身不通过类加载器创建,它是由Java虚拟机直接在内存中动态构造出来的。但数组的元素类型(Element Type,指的是数组去掉所有纬度的类型)最终还是要靠类加载器来完成加载,一个数组类创建过程遵循以下规则:

    1. 如果数组中的元素是引用类型,那就递归加载这个数组中的元素类型,数组将被标识在加载该元素类型的类加载器空间上
    1. 如果数组中的元素不是引用类型,Java虚拟机将会把数组标记为与引导类加载器关联
    1. 数组类的可访问性与它的元素类型的可访问性一致,如果元素类型不是引用类型,它的数组类的可访问性将默认为public,可被所有的类和接口访问

加载阶段结束后,Java虚拟机外部的二进制字节流就按照虚拟机所设定的格式存储在方法区中,数据类型妥善安置在方法区之后,会在Java堆内存中实例化一个Java.lang.Class类的对象,这个对象将作为程序访问方法区中的数据类型的外部接口。

验证

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

    1. 文件格式验证
    1. 元数据验证
    1. 字节码验证
    1. 引用验证

      准备

      准备阶段是正式为类变量(静态变量)分配内存并设置类变量初始值的阶段,类变量所使用的内存都在方法区进行分配(JDK7之前使用永久代实现方法区,JDK8起类变量和Class对象一起存放在Java堆中)。

      解析

      解析阶段是Java虚拟机将常量池内的符号引用替换为直接引用的过程。

      初始化

      开始执行类中编写的Java程序代码,将主导权移交给应用程序,初始化阶段会根据程序员通过程序编码制定的主观计划去初始化类变量和其他资源

03|类加载器

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

类与类加载器

对于任意一个类,都必须有加载它的类加载器和这个类本身一起共同确立其在Java虚拟机中的唯一性。换句话说,如果比较两个类是否相等,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于用一个Class文件,被同一个Java虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等。

双亲委派模型

类加载器类型:

  • 启动类加载器(Bootstrap ClassLoader):

由C++语言实现是虚拟机自身的一部分,负责加载存放在\lib目录,或者被-Xbootclasspath参数所指定的路径中存放的,而且是Java虚拟机能够识别的类库加载到虚拟机的内存中。

  • 其他类加载器:由Java语言实现独自存在于虚拟机外部,皆继承自java.lang.ClassLoader
    • 拓展类加载器(Extension Class Loader)

负责加载\lib\ext目录中,或者被java.ext.dirs系统变量所指定的路径中所有的类库。允许用户将具有通用型的类库放置在ext目录里以拓展Java SE的功能,在jdk9以后,这种拓展机制被模块化带来的天然的拓展能力所取代

  • 应用程序加载器(Application Class Loader)

负责加载用户类路径(ClassPath上所有的类库)

image.png
双亲委派模型的工作过程:
如果一个类加载器收到了类加载请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个请求加载时,子类加载器才会尝试自己去完成加载。