Bootstrap

图解JVM - 16.类的加载过程(类的生命周期)详解

1. 概述

Java虚拟机的类加载机制是Java生态体系的基石,理解类的生命周期对诊断内存泄漏、性能调优和解决类加载冲突至关重要。如图1所示,类的完整生命周期包括加载、链接、初始化、使用和卸载五个阶段,其中链接阶段又细分为验证、准备、解析三个子阶段:

需要特别注意的是:

  1. 这些阶段的开始顺序是固定的,但各阶段可能交叉进行
  2. 初始化之前的三个阶段(加载、链接)都是JVM控制的
  3. 初始化阶段才是真正开始执行Java程序代码

2. 过程一:Loading(加载)阶段

2.1 加载完成的操作

加载阶段需要完成三个核心任务:

技术细节:

  1. 二进制流不限定于Class文件格式(可实现动态代理)
  2. 方法区的数据结构包含版本号、字段表、方法表等元数据
  3. Class对象存储在堆中,作为方法区数据的访问入口

2.2 二进制流的获取方式

类加载器获取二进制流的七种典型场景:

加载场景实现机制示例
本地文件系统文件I/O读取JAR包中的类
网络加载URLClassLoaderApplet应用
运行时计算生成ProxyGenerator生成字节码动态代理
加密文件加载自定义ClassLoader解密商业软件保护
动态生成JSP→Servlet转换Web容器中的JSP
数据库存储JDBC读取BLOB字段企业配置中心
多版本加载Multi-Release JAR特性JDK9+模块化

2.3 类模型与Class实例的位置

内存结构关系示意图:

2.4 数组类的加载

数组类的加载具有特殊性:

  1. 数组元素类型为引用类型时:延迟到实际使用时才加载元素类
  2. 数组类本身由JVM直接创建,但遵循以下规则:

加载过程特点:

  • 数组类可见性 = 元素类型可见性
  • 基础类型数组无类加载器
  • 引用类型数组的加载器与元素类型一致

3. 过程二:Linking(链接)阶段

3.1 环节1:验证(Verification)

验证是保障JVM安全的第一道防火墙,通过四重验证机制确保字节码符合规范:

技术细节解析:

  1. 文件格式验证(文件→方法区映射)
    • 魔数验证:检查头4字节是否为<font style="background-color:rgb(252, 252, 252);">0xCAFEBABE</font>
    • 版本号检查:JDK版本兼容性验证(向下兼容原则)
    • 常量池tag标志验证:如<font style="background-color:rgb(252, 252, 252);">CONSTANT_Class_info</font>的tag值必须为7
  2. 元数据验证(语义分析)
// 示例:非法继承验证
public class MyString extends String { } // 报错:不能继承final类
  1. 字节码验证(最复杂阶段)
    • 类型检查:操作数栈与指令参数类型匹配
    • 控制流验证:确保不存在跳转到方法体外的指令
    • 使用<font style="background-color:rgb(252, 252, 252);">StackMapTable</font>属性加速验证(JDK7+)
  2. 符号引用验证(准备解析的前置检查)
    • 检查引用的类/字段/方法是否存在
    • 验证访问权限(如protected方法的跨包调用)

3.2 环节2:准备(Preparation)

此阶段完成类变量的内存分配和默认初始化:

关键技术点:

  • 普通static变量分配内存并赋零值(如int=0,对象=null)
  • final static变量直接赋真实值(编译期优化)
  • 特殊数据类型处理:
    • double/long占用两个Slot
    • 引用类型存储为压缩指针(开启指针压缩时)

3.3 环节3:解析(Resolution)

将符号引用转换为直接引用的过程:

解析类型详解:

引用类型解析目标可能异常
类引用对应类的Class对象NoClassDefFoundError
字段引用字段的偏移量NoSuchFieldError
方法引用方法入口地址NoSuchMethodError
接口方法引用接口方法表索引AbstractMethodError

延迟解析策略:

// 示例:解析可能延后到实际使用时
public class DynamicResolver {
    void callMethod() {
        // 此处的MethodRef可能到执行时才解析
        System.getProperty("key"); 
    }
}

4. 过程三:Initialization(初始化)阶段

4.1 static与final的搭配问题

不同修饰符组合的初始化差异:

内存分配示例:

class InitDemo {
    static int a = 1;          // 准备阶段a=0 → 初始化阶段a=1
    static final int b = 2;    // 准备阶段直接b=2
    final int c = 3;           // 每个对象独立分配
}

4.2 <font style="background-color:rgb(252, 252, 252);"><clinit>()</font>的线程安全性

类构造方法的执行机制:

关键特性:

  • 同步锁保证多线程环境只执行一次
  • 可能引发死锁(需避免在<font style="background-color:rgb(252, 252, 252);"><clinit></font>中创建线程)

4.3 类的初始化情况:主动使用vs被动使用

主动使用的12种场景:

  1. <font style="background-color:rgb(252, 252, 252);">new</font>关键字实例化
  2. 访问类的静态变量(非final)
  3. 调用类的静态方法
  4. 反射调用(Class.forName)
  5. 初始化子类触发父类初始化
  6. 作为JVM启动类(main方法所在类)
  7. 动态语言支持(MethodHandle)
  8. 接口默认方法(实现类初始化)
  9. 访问<font style="background-color:rgb(252, 252, 252);">java.lang.invoke</font>相关API
  10. 模块系统初始化(JDK9+)
  11. 嵌套类初始化(静态内部类)
  12. 枚举值访问

被动使用示例:

class Parent {
    static int value = 10;
    static { System.out.println("Parent初始化"); }
}

class Child extends Parent {
    static { System.out.println("Child初始化"); }
}

// 测试代码
public class Test {
    public static void main(String[] args) {
        System.out.println(Child.value); // 仅触发Parent初始化
    }
}

5. 过程四:类的Using(使用)

类进入运行阶段后的内存模型:

使用阶段的三种典型场景:

  1. 方法调用:通过方法表实现多态
  2. 字段访问:对象头中的类型指针定位字段偏移量
  3. 异常处理:异常表匹配处理代码位置

6. 过程五:类的Unloading(卸载)

6.1 引用关系模型

6.2 类的生命周期完整图示

6.3 卸载条件检查表

条件检查方法
所有实例被回收堆中无该类对象
ClassLoader被回收可达性分析
Class对象无引用-Xlog:class+unload=debug
非JVM系统类排除java.*, javax.*等

6.4 方法区垃圾回收

HotSpot虚拟机实现:

回收触发条件:

  • Full GC时检查元空间使用率
  • 使用<font style="background-color:rgb(252, 252, 252);">MetaspaceSize</font>控制元空间大小
  • 启用<font style="background-color:rgb(252, 252, 252);">-XX:+ClassUnloading</font>参数

7. 类的加载过程常见问题与解决方案

7.1 ClassNotFoundException vs NoClassDefFoundError

典型案例分析:

// 场景1:缺少依赖JAR
public class MissingDependency {
    public static void main(String[] args) {
        new org.apache.commons.lang3.StringUtils(); // NoClassDefFoundError
    }
}

// 场景2:动态加载不存在类
public class DynamicLoading {
    public static void main(String[] args) throws Exception {
        Class.forName("NonExistClass"); // ClassNotFoundException
    }
}

解决方案:

  1. 检查classpath配置(Maven/Gradle依赖)
  2. 使用<font style="background-color:rgb(252, 252, 252);">-verbose:class</font>参数追踪类加载
  3. 排查类加载器作用域(OSGi容器特别需要注意)

7.2 多版本类冲突

使用Mermaid展示依赖树冲突:

解决策略:

  1. Maven的<font style="background-color:rgb(252, 252, 252);"><exclusion></font>标签排除冲突
  2. 使用自定义类加载器隔离(如Tomcat的WebappClassLoader)
  3. 模块化改造(JDK9+的module-info.java)

7.3 静态初始化死锁

死锁产生条件图示:

预防措施:

  1. 避免在静态块中创建线程
  2. 不要交叉引用类的静态变量
  3. 使用懒加载模式(Holder模式)

8. 高频面试问题与解答

8.1 类加载过程各阶段做了什么?

答: 三级加载机制图示:

8.2 双亲委派机制如何工作?

关键点:

  • 防止核心类被篡改
  • 避免重复加载
  • 实现方式:<font style="background-color:rgb(252, 252, 252);">loadClass()</font>方法逻辑

8.3 如何打破双亲委派?

应用场景:

  1. Tomcat的Web应用隔离
  2. SPI服务发现机制(JDBC驱动加载)
  3. 热部署实现

代码示例:

class HotSwapLoader extends ClassLoader {
    @Override
    protected Class<?> loadClass(String name, boolean resolve) 
    throws ClassNotFoundException {
        // 自定义加载逻辑
        if (name.startsWith("com.example")) {
            return findClass(name);
        }
        return super.loadClass(name, resolve);
    }
}

8.4 <font style="background-color:rgb(252, 252, 252);"><clinit></font><font style="background-color:rgb(252, 252, 252);"><init></font>的区别

对比表格:

特性
执行时机类初始化阶段对象实例化时
内容组成静态变量赋值+静态块实例变量赋值+构造代码块
线程安全JVM保证需自行同步
调用顺序父类先于子类先执行父类构造器
是否必需没有静态内容则不生成至少包含默认构造器

8.5 如何实现热部署?

技术方案:

注意事项:

  1. 使用单独的类加载器实例
  2. 注意静态状态丢失问题
  3. 使用JMX等管理接口控制

全篇总结: 通过本文的系统讲解,读者应该掌握:

  1. 类加载过程的五个阶段及其技术细节
  2. 内存模型与各数据结构的关联关系
  3. 类加载异常的分析与解决思路
  4. 高频面试题的深度解答技巧
  5. 实际开发中的类加载最佳实践
;