上次写 JVM 结构的时候类加载器那部分写得很浅。面试官追问了三个问题我答得稀烂“双亲委派模型能保证什么”“SPI 机制是怎么打破双亲委派的”“Tomcat 的类加载器有什么特殊之处”当场社死。回来老老实实翻了一遍《深入理解JVM虚拟机》今天把我踩过的坑整理出来。一、类加载到底是干嘛的在说双亲委派之前先搞清楚类加载到底做了什么。一句话把 class 文件变成 Class 对象。但这个过程分了三步每步都有门道。加载Loading三件事通过类的全限定名找到 class 文件可以是磁盘、可以是 jar、可以是网络读取字节流转成方法区的运行时数据结构在堆里生成一个java.lang.Class对象作为程序访问方法区数据的入口关键点我当初以为 Class 对象就是存在堆里的这没问题。但方法区存的是什么这个搞不清楚JVM 调优时会很懵。方法区存的是类的元信息类名、父类、实现的接口、字段描述、方法字节码等。Class 对象只是个入口指针。链接Linking分三步验证Verify这个阶段我之前完全忽略过。JVM 要校验文件格式是否正确魔数 0xCAFEBABE字节码指令是否合法类型安全父类是否是 final 的、字段方法是否存在为什么要验证因为 class 文件可以被篡改。恶意代码编译成字节码后验证阶段会发现问题直接拒绝加载。准备Prepare给静态变量分配内存设置默认值。publicclassUser{staticintage18;// 这里 age 0不是 18staticStringnameTom;// 这里 name null不是 Tom}重点准备阶段只赋默认值不赋初始值。初始值在初始化阶段才赋。我之前就搞混过面试题问静态变量的赋值时机我答类加载时就赋值直接扣分。解析Resolve把符号引用替换成直接引用。符号引用是什么比如字节码里写着CONSTANT_Methodref #5这个 #5 只是个编号指向常量池里的方法描述。解析阶段要做的是把符号引用变成真正的内存地址直接引用。初始化Initialization这才是真正执行代码的地方。执行静态代码块给静态变量赋初始值注意这里的赋值和准备阶段的默认值是两回事publicclassUser{staticintage18;// 准备阶段 age0初始化阶段 age18staticStringname;static{nameTom;// 静态代码块初始化阶段执行}}触发初始化的时机new对象调用静态方法访问静态字段注意访问 final 修饰的静态字段不会触发因为已经在常量池里了反射Class.forName()子类加载时先触发父类二、双亲委派模型怎么实现的三层类加载器Bootstrap ClassLoader ← C 实现加载 JDK_HOME/jre/lib ↑ Extension ClassLoader ← 加载 JDK_HOME/jre/lib/ext ↑ Application ClassLoader ← 加载 classpath我们写的代码每个类加载器都有个 parent 引用向上委托。核心代码逻辑protectedClass?loadClass(Stringname,booleanresolve){// 1. 先查缓存Class?cfindLoadedClass(name);if(cnull){try{// 2. 向上委托给父加载器if(parent!null){cparent.loadClass(name,false);}else{// 3. 父为空说明到了 Bootstrap从 Bootstrap 开始找cfindBootstrapClassOrNull(name);}}catch(ClassNotFoundExceptione){// 父加载器找不到}if(cnull){// 4. 父加载器找不到自己来加载cfindClass(name);}}returnc;}双亲委派保证了什么面试被问到这个问题我直接愣住。保证了三件事1. 类的唯一性同一个类被两个不同的类加载器加载过那这两个 Class 对象是不同的。因为 Class 对象的 identity 包括类名 类加载器实例。// 两个不同的类加载器ClassLoaderloader1newCustomLoader();ClassLoaderloader2newCustomLoader();// 同一个类被两个加载器加载Class?c1loader1.loadClass(com.test.User);Class?c2loader2.loadClass(com.test.User);// c1 ! c2这是两个不同的类型2. 核心类的安全性你没法自定义一个java.lang.String来替换 JDK 的 String。因为java.lang.String会先被 Bootstrap ClassLoader 加载你自定义的String根本没机会。3. 类的层次关系同一个类被父加载器加载后子加载器不需要再加载一次。三、双亲委派的例外SPI 机制面试官追问“那 SPI 机制是怎么打破双亲委派的”这个问题我当时完全没准备到。什么是 SPISPI Service Provider Interface。典型例子JDBC。JDBC 的驱动接口是 JDK 定义的java.sql.Driver但具体实现是 MySQL、PostgreSQL 这些厂商写的。问题来了DriverManagerBootstrap ClassLoader 加载的驱动实现类Application ClassLoader 加载的DriverManager 怎么找到 MySQL 驱动的答案是Thread Context ClassLoader。线程上下文加载器每个线程都有一个类加载器引用存在 Thread 类的contextClassLoader字段里。JNDI 用它访问 SPI 服务JDBC 用它加载驱动实现。// JDBC 驱动加载的源码大概是这样的ServiceLoaderDriverdriversServiceLoader.load(Driver.class);// ServiceLoader 内部会使用 Thread.currentThread().getContextClassLoader()// 来加载驱动实现类为什么能打破双亲委派因为这段代码是 JDK 写的它明确指定了用 contextClassLoader 来加载。没有 contextClassLoader 的话DriverManager 就只能用 Bootstrap 加载而 Bootstrap 加载不了 MySQL 的驱动类因为驱动类在 classpath 里不在 jre/lib 里。四、我踩过的坑坑1Tomcat 的类加载器顺序Tomcat 根本不是标准的双亲委派Tomcat 的加载顺序1. Bootstrap ClassLoader 2. System ClassLoader 3. /WEB-INF/classes 下的自定义类 ← 优先于 classpath 4. /WEB-INF/lib/*.jar 5. Common ClassLoaderTomcat 全局为什么要打破双亲委派因为 Tomcat 要部署多个应用每个应用可能依赖不同版本的同一个类比如 Spring。如果用标准的双亲委派Tomcat 上的所有应用都只能用同一份类。Tomcat 的做法先自己加载自己加载不到再委托父加载器。这叫反向委托或子供委派。坑2MyBatis 的 classpath 优先级我之前遇到一个问题项目里有两个版本的 mybatis.jar一个在 WEB-INF/lib一个在 Tomcat 的 lib。结果运行时用的是 Tomcat 里的旧版本。这其实就是类加载器优先级的问题。Tomcat 加载器先找到旧版本就用了旧版本。解决方案搞清楚依赖的类加载器层级必要时把依赖放到 WEB-INF/lib优先级更高或者升级 Tomcat lib 里的版本坑3OSGI 和模块化之前公司想上 OSGI 框架遇到一个经典问题OSGI 的类加载机制是网状的每个 bundle 可以单独加载、单独卸载。好处是灵活坏处是类加载器之间的关系变得极其复杂不同 bundle 之间的类不能直接访问需要通过 OSGI 的服务注册机制来交互最后项目没上 OSGI改成了微服务。五、面试高频问题Q1类加载的过程是什么加载 → 链接验证→准备→解析→ 初始化每个阶段做什么要能说出来。Q2双亲委派模型是什么有什么好处三层类加载器父委托子保证类的唯一性、核心类安全、层次关系Q3能破坏双亲委派吗能。有两种方式自定义类加载器重写 loadClass()不向上委托直接自己加载线程上下文类加载器SPI 机制绕过父加载器从当前线程的 contextClassLoader 加载Q4Tomcat 为什么要打破双亲委派部署多个应用每个应用需要隔离同一应用需要加载自己优先的类使用反向委托子供委派Q5如何实现类的热加载// 自定义类加载器publicclassHotSwapClassLoaderextendsURLClassLoader{publicClass?loadClass(Stringname,booleanresolve)throwsClassNotFoundException{// 每次都重新加载不走缓存returnfindClass(name);}}关键点每次 new 一个新的 ClassLoader 实例旧的对象没有引用后就会被 GCClass 对象也会被卸载。六、记忆口诀类加载三步 加载 → 链接验证、准备、解析→ 初始化 准备赋默认值初始化赋初始值 双亲委派 Bootstrap → Extension → Application 保证唯一性、安全性、层次性 打破双亲委派 1. 重写 loadClass 2. 线程上下文类加载器SPI Tomcat 打破 子供委派自己先加载 类加载器优先级 子类 父类同级别下 Tomcat: WEB-INF/classes WEB-INF/lib classpath写在最后类加载器这块我踩过的坑比 JVM 其他部分加起来都多。核心原因类加载器是 JVM 和应用代码的桥梁涉及到依赖管理、模块化、容器隔离这些实战问题。光背概念没用。建议你自己实现一个自定义类加载器加载一个 class 文件体会一下整个流程。面试问到这块你如果说我自己实现过一个热加载的类加载器面试官基本不会再追问了。有些坑只有自己踩过才知道有多深。