JVM4️⃣类加载技术

1、类加载阶段

类加载的阶段:加载、链接、初始化

1.1、加载

加载将 Java 类的字节码载入方法区

  1. 方法区内部采用 C++ 的 instanceKlass 描述 Java 类。
  2. 加载和链接可能交替运行,不一定是先后完成。
  3. 类加载时发现父类没有加载,则先加载父类。

浅聊 oop-klass 模型

以 JDK 8 为例。

在 Hotspot 中,使用 oop-klass 模型,用于表示 Java 类和对象。

  • oop(ordinary object pointer):普通对象指针,标识对象实例。
  • klass(instanceKlass):类的元数据,表示类的成员变量、方法、常量池等结构。

简单来说,就是 Java 面向对象中,类与实例的关系。

  • 堆中的对象(xx.class)是一个个的实例。
  • 实例所属的类(klass),位于方法区。

JVM 不把 instanceKlass 暴露给 Java,也就是说 Java 不能直接访问 instanceKlass。

那 Java 如何访问到 Klass?

使用 new 关键字创建对象时,JVM 会创建一个 instanceOopDesc 实例,用于表示 java.lang.Class 对象的实例。

  • instanceOopDesc 实例,称为 instanceKlass 的 Java 镜像
  • 对象实例存放在堆中,对象实例的引用存放在栈中。
  • 实例包括两部分信息
    • 对象头:包含运行时数据,如多线程的锁信息
    • 元数据:维护一个指针,指向对象所属类的 instanceKlass
  • instanceKlass 中有一个 _java_mirror 属性,指向 instanceOopDesc 实例
  • 可以看出,instanceOopDesc 实例和 instanceKlass 之间互相持有对对方的引用

1.2、链接

链接阶段:验证、准备、解析

  • 验证:类是否符合 JVM 规范,安全性检查(e.g. 魔数 cafe babe)
  • 准备为 static 变量分配空间,设置默认值
  • 解析将常量池中的符号引用解析为直接引用

1.2.1、验证

类是否符合 JVM 规范,安全性检查

如:检查魔数,是否为 cafe babe

1.2.2、准备(❗)

为 static 变量分配空间,设置默认值。

  • static 变量的存储位置

    • < JDK 1.7:存储于 instanceKlass 的末尾(本地内存)
    • >=JDK 7:存储于 instanceOopDesc 实例的末尾(堆)
  • 赋值问题

    • 在准备阶段,static 变量仅分配空间和赋默认初始值

    • 在初始化阶段,才对变量赋值。

      // 举例
      static Object o = new Object();
      // 准备阶段:分配内存空间,赋初始值为null
      // 初始化阶段:赋值为new Object()
      
  • 注意若 static 变量是 final 的基本类型、字符串常量,则在准备阶段赋值

1.2.3、解析

Oracle 官方文档中说到,常量池在最初都是符号引用

解析阶段:将符号引用解析为直接引用。

  • 符号引用

    image-20220304204408022

  • 直接引用

    image-20220304204413941

1.3、初始化(❗)

初始化调用<cinit>()V方法。

  • JVM 会保证 <cinit>()V 的线程安全。
  • 类初始化是懒惰的,可以理解为非必要不初始化。

1.3.1、触发时机

主动引用一个类时,会触发初始化。

  1. main() 方法所在的类
  2. new 关键字创建对象
  3. Class.forName()
  4. 首次访问类的静态变量或静态方法(除 final 常量)
  5. 子类初始化,发现父类还没初始化:先触发父类的初始化
  6. 子类访问父类的静态变量:只触发父类初始化

1.3.2、何时不触发

当一个类被动引用时,不触发初始化。

  1. 访问类的静态常量(static final,基本类型或字符串)
  2. 获取某个类的 Class:类对象.class
  3. 创建某个类的数组
  4. Class.forName():第二个参数为 false,则触发加载,不触发初始化
  5. 类加载器的 loadClass():触发加载,不触发初始化

1.3.3、经典应用:单例模式

单例模式:静态内部类方式。

public final class Singleton{
    private Singleton(){}

    private static class LazyHolder{
        private static final Singleton INSTANCE = new Singleton();
    }

    public Singleton getInstance(){
        return LazyHolder.INSTANCE;  
    }
}

2、类加载器

类加载器在类加载阶段,通过一个类的全限定名来获取此类的二进制字节流

从 JVM 的角度,只有 2 种加载器

  • 启动类加载器(Bootstrap ClassLoader):C++ 实现,属于虚拟机的一部分
  • 其它类加载器: Java 实现(继承自抽象类 java.lang.ClassLoader),独立于虚拟机,。

从 Java 程序员的角度,有 3 种加载器

在有需要的情况下,还可以编写自定义的类加载器。

说明

  1. 启动类加载器无法被 Java 程序访问,显示为 null
  2. 应用程序类加载器是系统默认的类加载器
名称 默认加载范围
Bootstrap
ClassLoader
启动类(根) JAVA_HOME/jre/lib/*
Extension
ClassLoader
扩展类 JAVA_HOME/jre/lib/ext
Application
ClassLoader
应用程序类(系统类) classpath

2.1、双亲委派模型

The parent-delegation Model

双亲委派模型调用类加载器的 loadClass() 方法时,查找类的规则。

  • :理解为上级比较好,而不是“父亲”(因为不是继承关系,而是级别关系)

  • :理解为 2 个(应用程序类加载器,有 2 级垂直结构的上级,会向上委派两次)

    image-20211220171114248

2.1.1、工作机制(❗)

向上委派,向下加载

  1. 一个类加载器收到了类加载的请求。
  2. 把请求委托给父加载器,依次向上直到启动类加载器
  3. 从启动类加载器开始:
    • 根据类的全限类名,在加载范围内进行扫描查找。
    • 找到则进行加载,否则由下一级类加载器尝试加载
  4. 如果所有类加载器都无法完成加载,则抛出异常。

2.1.2、源码:loadClass()

  • name:类的二进制名

  • resolve:若 true 则解析类(链接阶段)

    image-20220304223253922

2.1.3、源码分析(❗)

大处着眼:方法结构

方法体:分为以下几个部分

  • findLoadedClass():根据类名,查找对应的已加载类

  • if (c == null):检查类是否已加载,未加载则进入 if 块。

  • if (resolve):根据方法调用传递的参数,判断是否要解析类。

  • 返回被加载的类,若无法加载则为 null

    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        synchronized (getClassLoadingLock(name)) {
            Class<?> c = findLoadedClass(name);
    
            if (c == null){
                ...
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }
    

小处着手:具体代码(❗)

类加载的核心代码:if (c == null) 块

分为 try 块和 if 块,分别对应双亲委派模型的 “向上委托” 和 “向下加载”

  • try 块向上委托,直至启动类加载器
    1. 若 parent == null,说明上级是启动类加载器
    2. 此时调用 findBootstrapClassOrNull()查找启动类加载器中的对应加载类(根据全限类名)
      • 若返回被加载的类,说明 c 已被启动类加载器加载。
      • 若返回 null,说明启动类加载器无法加载当前类。
  • if 块向下加载
    • 若 c == null,说明上级类加载器无法完成类加载操作
    • 调用 findClass()则当前的类加载器尝试去加载类(根据全限类名扫描)
      • 若加载成功,则返回被加载后的类。
      • 否则,抛出 ClassNotFoundException 异常
      • (下一级类加载器会捕获异常,但不做任何操作)

可以看出:双亲委派模型就是从应用程序类加载器开始,递归向上级调用 loadClass() 方法

  • 只要在某次递归中,c != null 说明 c 已被加载。
  • 若递归结束,c == null 说明 c 无法被加载,抛出 ClassNotFoundException 异常

类解析if (resolve)

根据调用 loadClass() 时传递的参数。

  • 若显式指定 resovle == true,则会触发类链接。

  • 若显式指定为 false 或不指定,则不会触发类链接

    // loadClass()的重载方法,默认为false
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        return loadClass(name, false);
    }
    

2.2、典例:自定义String

2.2.1、示例

自定义 java.lang.String

package java.lang;

public class String {
    public static void main(String[] args) {
        System.out.println("I'm the true String, haha!");
    }
}

执行 main()

image-20220305003734661

2.2.2、分析

main() 所在的类会触发初始化,而初始化之前需要经过加载和链接。

  1. 类加载器收到类加载请求,依次向上委托父加载器,直到启动类加载器
  2. 启动类加载器:扫描 JAVA_HOME/jre/lib/* 中的类,发现全限类名为java.lang.String的类加载。
  3. 类加载阶段完成,执行 main() 方法
  4. 此时加载的是真正的 java.lang.String,其中没有定义 main(),因此报错。
posted @ 2022-03-04 18:10  Jaywee  阅读(58)  评论(0编辑  收藏  举报

👇