类加载过程 → 双亲委派 → 打破双亲委派 追问链#
追问路径#
Q: 一个类从.class到能被使用经历哪些阶段?
→ 加载(读字节流生成Class对象) → 链接[验证→准备→解析] → 初始化(执行<clinit>);使用 → 卸载
Q: 准备阶段和初始化阶段有什么区别?
→ 准备阶段给静态变量分配内存并赋零值(int=0);初始化才执行<clinit>赋真实值和静态块
Q: 什么时候会触发类的初始化?
→ 主动引用:new/读写静态字段/调静态方法/反射/初始化子类/main类;被动引用(用父类静态字段、数组、final常量)不触发
├─ Q: 什么是双亲委派模型?
│ → 加载请求先逐层向上委派给父加载器,父加载不了才自己加载:自定义→App→Ext/Platform→Bootstrap
│ Q: 双亲委派解决什么问题?
│ → 避免核心类被篡改(java.lang.String无论谁加载都用Bootstrap版本) + 避免类重复加载,保证类型一致性
│ Q: 类的"相等"是怎么判断的?
│ → 同一个Class只有"全限定名相同 + 加载它的ClassLoader相同"才算同一个类,否则instanceof为false
│ Q: 为什么Tomcat每个webapp要独立ClassLoader?
│ → 隔离不同应用的同名类/不同版本依赖,所以Tomcat的WebappClassLoader故意打破双亲委派(先自己加载)
└─ Q: 哪些场景打破了双亲委派?
→ JDBC的SPI(Bootstrap加载的DriverManager要加载厂商驱动)、Tomcat类隔离、JDK9模块化、热部署/OSGi
Q: SPI是怎么打破的?用什么机制?
→ 线程上下文类加载器(TCCL):父加载器通过Thread.currentThread().getContextClassLoader()反向拿到App加载器去加载实现类
Q: 字节码层面类信息存在哪?运行时怎么优化?
→ class文件含常量池/字段/方法/属性表;JIT的C1/C2把热点字节码编译成机器码,逃逸分析做栈上分配/标量替换/锁消除plaintext涉及知识点#
- 类加载过程 — 加载/验证/准备/解析/初始化五阶段
- 双亲委派模型 — 委派机制与三层类加载器
- 打破双亲委派 — SPI/Tomcat/热部署的实现手段
- 字节码与class文件结构 — 常量池/方法表/属性表布局
- 对象创建过程 — new的字节码到内存分配
- 对象内存布局 — 对象头/实例数据/对齐填充
- JVM内存结构 — 方法区/元空间存类元信息
- JIT编译优化 — C1/C2分层编译与热点探测
- 逃逸分析与栈上分配 — 标量替换与锁消除
核心串联逻辑#
- 生命周期:加载→验证→准备→解析→初始化→使用→卸载,其中准备只赋零值、初始化才执行
<clinit> - 初始化时机:只有6种主动引用触发,被动引用(子类用父类静态字段、
SuperClass[]数组、final编译期常量)不触发 - 双亲委派三层:Bootstrap(核心类)→Ext/Platform→App(classpath)→自定义;向上委派保证核心类不可被覆盖
- 类相等条件:全限定名 + ClassLoader实例都相同才是同一类——这是Tomcat类隔离和热部署的底层依据
- 打破方式:SPI靠线程上下文类加载器反向委派、Tomcat的WebappClassLoader重写loadClass优先自己加载
- 代码示例:
java// 自定义类加载器打破双亲委派:重写loadClass而非findClass protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { Class<?> c = findLoadedClass(name); if (c == null && !name.startsWith("java.")) { // 核心类仍走双亲,其余自己加载 try { c = findClass(name); } catch (ClassNotFoundException ignore) {} } if (c == null) c = super.loadClass(name, false); return c; } }
面试回答串联#
30秒速答#
“类加载经历加载、验证、准备、解析、初始化五个阶段,准备阶段只给静态变量赋零值、初始化才赋真实值。双亲委派是加载请求逐层向上委派,保证核心类不被篡改、类型一致。判断两个类相等要全限定名和ClassLoader都相同。JDBC的SPI、Tomcat类隔离都打破了双亲委派,SPI靠线程上下文类加载器反向加载实现类。“
2分钟展开答#
“类从字节流到可用经历五个阶段:加载读取字节流生成Class对象,验证保证字节码安全,准备给静态变量分配内存并赋零值(注意此时int是0不是赋的值),解析把符号引用转直接引用,初始化才执行
给静态变量赋真实值和跑静态块。初始化是懒触发的,只有new、读写静态字段、调静态方法、反射、初始化子类、启动类这6种主动引用会触发,而用父类静态字段、定义数组、引用final编译期常量都是被动引用不触发。双亲委派模型是加载请求先逐层委派给父加载器,父加载不了才自己加载,层次是Bootstrap加载核心类→Platform/Ext→App加载classpath→自定义。它解决两个问题:一是安全,你自己写个java.lang.String也会被Bootstrap的版本顶替;二是避免重复加载保证类型一致。判断两个类是否同一个类要看全限定名和加载它的ClassLoader是否都相同,这正是Tomcat给每个webapp配独立WebappClassLoader做应用隔离、支持同名类不同版本的底层原理——它故意重写loadClass优先自己加载打破了双亲委派。另一个经典打破场景是JDBC的SPI:DriverManager由Bootstrap加载但要加载厂商的驱动实现类(在classpath上),只能通过线程上下文类加载器拿到App加载器反向去加载。字节码层面class文件包含常量池、字段表、方法表,运行时JIT的C1/C2会把热点代码编译成机器码,配合逃逸分析做栈上分配和锁消除。“
相关追问链#
- JVM-GC-内存泄漏-OOM追问链 — 元空间(类元信息)的OOM与ClassLoader泄漏
- Spring-IoC-AOP-事务-循环依赖追问链 — CGLIB动态生成字节码与类加载
- SpringBoot启动-自动配置-Starter追问链 — SpringBoot的SPI(spring.factories)与类加载
- 并发编程核心-synchronized-volatile-CAS-AQS追问链 — 锁消除等JIT优化