Skip to content

类加载

字数: 0 字 时长: 0 分钟

JVM加载.webp

JVM 类加载包含 LoadingLinkingInitialization 三个步骤

Loading 加载

Loading 就是将 Java 类的字节码文件加载到机器内存中,并在内存中构建出 Java 类的原型:类模板对象

  1. 通过类的全名,获取类的二进制数据流
  2. 解析类的二进制数据流为方法区内的数据结构(Java 类模型)
  3. 创建 java.lang.Class 类的实例,作为方法区这个类的各种数据的访问入口

什么是类模板对象?

类模板对象,就是 Class 对象,JVM 将从字节码文件中解析出的常量池、类字段、类方法等信息存储到类模板中,这样 JVM 在运行期间便能通过类模板而获取到 Java 类中的任何信息;反射的机制也是基于这一基础才能实现。

Linking 链接

Linking 包含三个过程:

  1. 验证(verifaication):验证是否符合 JAVA 语法规范
  2. 准备(preparation):为类的静态变量分配内存,并将其赋默认值,final 常量在此阶段直接赋值
  3. 解析(resolve):将类、接口、字段和方法的符号引用转换为直接引用

Initialization 初始化

初始化阶段,简言之,为类的静态变量赋上正确的初始值。到了初始化阶段,才开始真正执行类中定义的 Java 程序代码,初始化阶段的重要工作是执行类的初始化方法: <clinit>() 方法

  • <clinit>() 方法只能由 JAVA 编译器生成,并由 JVM 调用;只有在给类中的 static 变量显示赋值或在静态代码块中赋值了,才会生成此方法
  • <init>() 方法一定会出现在 Class 的 method 表中,因为每个 Class 都必定有构造器方法
java
public class InitializationTest {
    //非静态字段不会生成 <clinit>
    public int num = 1;
    //静态字段没有显示赋值也不会生成
    public static int num1;
    //静态常量也不会生成
    public static final int num2 = 1;
    //但这种情况会生成 <clinit> 方法,因为调用了方法需要 <clinit> 支撑
    public static final int num3 = Integer.valueOf(100);
}

子类加载前先加载父类

在加载一个类之前,虚拟机总是会试图加载该类的父类,因此父类的 <clinit> 总是在子类 <clinit> 前被调用,也就是说父类的 static 块优先级高于子类

类加载触发条件

Class 只有在必须要首次使用(主动使用)的时候才会被装载,JVM 不会无条件地装载所有 Class 类型。

  1. 创建一个类的实例时,比如使用 new 关键字,或者通过反射、克隆、反序列化

  2. 调用类的静态方法时,即当使用了字节码 invokestatic 指令

  3. 使用类、接口的静态字段时(final 修饰符特殊考虑),比如,使用 getstaticputstatic 指令

  4. 使用 java.lang.reflect 包中的方法反射类的方法时,比如 Class.forName("com.tt.Test");

  5. 初始化子类时,如果发现其父类还没有进行初始化,则需要先触发其父类的初始化

  6. 如果一个接口定义了 default 方法,那么直接实现或者间接实现该接口的类的初始化,该接口要在其之前初始化

  7. 当虚拟机启动时,用户指定的主类 (main() 方法的那个类),虚拟机会先初始化这个主类

被动使用不会触发类的初始化

  • 当访问一个静态字段时,比如通过子类去访问父类的静态字段,但不会触发子类的初始化,只会触发父类的初始化
java
public class Parent{
    static int num = 10;
    static {
        System.out.println("Parent init");
    }
}
public class Child extends Parent {
    static {
        System.out.println("Child init");
    }
}
public static void main(String[] args) {
    //通过子类去访问父类静态字段时,不会触发子类的初始化
    System.out.println(Child.num); // Parent init
}
  • 通过数组定义类引用,不会触发此类的初始化
java
//定义一个 Parent 的数组,此时并不会触发 Parent 的初始化
Parent[] parents = new Parent[10];
  • 引用常量不会触发此类或接口的初始化,因为常量在链接阶段就被显示赋值了
  • 调用 ClassLoaderloadClass() 方法不会触发初始化

类的 Using (使用)

任何一个类型在使用之前都必须经历过完整的加载链接初始化三个阶段。一旦一个类型成功经历这三个阶段后,那么这个类型就进入使用阶段,此时这个类型已经可以正常使用了。

类的 Unloading 卸载

一个类的实例总是引用代表这个类的 Class 对象。在 Object 类中定义了 getClass() 方法,这个方法返回代表对象所属类的 Class 对象的引用。此外,所有 JAVA 类都有一个静态属性 class,它引用代表这个类的 Class 对象。

类、加载器和实例的关系.webp

何种情况类会被卸载

在类的加载器的内部实现中,用一个 Java 集合来存放所加载类的引用,因此要卸载一个类,就得卸载相对应的类的加载器,又得卸载这个类的加载器加载的其他类,因此只有自定义的类的加载器才有可能被卸载

方法区的垃圾回收

方法区的垃圾回收主要回收两部分内容:常量池中废弃的常量不再使用的类型。不再使用的类型的判定条件很苛刻:

  1. 该类的所有实例被回收
  2. 加载该类的类加载器被回收
  3. 该类对应的 Java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法

类的加载器

传统类加载器的层级关系(jdk8及以前)

1. 启动类加载器

启动类加载器(Bootstrap ClassLoader)使用 C/C++ 语言实现,嵌套在 JVM 运行时系统中。它负责加载 Java 的核心库,用于提供 JVM 自身需要的类(包括扩展类加载器应用程序类加载器

2. 扩展类加载器

Java 语言编写,由 sun.misc.Launcher$ExtClassLoader 实现,父类加载器为Bootstrap ClassLoader,负责加载 jre/lib/extjava.ext.dirs 指定目录的类库

3. 系统类加载器

系统类加载器(System ClassLoader),父类加载器为扩展类加载器,负责加载 classpath 的用户类库

4. 自定义类加载器

必要时可以继承 ClassLoader 自定义类加载器,可实现应用隔离,比如 Tomcat 的 WebAppClassLoader

  • 双亲委派模型:优先父类加载,但不是所有类加载器都遵守这个模型,有时候,启动类加载器是可能要加载用户代码的,比如 JDK 的 SPI 机制 (ServiceProvider/ServiceLoader),用户可以在标准 API 框架上提供自己的实现。
  • 可见性:指一个类加载器在加载类时能否看到其他类加载器已加载的类的能力,类加载器能看到父加载器加载的类,但反之不可,这是双亲委派机制的核心,确保类加载的安全性和隔离性
  • 单一性:父加载器加载过的类型不会在子加载器中重复加载;但同一类型可以被邻居加载器加载多次,因为互相并不可见。

双亲委派机制

JVM 类加载器遵循双亲委派模型,即:是收到加载请求时,优先委托父类加载器尝试加载,如果父类加载器失败了,则自己尝试加载

好处是:避免核心类被篡改,比如用户自定义的 java.lang.Object 类不会覆盖默认的 java.lang.Object 类;还可以避免类重复加载,保证类依赖的一致性。

ClassLoader 双亲委派机制源码

java
protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    // 先去往上层找父类加载器
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        //直到最上层,尝试去用 bootstrap 类加载器加载
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }
                
                //父类都不行的话,再从上往下尝试加载
                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    c = findClass(name);

                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

注意

用户自定义类加载器不要重写这个 loadClass 方法,否则双亲委派机制就失效了,推荐去重写 findClass 方法,需要调用 defineClass 方法

Tomcat 破坏双亲委派机制

Tomcat 作为一个 Web 容器而言,可能需要部署多个应用程序,不同的应用程序可能会依赖同一个第三方类库的不同版本,不能要求同一个类库在同一个服务器只有一份,因此 Tomcat 有意设计类加载系统破坏双亲委派机制。

  1. Common ClassLoader 加载所有 Web 应用共享的类(如 Servlet API)
  2. 每个 Web 应用独占一个 WebApp ClassLoader(核心破坏点)加载 /WEB-INF/classes + /WEB-INF/lib/*.jar优先自己加载,失败才委托父类加载器

tomcat 类加载器.webp

jdk9 中类加载结构的新变化

1、jdk9 时基于模块化进行构建,此时的 Java 类库天然满足可扩展需求,原来的扩展机制被移除,扩展类加载器由于向后兼容性的原因被保留,不过被重命名为平台类加载器 PlatformClassLoader

2、启动类加载器、平台类加载器、应用程序类加载器全部继承于 jdk.internal.loader.BuiltinClassLoader

模块类加载器.webp