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

JVM学习笔记-----类加载

类加载

类加载阶段


加载

  • 将类的字节码载入方法区中,内部采用 C++ 的 instanceKlass 描述 java 类,它的重要 field 有:
    • _java_mirror 即 java 的类镜像,例如对 String 来说,就是 String.class,作用是把 klass 暴露给 java 使用
    • _super 即父类
    • _fields 即成员变量
    • _methods 即方法
    • _constants 即常量池
    • _class_loader 即类加载器
    • _vtable 虚方法表
    • _itable 接口方法表
  • 如果这个类还有父类没有加载,先加载父类
  • 加载和链接可能是交替运行的

注意

instanceKlass 这样的【元数据】是存储在方法区(1.8 后的元空间内),但 _java_mirror 是存储在堆中

可以通过前面介绍的 HSDB 工具查看

Person.class 的字节码被加载到 JVM 的方法区,JVM 会基于其中的信息创建 instanceKlass 等内部结构来管理类

链接

验证

验证类是否符合JVM规范,做一些安全性检查

准备

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

  • static 变量在 JDK 7 之前存储于 instanceKlass 末尾,从 JDK 7 开始,存储于 _java_mirror 末尾
  • static 变量分配空间和赋值是两个步骤,分配空间在准备阶段完成,赋值在初始化阶段完成
  • 如果 static 变量是 final 的基本类型,那么编译阶段值就确定了,赋值在准备阶段完成
  • 如果 static 变量是 final 的,但属于引用类型,那么赋值也会在初始化阶段完成

静态变量在堆中跟类对象存储在一起

解析

将常量池中的符号引用解析为直接引用

符号引用仅仅就只是一个符号,它并不知道这些符号在哪个内存的位置,但经过解析以后变成直接引用就能确切的找到类/方法等在内存中的位置

初始化

<clinit>() V 方法
初始化即调用<clinit>() V,虚拟机会保证这个类的『构造方法』的线程安全

发生的时机
概括得说,类初始化是【懒惰的】

  • main 方法所在的类,总会被首先初始化
  • 首次访问这个类的静态变量或静态方法时
  • 子类初始化,如果父类还没初始化,会引发
  • 子类访问父类的静态变量,只会触发父类的初始化
  • Class.forName
  • new 会导致初始化

不会导致类初始化的情况

  • 访问类的 static final 静态常量(基本类型和字符串)不会触发初始化(准备阶段)
  • 类对象.class 不会触发初始化
  • 创建该类的数组不会触发初始化
  • 类加载器的 loadClass 方法
  • Class.forName的参数2为false时

类加载器

以JDK8为例:

名称加载哪的类说明
Bootstrap ClassLoaderJAVA_HOME/jre/lib无法直接访问
Extension ClassLoaderJAVA_HOME/jre/lib/ext上级为 Bootstrap,显示为 null
Application ClassLoaderclasspath上级为 Extension
自定义类加载器自定义上级为 Application

各司其职,各管一块

Application ClassLoader去加载类的时候首先会问一问看一下这个类是不是由它的上级Extension ClassLoader加载过了,如果没有它还会问问它的上级Bootstrap ClassLoader是否加载过了,如果它的两个上级都没有加载那才轮的到Application ClassLoader去加载。

这种类的委托方式被称为双亲委派的类加载方式

启动类加载器

C++编写

扩展类加载器

应用程序类加载器

双亲委派模式

所谓的双亲委派,就是指调用类加载器的loadclass方法时,查找类的规则。

protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {// 加锁:保证同一时刻,一个类只会被一个线程加载,避免重复加载或加载冲突synchronized (getClassLoadingLock(name)) {// 1. 检查该类是否 **已经被加载过** // 类加载器内部维护已加载类缓存,先查缓存,避免重复加载Class<?> c = findLoadedClass(name);if (c == null) { long t0 = System.nanoTime(); // 记录开始时间,用于性能统计try {if (parent != null) { // 2. 有父加载器:委派 **父加载器** 尝试加载 // 体现 “双亲委派” 机制,优先让父加载器处理,保证基础类由更上层加载c = parent.loadClass(name, false); } else { // 3. 无父加载器(如 ExtClassLoader ,其 “父” 是 Bootstrap ,用特殊方式处理)// 尝试让 Bootstrap ClassLoader 加载(Bootstrap 是 C++ 实现,Java 层显式调用特殊方法)c = findBootstrapClassOrNull(name); }} catch (ClassNotFoundException e) {// 父加载器加载失败:这里不处理,后续走自己的 findClass 逻辑}// 4. 如果父加载器 **层层委派都没找到** ,调用当前加载器的 findClass 加载if (c == null) { long t1 = System.nanoTime(); // findClass 是模板方法,子类(如自定义类加载器)可重写,实现自定义加载逻辑(比如从网络、加密文件加载)c = findClass(name); // 5. 性能统计:记录类加载耗时、次数等(JVM 内部性能监控用)sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);sun.misc.PerfCounter.getFindClasses().increment();}}// 6. 如果需要解析(resolve=true),进行类的解析:把符号引用转为直接引用,准备静态变量、字节码验证等 if (resolve) {resolveClass(c);}return c;}
}// 模板方法:默认实现抛异常,让子类(如自定义类加载器)按需重写,实现自定义加载逻辑
protected Class<?> findClass(String name) throws ClassNotFoundException {throw new ClassNotFoundException(name);
}// 关键辅助方法:获取类加载锁,保证同一类加载的线程安全 
protected Object getClassLoadingLock(String className) {// 简单实现:通常用 ConcurrentHashMap 存锁,保证一个类对应一把锁return this; 
}// 访问 Bootstrap ClassLoader 的特殊方法(实际是 JVM 内部实现,这里简化示意)
private native Class<?> findBootstrapClassOrNull(String name); 
  1. 优先检查缓存:类加载时首先查询已加载类缓存(findLoadedClass),避免重复加载,提升效率,这是类加载的第一道检查机制。
  2. 双亲委派加载:若父加载器时优先委派父加载器加载,无父加载器则尝试 Bootstrap 加载器,通过层级委派保证基础类加载的一致性和安全性,防止类重复定义。
  3. 自身加载兜底:若父加载器均加载失败,当前类加载器调用 findClass 方法自行加载,该方法为模板方法,支持子类重写实现自定义加载逻辑(如从网络、加密文件加载)。

以下是对双亲委派机制优势总结:

优势
保证 Java 核心库安全性核心库(如 java.lang.Object )由启动类加载器加载,其基于 JVM 本地代码实现,加载路径固定,避免核心类被篡改,保障系统安全稳定
避免类重复加载若类已被父类加载器加载,子类加载器不再重复加载,减少冗余,提升类加载效率,降低内存消耗
保证类加载一致性确保同一类在 JVM 中仅有一个定义,规避类冲突。如应用与第三方库类名相同时,优先加载高层次类加载器中的类
提高类加载效率类加载请求向上委派,父类加载器若加载过该类,直接返回引用,减少重复加载开销
支持动态扩展不同类加载器分工加载不同类,像应用类加载器加载应用特定类、扩展类加载器加载扩展库类,便于动态扩展与模块化开发

线程上下文类加载器

使用 JDBC 时,早年需显式调用 Class.forName("com.mysql.jdbc.Driver") 加载驱动,如今不写这句代码,驱动仍可能自动加载。核心疑问:谁在 “暗中” 触发了 Driver 类的加载?

JDBC 核心类 DriverManager 中有静态代码块,会在类加载时自动执行:

public class DriverManager {// 注册驱动的集合private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();// 初始化驱动(类加载阶段执行)static {loadInitialDrivers(); // 关键方法:触发驱动自动加载println("JDBC DriverManager initialized");}
}

DriverManager是属于启动类路径下的,它的类加载器是Bootstrap ClassLoader,会到 JAVA_HOME/jre/lib 下搜索类,但 JAVA_HOME/jre/lib 下显然没有 mysql-connector-java-5.1.47.jar 包,这样问题来了,在 DriverManager 的静态代码块中,怎么能正确加载 com.mysql.jdbc.Driver 呢?

private static void loadInitialDrivers() {String drivers = null;// 1. 从系统属性 `jdbc.drivers` 读取驱动类名(SPI 加载的补充)try {drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {public String run() {return System.getProperty("jdbc.drivers");}});} catch (Exception ex) {drivers = null;}// 1) 使用 ServiceLoader(SPI 机制)加载驱动 AccessController.doPrivileged(new PrivilegedAction<Void>() {public Void run() {ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);Iterator<Driver> driversIterator = loadedDrivers.iterator();try {while (driversIterator.hasNext()) {// 关键:触发类加载 + 自动注册driversIterator.next(); }} catch (Throwable t) {// 捕获异常,不中断流程}return null;}});println("DriverManager.initialize: jdbc.drivers = " + drivers);// 2) 使用 `jdbc.drivers` 定义的驱动名,通过系统类加载器加载 if (drivers == null || drivers.equals("")) {return;}String[] driversList = drivers.split(":");println("number of Drivers:" + driversList.length);for (String aDriver : driversList) {try {println("DriverManager.Initialize: loading " + aDriver);// 使用系统类加载器(Application ClassLoader)加载Class.forName(aDriver, true, ClassLoader.getSystemClassLoader()); } catch (Exception ex) {println("DriverManager.Initialize: load failed: " + ex);}}
}

jdk在某些情况下打破双亲委派机制,调用系统类加载器,否则有些类是找不到的。

自定义类加载器

什么时候需要自定义类加载器

1)想加载非 classpath 随意路径中的类文件

2)都是通过接口来使用实现,希望解耦时,常用在框架设计

3)这些类希望予以隔离,不同应用的同名类都可以加载,不冲突,常见于 tomcat 容器

步骤:

1. 继承 ClassLoader 父类

2. 要遵从双亲委派机制,重写 findClass 方法 注意不是重写 loadClass 方法,否则不会走双亲委派机制

3. 读取类文件的字节码

4. 调用父类的 defineClass 方法来加载类

5. 使用者调用该类加载器的 loadClass 方法

import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;/*** 自定义类加载器实现* 遵循双亲委派机制,用于加载指定路径的类文件*/
public class CustomClassLoader extends ClassLoader {// 类文件所在的基础路径(可根据实际需求修改)private String basePath;public CustomClassLoader(String basePath) {// 调用父类构造方法,确保双亲委派链完整super();this.basePath = basePath;}/*** 步骤2:重写findClass方法(核心)* 不重写loadClass,保证双亲委派机制生效* 当父加载器无法加载类时,会自动调用此方法*/@Overrideprotected Class<?> findClass(String className) throws ClassNotFoundException {try {// 步骤3:读取类文件的字节码byte[] classData = loadClassData(className);if (classData == null) {throw new ClassNotFoundException("类字节码读取失败: " + className);}// 步骤4:调用父类的defineClass方法加载类// 该方法将字节码转换为Class对象,是类加载的关键步骤return defineClass(className, classData, 0, classData.length);} catch (IOException e) {throw new ClassNotFoundException("加载类时发生IO异常: " + className, e);}}/*** 读取类文件的字节数组(步骤3的具体实现)* @param className 类的全限定名(如com.example.Test)* @return 类文件的字节数组* @throws IOException 读取文件时可能发生的异常*/private byte[] loadClassData(String className) throws IOException {// 将类名转换为文件路径(如com.example.Test -> com/example/Test.class)String path = basePath + "/" + className.replace('.', '/') + ".class";try (InputStream is = new FileInputStream(path);ByteArrayOutputStream baos = new ByteArrayOutputStream()) {byte[] buffer = new byte[1024];int len;// 读取类文件内容到字节数组输出流while ((len = is.read(buffer)) != -1) {baos.write(buffer, 0, len);}return baos.toByteArray();}}/*** 使用示例(步骤5:使用者调用loadClass方法)*/public static void main(String[] args) {try {// 创建自定义类加载器,指定类文件所在的基础路径CustomClassLoader classLoader = new CustomClassLoader("/path/to/classes");// 步骤5:调用loadClass方法(继承自父类ClassLoader)// 该方法会先触发双亲委派机制,父加载器无法加载时才调用自定义的findClassClass<?> clazz = classLoader.loadClass("com.example.User");// 验证加载结果System.out.println("类加载器: " + clazz.getClassLoader()); // 输出自定义类加载器Object instance = clazz.newInstance();System.out.println("实例创建成功: " + instance);} catch (Exception e) {e.printStackTrace();}}
}

http://www.lryc.cn/news/625175.html

相关文章:

  • FPGA-Vivado2017.4-建立AXI4用于单片机与FPGA之间数据互通
  • Google 的 Opal:重新定义自动化的 AI 平台
  • WPF 打印报告图片大小的自适应(含完整示例与详解)
  • Rust 入门 生命周期-next2 (十九)
  • 牛津大学xDeepMind 自然语言处理(1)
  • Centos7 使用lamp架构部署wordpress
  • 接口和抽象类的区别(面试回答)
  • 【深度长文】Anthropic发布Prompt Engineering全新指南
  • Java面向对象三大特性:封装、继承、多态深度解析与实践应用
  • ⭐CVPR2025 RigGS:从 2D 视频到可编辑 3D 关节物体的建模新范式
  • 音频分类模型笔记
  • OOP三大特性
  • 【计算机视觉与深度学习实战】05计算机视觉与深度学习在蚊子检测中的应用综述与假设
  • 网络基础——协议认识
  • Pytest项目_day18(读取ini文件)
  • Unity 中控开发 多路串口服务器(一)
  • 深层语义知识图谱:提升NLP文本预处理效果的关键技术
  • C++ 多进程编程深度解析【C++进阶每日一学】
  • 一个基于纯前端技术实现的五子棋游戏,无需后端服务,直接在浏览器中运行。
  • 深度学习篇---softmax层
  • Maven 生命周期和插件
  • 大数据分析-读取文本文件内容进行词云图展示
  • 大厂求职 | 2026海尔校园招聘,启动!
  • Vuex 状态持久化企业级解决方案
  • ​Kali Linux 环境中的系统配置文件与用户配置文件大全
  • MongoDB 从入门到精通:安装配置与基础操作指令详解
  • 计算机组成原理(9) - 整数的乘除法运算
  • 抽象类和接口的区别
  • VLN视觉语言导航(3)——神经网络的构建和优化 2.3
  • qsort函数使用及其模拟实现