JVM知识梳理(3)
作者没有四次元口袋的蓝胖日期2026-08-01标签Java, JVM, 类加载, 双亲委派JVM知识梳理(3)类加载决定类怎么进入内存是 JVM 面试的另一大核心。掌握类加载过程、双亲委派原理、如何打破双亲委派。一、类加载过程1.1 类的生命周期加载 → 连接验证 → 准备 → 解析 → 初始化 → 使用 → 卸载1.2 加载Loading任务通过类的全限定名获取二进制字节流将字节流代表的静态存储结构转化为方法区的运行时数据结构在堆中生成java.lang.Class对象作为方法区数据的访问入口来源本地文件系统.class文件JAR 包网络Applet运行时计算动态代理1.3 连接Linking连接分为三个阶段验证、准备、解析。验证Verification确保字节码符合 JVM 规范不会危害虚拟机。验证内容 - 文件格式验证是否以 CAFEBABE 开头 - 元数据验证是否有父类、是否继承了 final 类 - 字节码验证字节码指令是否正确 - 符号引用验证类名、方法名是否存在准备Preparation为类的静态变量分配内存并设置默认值不是代码中的赋值。publicclassTest{publicstaticinta10;publicstaticfinalintb20;}// 准备阶段a 0int 默认值b 20final 常量直接赋值// 初始化阶段a 10执行 clinit() 方法关键区分准备阶段设默认值int 是 0初始化阶段才赋代码中的值10。解析Resolution将常量池中的符号引用替换为直接引用内存地址。符号引用java/lang/String类名 直接引用0x7f8b2c00内存地址1.4 初始化Initialization执行类构造器clinit()方法初始化静态变量和静态代码块。publicclassTest{publicstaticinta10;static{b30;}publicstaticintb20;}// 初始化后a 10, b 20静态代码块和静态变量按顺序执行注意clinit()方法是编译器自动收集的静态变量赋值和静态代码块合并而成的。二、类加载器2.1 类加载器层次Bootstrap ClassLoader启动类加载器 ↑ Extension ClassLoader扩展类加载器 ↑ Application ClassLoader应用程序类加载器 ↑ Custom ClassLoader自定义类加载器类加载器加载范围实现者BootstrapJAVA_HOME/lib如java.lang.*JVM 实现CExtensionJAVA_HOME/lib/extJava 代码Application用户类路径classpathJava 代码Custom自定义路径用户代码三、双亲委派模型3.1 工作原理加载请求 → Application ClassLoader ↓先委派给父加载器 Extension ClassLoader ↓再委派给父加载器 Bootstrap ClassLoader ↓父加载器无法加载才自己加载 尝试加载 → 失败则抛出 ClassNotFoundException核心逻辑// ClassLoader.loadClass() 简化版protectedClass?loadClass(Stringname,booleanresolve){// 1. 先检查是否已加载Class?cfindLoadedClass(name);if(cnull){// 2. 委派给父加载器if(parent!null){cparent.loadClass(name);}else{// 3. 父加载器为空Bootstrap自己尝试加载cfindClass(name);}}returnc;}3.2 为什么需要双亲委派好处避免类重复加载父加载器已加载的类子加载器不会再加载保护核心类库用户自定义的java.lang.String不会被加载防止核心类被篡改// 用户代码packagejava.lang;publicclassString{}// 自定义 String// 双亲委派保证Bootstrap 加载的 java.lang.String 不会被覆盖// 用户代码中的 String 会被委派给 Bootstrap 加载最终加载的是 JDK 的 String面试回答模板双亲委派的好处有两个一是避免类重复加载父加载器加载过的类子加载器不会再加载二是保护核心类库用户自定义的 java.lang.String 不会被加载防止核心类被篡改。四、打破双亲委派4.1 什么场景需要打破场景原因Tomcat每个 Web 应用有独立的类加载器互不干扰OSGi模块化框架需要灵活的类加载策略SPI 机制JDBC、JNDI 等需要加载第三方实现4.2 如何打破方法重写loadClass()方法。publicclassCustomClassLoaderextendsClassLoader{OverrideprotectedClass?loadClass(Stringname,booleanresolve)throwsClassNotFoundException{// 打破双亲委派自己先尝试加载失败再委派给父加载器try{Class?cfindClass(name);returnc;}catch(ClassNotFoundExceptione){returnsuper.loadClass(name,resolve);}}OverrideprotectedClass?findClass(Stringname)throwsClassNotFoundException{// 自定义加载逻辑如从网络、数据库加载byte[]dataloadClassData(name);returndefineClass(name,data,0,data.length);}}4.3 SPI 机制打破双亲委派JDBC 的 SPI 场景// JDBC 核心接口在 Bootstrap 加载java.sql.*// 但实现类如 MySQL Driver在 classpath 中由 Application 加载// 解决方案Thread Context ClassLoaderClassLoadercontextClassLoaderThread.currentThread().getContextClassLoader();// 父加载器通过线程上下文加载器调用子加载器加载实现类流程Bootstrap 加载 java.sql.DriverManager ↓ DriverManager 需要加载 com.mysql.cj.jdbc.Driver ↓ Bootstrap 无法加载在 classpath 中 ↓ 通过 Thread.currentThread().getContextClassLoader() 获取 Application 加载器 ↓ Application 加载器加载 MySQL Driver五、类加载器隔离与 Tomcat5.1 Tomcat 的类加载器Common ClassLoader加载 Tomcat 通用类 ↑ Catalina ClassLoader加载 Tomcat 内部类 ↑ Shared ClassLoader加载共享类 ↑ WebApp ClassLoader每个 Web 应用独立 ├── /webapp1 → WebAppClassLoader1 └── /webapp2 → WebAppClassLoader2设计目的不同 Web 应用的类互相隔离同一份第三方库可以被多个应用共享️ 思维导图速览JVM 类加载 ├── 类加载过程 │ ├── 加载获取字节流 → 方法区 → Class 对象 │ ├── 连接 │ │ ├── 验证字节码合法性 │ │ ├── 准备静态变量设默认值 │ │ └── 解析符号引用 → 直接引用 │ └── 初始化执行 clinit()赋实际值 ├── 类加载器层次 │ ├── BootstrapJDK 核心类 │ ├── Extension扩展类 │ ├── Applicationclasspath │ └── Custom自定义 ├── 双亲委派 │ ├── 原理先委派父加载器父加载器无法加载才自己加载 │ ├── 好处避免重复加载、保护核心类库 │ └── 打破重写 loadClass() └── SPI 机制 └── Thread Context ClassLoader 反向调用子加载器 写在最后学习建议类加载过程要记顺序加载→连接验证→准备→解析→初始化准备阶段设默认值初始化阶段赋实际值双亲委派要理解为什么不是死记硬背理解避免重复加载和保护核心类库两个核心好处打破双亲委派要知道场景Tomcat 的 WebAppClassLoader、SPI 的Thread.currentThread().getContextClassLoader()准备 vs 初始化面试经常问静态变量int a 10什么时候赋值——准备阶段赋 0初始化阶段赋 10面试回答模板Q说说类的加载过程类的加载过程分为加载、连接、初始化三个阶段。加载阶段获取字节流并在方法区创建运行时数据结构在堆中生成 Class 对象。连接阶段分为验证字节码合法性、准备静态变量设默认值如 int 是 0、解析符号引用转直接引用。初始化阶段执行clinit()方法为静态变量赋代码中的实际值。Q什么是双亲委派为什么需要双亲委派是类加载器之间的委派机制。类加载请求先委派给父加载器处理父加载器无法加载才自己加载。层次是 Bootstrap → Extension → Application → Custom。好处有两个一是避免类重复加载父加载器加载过的类子加载器不会再加载二是保护核心类库用户自定义的 java.lang.String 不会被加载防止核心类被篡改。Q如何打破双亲委派通过重写 ClassLoader 的loadClass()方法自己先尝试加载失败再委派给父加载器。典型场景有Tomcat 的每个 Web 应用有独立的 WebAppClassLoader保证应用之间类隔离JDBC 的 SPI 机制通过Thread.currentThread().getContextClassLoader()让父加载器调用子加载器加载第三方实现。