当前位置: 首页 > news >正文

【JVM】Java虚拟机(三)——类加载与类加载器

目录

一、类加载

(一)类加载概述

类加载的时机

(二)类加载过程详解

1. 加载(Loading)

2. 验证(Verification)

3. 准备(Preparation)

4. 解析(Resolution)

5. 初始化(Initialization)

二、类加载器

(一)类加载器层次结构

(二) 类加载器分类

(1) 启动类加载器(Bootstrap ClassLoader)

(2) 扩展类加载器(Extension ClassLoader)

(3) 应用程序类加载器(Application ClassLoader)

(4) 自定义类加载器

(三)双亲委派模型

(四)自定义类加载器

1. 实现步骤

2. 示例代码

3. 使用场景

(五)类加载器重要方法

1. loadClass() vs findClass()

2. defineClass()

3. resolveClass()

(六)常见问题与解决方案

1. ClassNotFoundException

2. NoClassDefFoundError

3. LinkageError

4. 类加载器内存泄漏

(七)类卸载机制


一、类加载

(一)类加载概述

类加载是 Java 虚拟机(JVM)将类的字节码文件(.class)加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被 JVM 直接使用的 Java 类型的过程。

类加载的时机

  • 创建类的实例

  • 访问类的静态变量

  • 调用类的静态方法

  • 反射调用(Class.forName())

  • 初始化子类时(父类需先初始化)

  • JVM 启动时被标记为启动类的类

(二)类加载过程详解

类加载过程分为五个阶段:加载 → 验证 → 准备 → 解析 → 初始化

1. 加载(Loading)

  • 核心任务:查找并加载类的二进制数据

  • 具体操作

    1. 通过类的全限定名获取其二进制字节流

    2. 将字节流转化为方法区的运行时数据结构

    3. 在堆中创建对应的 java.lang.Class 对象,作为方法区数据的访问入口

示例:

// 当执行以下代码时会触发加载
Class<?> clazz = Class.forName("com.example.MyClass");

2. 验证(Verification)

  • 目的:确保加载的类符合 JVM 规范,不会危害虚拟机安全

  • 验证阶段

    • 文件格式验证(魔数、版本号等)

    • 元数据验证(语义分析)

    • 字节码验证(程序逻辑校验)

    • 符号引用验证(常量池中的引用检查)

3. 准备(Preparation)

  • 核心任务:为类变量(static 变量)分配内存并设置初始值

  • 重要细节

    • 仅分配类变量,不包括实例变量

    • 初始值为数据类型的零值(0, false, null 等)

    • 若变量有 ConstantValue 属性(final static),则直接赋值

示例:

public static int value = 123;
// 准备阶段后 value = 0
// 初始化阶段后 value = 123

4. 解析(Resolution)

  • 核心任务:将常量池中的符号引用替换为直接引用

  • 解析类型

    • 类或接口解析

    • 字段解析

    • 类方法解析

    • 接口方法解析

符号引用 vs 直接引用:

  • 符号引用:用一组符号描述所引用的目标

  • 直接引用:指向目标的指针、偏移量或句柄

5. 初始化(Initialization)

  • 核心任务:执行类构造器 <clinit>() 方法

  • 重要细节

    • <clinit>() 由编译器自动收集类中的所有类变量的赋值动作静态代码块生成

    • JVM 保证子类的 <clinit>() 执行前,父类的 <clinit>() 已执行

    • 多线程环境下会被正确加锁同步

示例:

public class InitializationExample {static {System.out.println("静态代码块执行");}public static int value = 123;
}

二、类加载器

(一)类加载器层次结构

(二) 类加载器分类

(1) 启动类加载器(Bootstrap ClassLoader)
  • C++ 实现,是 JVM 的一部分

  • 负责加载 Java 核心库(<JAVA_HOME>/lib 目录)

  • 不继承 java.lang.ClassLoader

  • 获取时为 null

(2) 扩展类加载器(Extension ClassLoader)
  • sun.misc.Launcher$ExtClassLoader 实现

  • 负责加载扩展库(<JAVA_HOME>/lib/ext 目录)

  • 父加载器为启动类加载器

(3) 应用程序类加载器(Application ClassLoader)
  • sun.misc.Launcher$AppClassLoader 实现

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

  • 默认的类加载器

  • 父加载器为扩展类加载器

(4) 自定义类加载器
  • 继承 java.lang.ClassLoader

  • 可实现热部署、模块隔离等特殊需求

(三)双亲委派模型

  • 工作原则

    1. 收到加载请求时,先委托给父加载器

    2. 父加载器无法完成时,自己尝试加载

  • 工作流程

  • 优点

    1. 避免重复加载

    2. 保证核心类库安全

    3. 确保类的一致性(如 java.lang.Object)

  • 代码实现

protected Class<?> loadClass(String name, boolean resolve) {synchronized (getClassLoadingLock(name)) {// 1. 检查类是否已加载Class<?> c = findLoadedClass(name);if (c == null) {try {// 2. 委托父加载器if (parent != null) {c = parent.loadClass(name, false);} else {c = findBootstrapClassOrNull(name);}} catch (ClassNotFoundException e) {}if (c == null) {// 3. 自己尝试加载c = findClass(name);}}if (resolve) {resolveClass(c);}return c;}
}

破坏双亲委派模型

  • 场景

    1. SPI 服务加载(如 JDBC)

    2. OSGi 模块化系统

    3. 热部署需求

  • 解决方案:线程上下文类加载器

// 设置上下文类加载器
Thread.currentThread().setContextClassLoader(customClassLoader);// 获取上下文类加载器
ClassLoader loader = Thread.currentThread().getContextClassLoader();

(四)自定义类加载器

1. 实现步骤

  1. 继承 java.lang.ClassLoader

  2. 重写 findClass() 方法

  3. 在 findClass() 中实现类加载逻辑

  4. 调用 defineClass() 将字节数组转为 Class 对象

2. 示例代码

public class CustomClassLoader extends ClassLoader {private final String classPath;public CustomClassLoader(String classPath) {this.classPath = classPath;}@Overrideprotected Class<?> findClass(String name) throws ClassNotFoundException {byte[] classData = loadClassData(name);if (classData == null) {throw new ClassNotFoundException();}return defineClass(name, classData, 0, classData.length);}private byte[] loadClassData(String className) {String path = classPath + File.separatorChar + className.replace('.', File.separatorChar) + ".class";try (InputStream ins = new FileInputStream(path);ByteArrayOutputStream baos = new ByteArrayOutputStream()) {int bufferSize = 4096;byte[] buffer = new byte[bufferSize];int bytesNumRead;while ((bytesNumRead = ins.read(buffer)) != -1) {baos.write(buffer, 0, bytesNumRead);}return baos.toByteArray();} catch (IOException e) {e.printStackTrace();}return null;}
}

3. 使用场景

  • 热部署:不重启应用更新类

  • 模块隔离:不同模块使用不同版本库

  • 代码加密:加载加密的类文件

  • 从非标准位置加载类(网络、数据库等)

(五)类加载器重要方法

1. loadClass() vs findClass()

  • loadClass():实现双亲委派逻辑

  • findClass():自定义类加载的实际加载点

2. defineClass()

  • 将字节数组转为 Class 对象

  • 核心的类定义方法

  • 通常由 findClass() 调用

3. resolveClass()

  • 执行类的连接(解析阶段)

  • 可选调用,可使类提前完成解析

(六)常见问题与解决方案

1. ClassNotFoundException

  • 原因:类加载器找不到类定义

  • 解决:检查类路径和类名拼写

2. NoClassDefFoundError

  • 原因:编译时存在,运行时缺少类依赖

  • 解决:确保所有依赖类在运行时可用

3. LinkageError

  • 原因:类加载过程中出现不一致

  • 解决:检查类版本冲突和类加载器隔离

4. 类加载器内存泄漏

  • 原因:类加载器未释放导致加载的类无法卸载

  • 解决:合理设计类加载器生命周期

(七)类卸载机制

  • 条件

    1. 类的所有实例已被回收

    2. 类的 Class 对象没有被引用

    3. 加载该类的 ClassLoader 已被回收

  • 注意:由启动类加载器加载的类不会被卸载

相关文章:

  • [创业之路-410]:经济学 - 国富论的核心思想和观点,以及对创业者的启发
  • Golang 面试经典题:map 的 key 可以是什么类型?哪些不可以?
  • 2025-06-02-IP 地址规划及案例分析
  • OD 算法题 B卷【反转每对括号间的子串】
  • Secs/Gem第八讲(基于secs4net项目的ChatGpt介绍)
  • 剑指offer19_链表中倒数第k个节点
  • Netty集群搭建
  • python打卡day48
  • PandasAI使用
  • 深入解析 Pandas 核心数据结构:Series 与 DataFrame
  • 重读《人件》Peopleware -(15)Ⅱ 办公环境 Ⅷ 撑伞之步:构建理想办公环境(下)
  • 【C++系列】智能指针自定义析构
  • 6.5 自学测试 数据库基础 Day5
  • C++11 右值引用
  • 第十八章 归档与备份
  • python打卡训练营打卡记录day48
  • JS的数据类型分类
  • 自动交换两个文件的文件名 VSB脚本技巧 电脑技巧
  • java面试:JAVA并发篇
  • 《双指针》题集
  • 做网站做地区好还是全国的好/班级优化大师
  • 网站favicon.ico尺寸/seo优化方法网站快速排名推广渠道
  • 襄阳的网站建设公司/扫一扫识别图片
  • 沙漠风网站建设/电脑系统优化工具
  • 问佛教网站大师做早课烧香烛可以吗/公司产品推广文案
  • 怎么快速提高网站权重/seo网络优化招聘