你以为 Class.forName() 就是加载类?别傻了,JVM 背后的“找爹”大戏你真的看懂了吗?
开篇语
哈喽,各位小伙伴们,你们好呀,我是喵手。运营社区:C站/掘金/腾讯云/阿里云/华为云/51CTO;欢迎大家常来逛逛
今天我要给大家分享一些自己日常学习到的一些知识点,并以文字的形式跟大家一起交流,互相学习,一个人虽可以走的更快,但一群人可以走的更远。
我是一名后端开发爱好者,工作日常接触到最多的就是Java语言啦,所以我都尽量抽业余时间把自己所学到所会的,通过文章的形式进行输出,希望以这种方式帮助到更多的初学者或者想入门的小伙伴们,同时也能对自己的技术进行沉淀,加以复盘,查缺补漏。
小伙伴们在批阅的过程中,如果觉得文章不错,欢迎点赞、收藏、关注哦。三连即是对作者我写作道路上最好的鼓励与支持!
0. 前言:那年我瞎改了一个类,结果整个项目都崩了 😱
还记得我刚工作那会儿,有个需求要修改一个第三方 Jar 包里的类。我仗着自己“艺高人胆大”,直接把 Jar 包解压,找到对应的 .class 文件改了一通,然后打成新的 Jar 包替换上去。结果呢?部署到服务器上,项目启动失败,各种 ClassNotFoundException 或者 NoSuchMethodError,直接把我整懵了!😭
当时的我百思不得其解:我明明把文件放进去了,怎么就找不到呢?后来才知道,JVM 加载类可不是你把文件扔进去就行的,它有一整套严谨的“身份证查验”和“背景调查”流程。而这个流程的核心,就是今天我们要聊的——类加载机制和双亲委派模型!
学好这个,不仅能让你在面试中脱颖而出,更能让你在排查各种奇葩的类冲突、版本不兼容问题时,如同拥有“透视眼”一般,一眼看穿本质!✨
1. 什么是“类加载”?(请客入座的流程)
咱们写的 Java 代码,最终会被编译成 .class 文件。这些 .class 文件,JVM 并不能直接拿来用。它得先把这些文件加载到内存里,并且解析、初始化,然后才能变成可用的 Java 对象。这个从 .class 文件到内存中的 Class 对象的过程,就叫类加载。
类加载的过程,大致可以分为 7 个阶段:
1. 加载(Loading):
* 找到 .class 文件。
* 将 .class 文件读取成二进制字节流。
* 将这个字节流的静态存储结构转化为方法区的运行时数据结构。
* 在内存中生成一个代表这个类的 java.lang.Class 对象。
2. 验证(Verification):确保 .class 文件格式正确、语义合法、符合 JVM 规范,别有啥恶意代码或者不安全的指令。这是 JVM 的“安检”!
3. 准备(Preparation):为类的静态变量分配内存,并初始化为零值(比如 int 是 0,boolean 是 false,引用类型是 null)。注意,这时候还没执行任何 Java 代码哦!
4. 解析(Resolution):将常量池里的符号引用(比如 java.lang.Object 的字符串)替换为直接引用(直接指向内存中的地址)。
5. 初始化(Initialization):
* 这才是真正执行类构造器 <clinit>() 方法的时候!
* 在这个阶段,会执行静态代码块、静态变量的赋值操作。
* 初始化只发生一次,且是线程安全的。
后面还有使用(Using)和卸载(Unloading),但最核心的是前面这五步。
2. 类加载器(ClassLoader):请客的“管家” 🤵
谁来负责执行上面“加载”这第一步呢?就是类加载器(ClassLoader)。
JVM 默认有 3 种类加载器,它们就像不同级别的管家,负责从不同地方“请”类进来:
-
启动类加载器(Bootstrap ClassLoader):
- 老大! 用 C++ 实现,所以它不是
java.lang.ClassLoader的子类。 - 负责加载
<JAVA_HOME>/lib目录下的核心类库(rt.jar等),以及-Xbootclasspath参数指定路径的类。 - 你可以把它理解成“系统管理员”,负责加载 JVM 运行的基石,连 Java 自己都管不了它。
- 老大! 用 C++ 实现,所以它不是
-
扩展类加载器(Extension ClassLoader):
ExtClassLoader。负责加载<JAVA_HOME>/lib/ext目录下的所有 Jar 包,以及java.ext.dirs系统变量指定的路径中的类库。- 它有点像“高级助手”,负责加载一些标准扩展类。
-
应用程序类加载器(Application ClassLoader):
AppClassLoader,也叫系统类加载器(System ClassLoader)。- 负责加载用户类路径(
classpath)上指定的类库。 - 我们自己写的代码,就是它来加载的!
- 它是所有用户自定义类加载器的默认父加载器。
这三个类加载器之间,可不是平级关系,它们之间有一个非常重要的“父子”层级结构,这个结构就是双亲委派模型的核心!
3. 双亲委派模型(Parent-Delegation Model):严格的“家规” 👨👩👦👦
好了,高潮来了!这就是面试官最爱问的,也是最能体现你对 JVM 理解深度的地方。
什么是双亲委派模型?
当一个类加载器收到类加载请求时,它并不会马上去尝试加载这个类,而是首先把这个请求委派给它的父类加载器去完成。只有当父类加载器无法完成加载(在它的搜索路径下找不到该类)时,子类加载器才会尝试自己去加载。
整个流程:
AppClassLoader 收到加载请求 -> 委派给 ExtClassLoader -> ExtClassLoader 再委派给 Bootstrap ClassLoader。
如果 Bootstrap ClassLoader 找不到,就返回给 ExtClassLoader 自己找。
如果 ExtClassLoader 也找不到,就返回给 AppClassLoader 自己找。
如果 AppClassLoader 也找不到,那才抛出 ClassNotFoundException!
生动比喻:
你(AppClassLoader)想要找一本书(某个类)。
你先问你爸(ExtClassLoader):“爸,这本书你有吗?”
你爸不直接回答,他先问他爸(Bootstrap ClassLoader):“爸,您有这本书吗?”
你爷爷说:“我没有(核心库里没这书)。”
你爸这才自己去他书架上找(扩展库里找),找到了就给你。
如果他也找不到,他才会跟你说:“儿子,你再去你自己的书架上找找吧。”
你才自己去你的书架上找。如果连你也找不到,那你就真的没这本书了。
🌟 为什么要这么设计?(好处多多)
-
避免重复加载:爹妈都加载过的类,儿子就没必要再加载了。保证一个类在 JVM 中只有一份
Class对象。 -
保证核心 API 的安全:这是最重要的!
- 如果允许用户自己写一个
java.lang.String类并加载,那后果不堪设想! - 有了双亲委派,你想加载
java.lang.String?AppClassLoader会委派给ExtClassLoader,再委派给Bootstrap ClassLoader。Bootstrap ClassLoader发现自己已经加载过JDK里的String类了,就直接返回这个核心类。你的“山寨”String根本没机会被加载!这就是 JVM 的“沙箱安全机制”!🔒
- 如果允许用户自己写一个
4. 打破双亲委派模型:偶尔也得“逆子”一把 😈
虽然双亲委派模型好处多多,但有时候它也会成为我们的“桎梏”。
比如,JDBC 驱动加载、Tomcat/Spring 等框架,都需要打破双亲委派。
为啥要打破?
以 JDBC 为例:
- JDBC 核心接口(
java.sql.Driver等)是由Bootstrap ClassLoader加载的(因为是 JDK 核心库)。 - 但是,具体的 JDBC 驱动实现(比如
mysql-connector-java.jar)是我们引入的第三方 Jar 包,它是由AppClassLoader加载的。 - 问题来了:
Bootstrap ClassLoader无法“看到”AppClassLoader加载的类! 爹看不到儿子的东西啊! - 所以,如果按照双亲委派,
Bootstrap ClassLoader在加载Driver接口时,需要实例化具体的驱动,但它找不到具体的实现类。
怎么办?
Java 设计者引入了线程上下文类加载器(Thread Context ClassLoader, TCCL)。
TCCL 允许父类加载器“请求”子类加载器去完成类加载的动作。
这就像儿子问爹要钱(正常委派),结果爹没钱反过来让儿子给钱(打破委派)! 😂
具体到 JDBC,就是 Bootstrap ClassLoader 发现需要加载驱动实现时,它会获取当前线程的 TCCL (通常是 AppClassLoader),然后让 TCCL 去加载驱动类。
👨💻 代码实战:看类加载器本尊!
public class ClassLoaderHierarchy {
public static void main(String[] args) {
// 1. 获取应用程序类加载器
ClassLoader appClassLoader = ClassLoaderHierarchy.class.getClassLoader();
System.out.println("AppClassLoader: " + appClassLoader);
// 2. 获取扩展类加载器 (AppClassLoader 的父类加载器)
ClassLoader extClassLoader = appClassLoader.getParent();
System.out.println("ExtClassLoader: " + extClassLoader);
// 3. 启动类加载器 (ExtClassLoader 的父类加载器)
// 注意:它不是java对象,所以会返回null
ClassLoader bootstrapClassLoader = extClassLoader.getParent();
System.out.println("BootstrapClassLoader: " + bootstrapClassLoader); // 输出 null,因为是 C++ 实现
System.out.println("----------------------------------------");
// 看看咱们自己写的类是谁加载的?
System.out.println("当前类加载器: " + ClassLoaderHierarchy.class.getClassLoader());
// 看看 Object (核心类) 是谁加载的?
System.out.println("java.lang.Object 的类加载器: " + Object.class.getClassLoader()); // 输出 null,说明是 Bootstrap 加载的
// 看看一个扩展包里的类是谁加载的?(比如 sun.misc.Launcher$ExtClassLoader)
// 你可以尝试加载一些JDK自带的扩展类,例如 com.sun.tools.attach.AttachProvider
try {
Class<?> extClass = Class.forName("sun.nio.cs.StreamEncoder"); // 这个类通常在 rt.jar 但实际是由 ExtClassLoader 加载
System.out.println("sun.nio.cs.StreamEncoder 的类加载器: " + extClass.getClassLoader());
} catch (ClassNotFoundException e) {
System.err.println("找不到类 sun.nio.cs.StreamEncoder");
}
// 演示 TCCL
System.out.println("----------------------------------------");
System.out.println("当前线程的上下文类加载器 (TCCL): " + Thread.currentThread().getContextClassLoader());
}
}
5. 结语:深入 JVM,做更酷的全栈!🚀
JVM 的类加载机制和双亲委派模型,是不是比你想象的要复杂和有趣得多?
它不仅仅是一个技术细节,更体现了 Java 平台在安全、稳定、高效方面深思熟虑的设计哲学。
理解了这些,下次当你遇到:
- 线上 Jar 包冲突导致的
NoSuchMethodError。 - Tomcat 部署多个 Web 应用时,每个应用都有独立的类加载器体系。
- 或者想实现插件化热插拔功能,需要自定义类加载器时。
你就能从容不迫,一眼看穿问题的本质,而不是只会抓耳挠腮、盲目尝试了!这就是从“被动解决问题”到“主动掌控问题”的巨大飞跃!
全栈开发嘛,不仅要能把功能实现,更要能把系统“玩”得转! 咱们继续加油,把这些底层的东西都吃透,你就是下一个技术大佬!💪 感觉这波知识点吸收得还不错吧?给我个小鼓励呗!💖
… …
文末
好啦,以上就是我这期的全部内容,如果有任何疑问,欢迎下方留言哦,咱们下期见。
… …
学习不分先后,知识不分多少;事无巨细,当以虚心求教;三人行,必有我师焉!!!
wished for you successed !!!
⭐️若喜欢我,就请关注我叭。
⭐️若对您有用,就请点赞叭。
⭐️若有疑问,就请评论留言告诉我叭。
版权声明:本文由作者原创,转载请注明出处,谢谢支持!
- 点赞
- 收藏
- 关注作者
评论(0)