类加载机制和双亲委派机制

📕我是廖志伟,一名Java开发工程师、《Java项目实战——深入理解大型互联网企业通用技术》(基础篇)、(进阶篇)、(架构篇)清华大学出版社签约作家、Java领域优质创作者、优快云博客专家、阿里云专家博主、51CTO专家博主、产品软文专业写手、技术文章评审老师、技术类问卷调查设计师、幕后大佬社区创始人、开源项目贡献者。

📘拥有多年一线研发和团队管理经验,研究过主流框架的底层源码(Spring、SpringBoot、SpringMVC、SpringCloud、Mybatis、Dubbo、Zookeeper),消息中间件底层架构原理(RabbitMQ、RocketMQ、Kafka)、Redis缓存、MySQL关系型数据库、 ElasticSearch全文搜索、MongoDB非关系型数据库、Apache ShardingSphere分库分表读写分离、设计模式、领域驱动DDD、Kubernetes容器编排等。不定期分享高并发、高可用、高性能、微服务、分布式、海量数据、性能调优、云原生、项目管理、产品思维、技术选型、架构设计、求职面试、副业思维、个人成长等内容。

Java程序员廖志伟

🌾阅读前,快速浏览目录和章节概览可帮助了解文章结构、内容和作者的重点。了解自己希望从中获得什么样的知识或经验是非常重要的。建议在阅读时做笔记、思考问题、自我提问,以加深理解和吸收知识。阅读结束后,反思和总结所学内容,并尝试应用到现实中,有助于深化理解和应用知识。与朋友或同事分享所读内容,讨论细节并获得反馈,也有助于加深对知识的理解和吸收。💡在这个美好的时刻,笔者不再啰嗦废话,现在毫不拖延地进入文章所要讨论的主题。接下来,我将为大家呈现正文内容。

优快云

文章目录

    • 类加载机制和双亲委派机制
      • 第一步
      • 第二步
      • 第三步
      • 第四步
      • 第五步
      • 第六步
      • 第七步
      • 第八步


类加载机制和双亲委派机制

Java程序员廖志伟

第一步

加载,一个Java源文件进行编译之后,成为一个class字节码文件存储在磁盘上面,这个时候jvm需要读取这个字节码文件,通过通过IO流读取字节码文件,这一步就是加载。

类加载器将.class文件加载到JVM,首先是看当前类是不是使用自定义加载类加载的,如果不是,就委派应用类加载器加载,如果有加载过这个class文件,那就不用再加载了。如果没有,那么会拿到父加载器,然后调用父加载器的loadClass方法。父类的扩展类加载器同理也会先检查自己是不是已经加载过,如果没有再往上,看看启动类加载器。到启动类加载器,已经没有父加载器了,这时候开始考虑自己是否能加载了,如果自己加载不了,就会下沉到子加载器去加载,一直到最底层,如果没有任何加载器能加载,就会抛出ClassNotFoundException找不到类异常,这就是所谓的双亲委派机制。

Java程序员廖志伟

这种机制可以避免,同路径下的同文件名的类,比如,自己写了一个java.lang.obejct,这个类和jdk里面的object路径相同,文件名也一样,这个时候,如果不使用双亲委派机制的话,就会出现不知道使用哪个类的情况,而使用了双亲委派机制,它就委派给父类加载器就找这个文件是不是被加载过,从而避免了上面这种情况的发生。
Java程序员廖志伟


举个例子辅助小白理解:可以想象成快递员送包裹。Java的类(比如你的Test.class文件)就像一个个包裹,快递员(‌类加载器‌)要把包裹送到你家(JVM)。

‌第一步:找快递员‌
快递员送包裹时,不会直接冲去你家,而是先问:“这包裹是不是应该由我送?”如果不是,他会按规矩把包裹转交给上级快递员(比如爸爸级快递员),一级一级往上问。

‌第二步:爸爸优先‌
最高级的快递员(比如“JDK官方快递员”)会先检查:“这包裹是不是我的?比如是不是java.lang.String这种核心包裹?”如果是,他直接送。如果不是,就往下级快递员扔:“这包裹归你管!”

‌第三步:防止假货‌
如果你自己偷偷写了一个java.lang.Object(和JDK的类同名同路径),快递员会直接交给“JDK官方快递员”送,而不会用你的版本,避免“假货”混进去。

双亲委派的好处‌:
‌不重复送快递‌:一个包裹只由一个快递员送,不会重复。
‌保护官方包裹‌:核心类(比如String)永远由最信任的快递员送,安全可靠。
‌分工明确‌:快递员各管一摊,爸爸送官方包裹,儿子送你写的代码,孙子送特殊需求(比如Tomcat的Web应用)。

第二步

目前很多博客都会使用以下图片来说明类加载机制,但其实这张图还是太笼统了,有些执行的顺序并不是按照图片的顺序来执行的,像验证和加载这块,它在执行时,它们工作有很多,并不是所有都执行完成之后才进入到下面的步骤,而是交替发生的,实际上,在加载类的字节码过程中,JVM会同时进行验证工作。所以从这一步,我将详细讲解更贴合实际的底层技术细节,配合生活中的例子帮助读者理解,让你在面试时能清晰的讲出类加载机制和双亲委派机制底层的工作原理。
Java程序员廖志伟
验证,JVM读到文件也不是直接运行,还需要校验加载进来的字节码文件是不是符合JVM规范

验证的第一步就是文件的格式验证,验证class文件里面的魔数和主次版本号,发现它是一个jvm可以支持的class文件并且它的主次版本号符合兼容性要求,所以验证通过。


验证阶段‌就像拆快递前的安全检查‌:快递员(类加载器)把包裹(.class文件)送到你家(JVM)门口,但JVM是个严格的保安,会先做两件事:

  1. 检查包裹是不是“正经快递”‌(文件格式验证)
  • 保安会先看包裹的“快递单号”(‌魔数‌),比如所有正规Java包裹的单号必须是CAFEBABE(一串特殊标识)。如果不是,直接拒收!
  • 再检查包裹的“生产日期”(‌版本号‌),比如Java 8的保安只收Java 8及以前版本的包裹,如果发现是Java 20的包裹,直接扔出去:“版本太新,我不认识!”
  1. 防毒防骗‌
  • 保安还会偷偷扫描包裹里有没有藏病毒(比如篡改的字节码)、或者骗人的假货(比如代码逻辑错误),确保你的包裹安全无害,才允许进门

第三步

加载,它会将class文件这个二进制静态文件转化到方法区里面,转化为方法区的时候,会有一个结构的调整,将静态的存储文件转化为运行时数据区,这个转化等于说又回到了加载。

接着到了方法区的运行时数据区以后,在java堆内存里面生成一个当前类的class对象,作为方法区里面这个类,被各种访问的一个入口。比如说object类,它是所有类都继承它,访问它,所以它也需要一个被各种类访问的入口。object类先加载,加载完成之后,它经过这一系列的操作,把自己java.lang.object放到这个堆里面,要让其他的类进行访问,这个也是加载。


加载阶段‌就像图书馆新书上架‌:

‌第一步:把书搬进仓库‌
你写的Test.class文件就像一本新书,图书管理员(‌类加载器‌)先把它搬到图书馆的“方法区”仓库里。但仓库里的书不能直接看,得先整理成标准格式(比如贴上标签、分类放好),方便后续查找。

‌第二步:制作目录卡片‌
管理员还会在图书馆的“公共借阅区”(堆内存)放一张‌目录卡片‌(Class对象),比如Test.class对应的卡片。以后你想看这本书,不用翻仓库,直接看卡片就能知道书的位置和内容。

‌为什么需要目录卡片?‌
比如所有书都默认继承的《Java宝典》(Object类),它的目录卡片会最早做好。其他书(类)想参考《Java宝典》,直接看它的卡片就行,不用每次都去仓库翻原书。

第四步

继续验证,接着元数据验证,它会对字节码描述的信息进行语义分析,比如:这个类是不是有父类,是不是实现了父类的抽象方法,是不是重写了父类的final方法,是不是继承了被final修饰的类等等。

然后字节码验证,通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的,比如:操作数栈的数据类型与指令代码序列是不是可以配合工作,方法中的类型转换是不是有效等等。

最后符号引用验证:确保解析动作可以正确执行,比如说:通过符号引用是不是可以找到对应的类和方法,符号引用中类、属性、方法的访问性是不是能被当前类访问等,验证完成之后,需要做准备。


用 ‌「装修房子」‌ 来比喻验证阶段的不同步骤,更简单易懂:

  1. 元数据验证:检查「设计图」是否合理‌
    就像装修前要检查设计图是否合法:
    检查「家族关系」‌:比如你的设计图说“卫生间继承自客厅”,但装修师傅会骂:“胡扯!卫生间不能继承客厅,必须继承水管房!”(不能乱认爹,父类必须存在且合法)。
    检查「承诺是否兑现」‌:比如你承诺在厨房装抽油烟机(实现抽象方法),师傅会核实:“抽油烟机装了吗?没装的话这厨房不合格!”
    检查「违规操作」‌:比如你偷偷拆了承重墙(重写 final 方法),师傅直接叫停:“这墙不能动!违法!”

  2. 字节码验证:检查「电路和水管」是否安全‌
    师傅会拿着设计图,检查实际施工是否符合逻辑:
    电路匹配‌:比如你设计图上说“客厅用220V插座”,但实际装了110V的,师傅会警告:“电压不匹配,会短路!”(操作数栈的数据类型和指令不匹配)。
    水管连接‌:比如你把洗衣机水管接到燃气管道上,师傅怒吼:“这俩接口不匹配,会爆炸!”(无效的类型转换)。

  3. 符号引用验证:检查「地址簿」是否有效‌
    最后师傅会检查你留的联系人信息是否靠谱:
    电话能否打通‌:比如你说“水电工是老张,电话123”,师傅会打过去确认:“是老张吗?你会修水管吗?”(通过符号引用能否找到真实的类和方法)。
    权限是否足够‌:比如你说“物业钥匙在抽屉里”,但师傅发现抽屉锁着,钥匙拿不到:“这钥匙我打不开啊!”(没有权限访问其他类的私有方法)。

JVM第四步的验证就像装修师傅的三次检查:设计图合法‌(元数据验证) → 2. ‌施工安全‌(字节码验证) → 3. ‌联系人靠谱‌(符号引用验证)。如果哪一步不通过,直接停工(抛出错误),防止房子塌了(程序崩溃)!

第五步

分配内存空间,准备就是给类的静态变量分配内存,并赋予默认值。我们的类里,可能会包含一些静态变量, 比如说public static int a = 12; 得给a这个变量分配个默认值 0,再比如public static User user = new User(); 给 static的变量User分配内存,并赋默认值null。如果是final修饰的常量,就不需要给默认值了,直接赋值就可以了。


用 ‌「开餐厅前的备菜」‌ 来比喻 ‌准备阶段‌,更简单:

  1. ‌准备阶段:备好基础食材(分配内存 + 默认值)‌
    假设你开了一家餐厅(类),菜单上有几道菜(静态变量):
  • 普通食材‌:比如“番茄炒蛋”需要番茄和鸡蛋,但还没开始炒。
    • 后厨(JVM)会先把番茄和鸡蛋(静态变量)摆到灶台旁(分配内存),但暂时不处理(默认值):
public static int a = 12; → 番茄先放桌上,但没切(a 先被设为 0)。
public static User user = new User(); → 鸡蛋放桌上,但没打碎(user 先被设为 null)。
  • 预制菜(final常量)‌:比如“凉拌黄瓜”已经提前腌好了(final修饰)。
public static final int MAX = 100; → 腌黄瓜直接上桌(直接赋值 100),不用再加工。
  1. ‌为什么要先备菜?‌
  • 效率高‌:客人点菜时(程序运行),后厨不用临时找食材(避免运行时再分配内存)。
  • 安全‌:避免食材没准备好就开火(防止使用未初始化的变量)。

小总结‌:

  • 普通静态变量‌:先占个位置(内存),给默认值(比如0、null)。
  • final常量‌:直接上桌(赋值),不用等。
  • 真正的炒菜(赋值)‌ 要等到下一步(初始化阶段)才开始!

第六步

解析,解析就是将符号引用变为直接引用,该阶段会把一些静态方法替换为指向数据储存在内存中的指针或者句柄,也就是所谓的直接引用,这个就是静态链接过程,是在初始化之前完成。有静态链接就有动态链接,动态链接是在程序运行期间完成将符号引用替换为直接引用,比如静态方法里面有个方法,在运行的时候,方法是存放在常量池中的符号,运行到这个符号,就是找这个符号对应的方法区,因为代码的指令是加载到方法区里面去的,最后把方法对应代码的地址放到栈帧中的动态链接里。

  • 编译器中的解析阶段其目的是将符号引用(如变量、函数等名字)转换成直接引用(如指向内存中的地址),从而使程序能够正确地执行。
  • 直接引用是指程序中直接使用的方法或数据在内存中对应的地址。在程序编译期间,编译器将所有的方法和数据按照一定的规则映射到内存中的不同位置,生成可执行文件。在程序运行期间,程序通过直接引用来访问这些方法和数据,直接引用一般是一个绝对地址或偏移量。因此,在程序运行时,操作系统需要将程序中的符号引用转换成直接引用,以正确地访问方法和数据。
  • 符号引用是指程序中使用的方法或数据的标识符,不是直接的内存地址。在编译期间,编译器不能确定符号引用对应的具体地址,因为这些方法和数据在运行时可能会被加载到不同的内存地址上。因此,在编译期间,编译器将符号引用记录在符号表中,并在链接期间将符号引用解析为直接引用。在操作系统加载程序之前,链接器会根据符号表中的信息,找到并解析程序中所有的符号引用,将它们转换为直接引用。
  • 静态链接是指将程序中所有需要用到的代码和数据在编译时就全部链接成一个可执行文件的过程。在静态链接过程中,编译器会将静态库中的函数和变量复制到可执行文件中,形成一个完整的可执行文件。在程序运行时,所有的代码和数据都存在于内存中,程序不需要再依赖外部库文件或动态链接库。静态链接的缺点是可执行文件较大,不易于维护和更新。
  • 动态链接是一个程序运行时(运行期间)连接目标文件模块的过程,将需要的代码添加到进程的地址空间中,并将不同的模块组合在一起,使它们能够相互调用。在动态链接的过程中,程序中的符号引用被动态解析为直接引用,这样程序才能正确地访问方法和数据。动态链接的好处是减小了程序的大小,同时也方便了程序的更新和维护。常见的动态链接库(DLL)就是使用动态链接方式加载的。
  • 在静态链接过程中,一些静态方法会被替换成直接引用,这个过程在程序初始化之前完成。动态链接是在程序运行期间完成,它会将符号引用替换成直接引用,使程序能够正确地访问方法和数据。具体来说,动态链接会将符号引用对应的方法的代码地址放到栈帧中的动态链接里,从而实现符号引用到直接引用的转换。

用 ‌「快递配送系统」‌ 来比喻解析阶段,更直观:

  1. 符号引用 vs 直接引用‌
  • 符号引用‌:就像快递单上写的 ‌“收件人:张三,地址:北京海淀区某小区”‌,但快递员并不知道具体门牌号。
  • 直接引用‌:快递员通过系统查到了张三家的 ‌“具体门牌号(比如3号楼502)”‌,这就是直接引用。
    解析阶段‌的任务,就是帮快递员(JVM)把模糊的地址(符号引用)转换成精确的门牌号(直接引用)。
  1. 静态链接:提前写好地址的快递单‌

场景‌:有些快递单的地址是固定不变的(比如公司总部地址)。
比如‌:Math.sqrt() 这种静态方法,JVM在类加载时就能确定它的位置(直接引用)。
操作‌:快递员提前把“总部地址”贴到快递单上(静态链接),后续直接按这个地址送,速度超快!

  1. 动态链接:临时查地址的快递单‌

场景‌:有些快递单的地址是动态变化的(比如外卖小哥接单后才知道顾客地址)。
比如‌:调用接口方法或重写方法时,实际执行的方法可能由运行时对象决定(比如多态)。
操作‌:快递员(JVM)接到订单后,临时去系统查实时地址(运行时解析符号引用),再把地址记到订单备注里(栈帧中的动态链接)。

  1. 为什么分静态和动态?‌

静态链接(快但占空间)‌:

  • 提前贴好地址,配送快(执行效率高)。
  • 但所有地址都提前贴单子上,包裹体积大(可执行文件大)。

动态链接(灵活但稍慢)‌:

  • 按需查地址,包裹轻便(节省内存,方便更新)。
  • 但每次配送要多一步查地址(运行时解析,稍慢)。

举个栗子 🌰‌
静态方法‌:

public static void sendToHQ() {
    // 直接按“总部地址”发货(静态链接)
}

动态方法‌:

public void deliverFood() {
    // 外卖小哥接单后,才知道送哪里(动态链接)
}

总结‌:

  • 解析阶段‌ = 快递员把模糊地址转成精确门牌号。
  • 静态链接‌:提前贴地址,适合固定目标(静态方法)。
  • 动态链接‌:运行时查地址,适合灵活目标(多态、接口)。
  • 最终目的:让程序像快递系统一样,精准高效地“送货”(执行代码)!

第七步

初始化了,初始化就是对类的静态变量初始化为指定的值并且会执行静态代码块。比如准备阶段的public static final int a = 12;这个变量,就是准备阶段给static变量a赋了默认值0,这一步就该把12赋值给它了。还有static的User public static User user = new User(); 把User进行实例化。


用 ‌「餐厅开张当天」‌ 来比喻初始化阶段,更简单:

  1. ‌初始化阶段:正式营业,上菜!‌

假设餐厅(类)已经备好了食材(准备阶段分配了内存和默认值),但食材还是生的。

普通食材加工‌:

public static int a = 12; → 备菜时番茄是生的(默认值 0),现在要炒成番茄炒蛋(赋值为 12)。
public static User user = new User(); → 备菜时鸡蛋是生的(默认值 null),现在要煎成荷包蛋(实例化 User 对象)。

预制菜直接上桌‌:

public static final int MAX = 100; → 凉拌黄瓜(final常量)提前腌好了,直接端上桌(不需要加工)。
  1. ‌静态代码块:开张仪式!‌

餐厅开张当天,老板会做点特殊操作:

static {  
    System.out.println("放鞭炮!挂招牌!"); // 开张仪式(静态代码块)  
    user.setRole("VIP");                    // 给会员发福利(初始化额外操作)  
}  

这些操作‌只在开张当天执行一次‌(类加载时运行静态代码块)。

  1. ‌为什么分「备菜」和「开张」?‌

备菜阶段(准备)‌:快速摆好食材,但不动火(避免耗时操作)。
开张阶段(初始化)‌:正式点火炒菜(赋值、实例化、执行代码块)。
目的‌:防止餐厅还没开门,客人就冲进来吃生鸡蛋(避免使用未赋值的变量)!

小总结‌:

  • 初始化 = 餐厅开张‌:生食材变熟菜(默认值变实际值),外加放鞭炮(静态代码块)。
  • final常量 = 预制菜‌:提前搞定,直接上桌。
  • 静态代码块 = 老板的强迫症‌:开张时必须搞点仪式感!

第八步

就是使用和卸载了,到此整个加载流程就走完了。


用 ‌「餐厅营业到打烊」‌ 来比喻类加载的最后两步,更直观:

  1. ‌使用阶段:开门迎客,点菜上菜!‌

餐厅(类)已经完成备菜、贴地址、开张仪式(加载、解析、初始化),正式营业!

客人点单‌:程序开始用这个类创建对象(比如 User user = new User();),就像客人点了一份番茄炒蛋。
厨师炒菜‌:调用类的方法(user.login()),后厨(JVM)按菜单步骤执行代码。
上菜吃饭‌:程序运行起来,数据和逻辑开始流转,就像客人吃到了热乎的菜。

核心‌:这是类真正“干活”的阶段,所有代码逻辑在此阶段生效!

  1. ‌卸载阶段:打烊收工,清理后厨!‌

餐厅营业结束(程序运行结束或不再需要这个类),开始收拾:

条件苛刻‌:只有当所有客人都离开(类的所有实例被回收),且老板决定关店(类加载器被回收),才会触发卸载。
清理操作‌:JVM 像保洁阿姨一样,把后厨的食材(静态变量)、菜单(方法区代码)全部清空,释放内存。

注意‌:现实中餐厅很少彻底关门(卸载类),除非内存紧缺或程序重启,就像网红店突然倒闭一样少见!

  1. ‌为什么分使用和卸载?‌

使用阶段‌:餐厅的核心价值(类的功能真正被用到)。
卸载阶段‌:避免浪费资源(内存泄漏就像后厨堆满腐烂食材,必须清理)。

总结整个类加载流程‌:

  • 备菜‌(加载):摆好食材(分配内存),但不动火。
  • 贴地址‌(解析):把模糊地址转成精确门牌号。
  • 开张‌(初始化):生食材变熟菜,放鞭炮庆祝。
  • 营业‌(使用):客人点菜,厨师开火。
  • 打烊‌(卸载):清空后厨,彻底收工。

就像一家餐厅从筹备到关门的完整生命周期!

优快云

📥博主的人生感悟和目标

Java程序员廖志伟

希望各位读者大大多多支持用心写文章的博主,现在时代变了,信息爆炸,酒香也怕巷子深,博主真的需要大家的帮助才能在这片海洋中继续发光发热,所以,赶紧动动你的小手,点波关注❤️,点波赞👍,点波收藏⭐,甚至点波评论✍️,都是对博主最好的支持和鼓励!

📙经过多年在优快云创作上千篇文章的经验积累,我已经拥有了不错的写作技巧。同时,我还与清华大学出版社签下了四本书籍的合约,并将陆续出版。这些书籍包括了基础篇进阶篇、架构篇的📌《Java项目实战—深入理解大型互联网企业通用技术》📌,以及📚《解密程序员的思维密码–沟通、演讲、思考的实践》📚。具体出版计划会根据实际情况进行调整,希望各位读者朋友能够多多支持!

🔔如果您需要转载或者搬运这篇文章的话,非常欢迎您私信我哦~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Java程序员廖志伟

赏我包辣条呗

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值