揭秘Java类加载机制与双亲委派:知其所以然,舞动代码世界

📕我是廖志伟,一名Java开发工程师、Java领域优质创作者、优快云博客专家、51CTO专家博主、阿里云专家博主、清华大学出版社签约作者、产品软文创造者、技术文章评审老师、问卷调查设计师、个人社区创始人、开源项目贡献者。🌎跑过十五公里、徒步爬过衡山、🔥有过三个月减肥20斤的经历、是个喜欢躺平的狠人。

📘拥有多年一线研发和团队管理经验,研究过主流框架的底层源码(Spring、SpringBoot、Spring MVC、SpringCould、Mybatis、Dubbo、Zookeeper),消息中间件底层架构原理(RabbitMQ、RockerMQ、Kafka)、Redis缓存、MySQL关系型数据库、 ElasticSearch全文搜索、MongoDB非关系型数据库、Apache ShardingSphere分库分表读写分离、设计模式、领域驱动DDD、Kubernetes容器编排等。🎥有从0到1的高并发项目经验,利用弹性伸缩、负载均衡、报警任务、自启动脚本,最高压测过200台机器,有着丰富的项目调优经验。

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

以梦为马,不负韶华

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

💡在这个美好的时刻,本人不再啰嗦废话,现在毫不拖延地进入文章所要讨论的主题。接下来,我将为大家呈现正文内容。

优快云

揭秘Java类加载机制与双亲委派:知其所以然,舞动代码世界

🍊 加载

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

当我们在使用Java编写程序时,需要使用各种各样的类来实现功能。这些类文件是不能直接被计算机识别的,需要经过类加载器加载到JVM中才能被使用。

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

这种机制的好处是可以避免同路径下的同文件名的类文件冲突,例如,自己写了一个java.lang.object类,这个类和JDK里面的object类路径相同、文件名也一样。如果不使用双亲委派机制,就会出现不知道使用哪个类的情况。而使用了双亲委派机制,它就委派给父类加载器就找这个文件是不是被加载过,从而避免了上述情况的发生。

举个例子,当应用程序需要使用java.lang.Object类时,首先会由应用类加载器尝试加载该类。由于java.lang.Object类已经被JDK的启动类加载器加载过,因此应用类加载器不会再次加载该类,而是直接使用启动类加载器加载的版本。如果应用程序需要使用自定义的Object类,可以通过自定义加载器来加载该类,从而避免与JDK中的Object类冲突。

双亲委派

🍊 验证

JVM读到文件也不是直接运行,还需要校验加载进来的字节码文件是不是符合JVM规范

在进行软件验证时,首先需要进行文件格式验证。这个步骤非常重要,因为在验证过程中我们需要确认该文件是符合要求的,可以被解析和执行。在验证class文件时,需要验证其中包含的魔数和主次版本号。

魔数是class文件中的一个特殊标识符,它的作用类似于文件的签名。只有符合规定的魔数才能被JVM(Java虚拟机)正确识别和加载。类似于我们平时打开文件的时候,需要查看文件扩展名是否是我们熟悉的格式。

主次版本号是指class文件的版本信息。如果使用一个较旧的JVM版本来解析一个新版的class文件,那么就会出现兼容性问题。因此,在验证class文件时,需要检查其主次版本号是否符合JVM的版本要求,以确保文件可以被正确地解析和执行。

简单来说,如果一个class文件的魔数和主次版本号符合规定,那么它就可以被JVM正确识别和加载,验证就通过了。否则,它就不能被加载和执行。

举个例子,我们可以把class文件比作一个书包。我们每天开学前都会检查书包里面的物品是否齐全,包括笔、纸、书、水杯等等。如果我们发现书包里面缺少一个必要的物品,比如说笔,那么我们就无法在上课时正常写字,这就会影响我们的学习。同样地,如果一个class文件缺少必要的魔数和主次版本号,它也无法被JVM正确识别和执行,从而无法正常工作。

🍊 加载

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

在Java程序中,当执行到方法区的运行时数据区时,会在Java堆内存中生成一个当前类的Class对象,作为方法区中该类被各种访问的入口。例如,Object类是所有类的父类,需要被各种类访问,因此它也需要一个被访问的入口。在加载Object类时,它会经过一系列操作,将自己的java.lang.Object存储到Java堆内存中,以便其他类可以进行访问。

举个例子,如果我们有一个名为Student的类,该类定义了学生的一些基本信息,如姓名和年龄等。当我们在程序中调用该类时,Java虚拟机会在堆内存中创建一个Student类的Class对象,作为该类的访问入口,方便其他类在程序运行时对该类进行访问和调用。

🍊 继续验证

🎉 元数据验证

它会对字节码描述的信息进行语义分析的方法,检查一个程序是否符合编程规范和逻辑。

举个例子,如果一个类声明了一个方法,但在实现时却没有按照方法声明的参数个数和类型来编写代码,元数据验证会指出这种错误。又比如,如果一个类继承了一个被final修饰的类,元数据验证会判断出这种违规。

元数据验证还会检查一个类是否具有必要的继承关系,例如,一个类是否继承自一个合法的父类,是否实现了必要的接口等等。此外,元数据验证还会验证一个类是否重载了父类的方法,以及是否按照规定重载了这些方法。

🎉 字节码验证

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

为了更好地理解字节码验证的过程,我们可以将其分为两个部分:数据流分析和控制流分析。数据流分析主要关注变量、常量、数组元素和对象的使用。变量的使用需要满足其数据类型的规定。例如,如果一个变量被声明为整型,那么它就不能被赋值为一个字符串。常量和数组元素的使用也需要遵循它们的类型规定。而对象的使用需要注意对象的创建过程和访问权限。控制流分析则关注代码的执行过程。它检查代码中的分支结构、循环结构和异常处理等是否符合逻辑,以避免程序出现无限循环、空指针异常、数组越界等错误。除了数据流和控制流分析,字节码验证还会检查一些特殊情况,比如方法中的类型转换是否有效,是否有非法访问等等。这些检查都是为了确保Java程序的安全性和正确性。如果Java程序经过了字节码验证,那么就可以在Java虚拟机上运行了。

举个例子,如果我们在程序中声明了一个整型变量,并将其赋值为100,但在代码中又企图将其赋值为一个字符串,那么字节码验证就会发现这样的错误,并在运行之前将其标记为不合法代码。另一个例子是,在一个方法中,如果我们尝试将一个字符串类型的变量转换为整型,但实际上这个变量不是一个合法的整型字符串,也会被字节码验证发现并标记为不合法代码。

🎉 符号引用验证

确保解析动作可以正确执行,在程序中,我们可以通过符号引用来找到对应的类和方法,并确认类、属性、方法的访问权限是否可以被当前类访问。这个过程就像是在地图上找到一个地点的坐标,确定这个地点是否可以到达,是否有可行的路径等。

举个例子,假如这个符号引用是一个类,我们需要确认这个类是否存在,是否可以被我们的程序访问,以及它是否有被继承的父类。如果符号引用是一个方法,我们需要确认这个方法是否存在并且是否可以被访问,它的参数类型是否与实际参数匹配,等等。

完成符号引用验证之后,接下来需要做的准备是将符号引用解析成直接引用。这个过程就像是在地图上根据坐标找到了目的地,需要确定具体怎样走,是否需要穿过森林、河流等障碍。例如,我们需要将一个符号引用解析成一个直接引用,需要在程序中找到这个类或方法的具体位置,查看它是否已经被加载到内存中,等等。

🍊 准备阶段

在编写代码的过程中,我们经常需要定义一些静态变量来保存一些公共数据或状态。这些静态变量是属于类的,而不是属于对象的,因此在类加载到内存中时就需要为这些变量分配内存空间,并给它们赋予默认值。这个过程就叫做分配内存空间,也叫做准备阶段。

假设我们有一个类叫做Student,其中定义了一个静态变量int age和一个静态变量String name。在准备阶段,JVM会为这两个变量分配内存空间,并给它们赋予默认值。对于int类型的变量,其默认值是0;对于引用类型的变量,其默认值是null。因此,在准备阶段后,age的值为0,name的值为null。

另外,如果静态变量被final修饰为常量,那么在准备阶段就不需要为其分配默认值了,因为常量必须在定义时初始化,而且一旦被初始化就不能被修改。比如,我们可以定义一个常量PI,其值为3.14,代码如下:

public static final double PI = 3.14;

在准备阶段,JVM会为PI分配内存空间,并将其值设置为3.14,因此不需要再进行默认值初始化了。

最后再举一个例子,假设我们有一个类叫做Config,其中定义了一个静态变量Map<String, String> config。在准备阶段,JVM会为config变量分配内存空间,并将其值设置为null。如果我们想要在程序运行时给config变量赋值,可以在静态代码块中进行初始化,代码如下:

public class Config {
    public static Map<String, String> config;

    static {
        config = new HashMap<>();
        config.put("name", "张三");
        config.put("age", "18");
    }
}

在静态代码块中,我们为config变量分配了内存空间,并创建了一个HashMap实例并赋值给config变量,因此,在程序运行时就可以使用config变量来保存一些配置信息了。

在准备阶段,JVM会为类的静态变量分配内存空间,并设置默认值。如果是常量,那么不需要设置默认值,而是直接赋值。我们可以利用静态代码块来对静态变量进行初始化,并在程序运行时使用这些变量来保存一些公共数据或状态。

🍊 解析

在编译器中,解析是指将程序中的符号引用转化为直接引用的过程。通过解析,程序能够准确地找到需要使用的方法和数据,并将它们加载到内存中,方便程序正确地执行。

🎉 符号引用和直接引用

符号引用是指在程序中使用的变量、函数等标识符,它并不是指向具体内存地址的指针。编译器在编译程序时,并不能确定符号引用对应的具体内存地址,因为这些方法和数据可能会被加载到内存的不同位置上。因此,编译器将符号引用记录在符号表中,以便在链接期间将符号引用解析为直接引用。

直接引用是指程序中直接使用的方法或数据在内存中对应的地址。在程序编译期间,编译器将所有的方法和数据映射到内存中的不同位置,生成可执行文件。在程序运行期间,程序通过直接引用来访问这些方法和数据,直接引用是一个实际的内存地址或者偏移量。

举个例子,假设程序中有一个变量count,在编译程序时,编译器无法确定count在内存中的具体位置,因此将它记录在符号表中。在程序运行时,操作系统需要将符号引用count解析为直接引用,以便程序正确地访问count变量的值。

🎉 静态链接和动态链接

静态链接是指在编译期间将程序中所有需要使用的代码和数据链接成一个可执行文件的过程。在静态链接过程中,编译器会将静态库中的函数和变量复制到可执行文件中,形成一个完整的可执行文件。在程序运行时,所有的代码和数据都存在于内存中,程序不需要再依赖外部库文件或动态链接库。

静态链接的缺点是可执行文件较大,不易于维护和更新。比如,如果需要对程序中的某个函数进行修改,就需要重新编译整个程序,然后重新链接生成一个新的可执行文件。这样的操作比较麻烦,而且会占用大量的时间和资源。

动态链接是相对于静态链接的一种链接方式。在动态链接的过程中,程序在运行时才将需要的代码和数据添加到进程的地址空间中,并将不同的模块组合在一起,使它们能够相互调用。动态链接的好处是减小了程序的大小,同时也方便了程序的更新和维护。常见的动态链接库(DLL)就是使用动态链接方式加载的。

动态链接的过程是在程序运行期间完成的。它会将符号引用替换成直接引用,使程序能够正确地访问方法和数据。具体来说,动态链接会将符号引用对应的方法的代码地址放到栈帧中的动态链接里,从而实现符号引用到直接引用的转换。

举个例子,假设程序中有一个静态方法需要调用一个库函数,在静态链接中,编译器会将这个库函数的代码复制到可执行文件中。在动态链接中,程序在运行时才会将这个库函数加载到内存中,并将它的代码地址放到栈帧中的动态链接里,方便程序调用。

总之,解析阶段是程序执行的必要步骤。通过符号引用和直接引用之间的转换,程序能够正确地访问方法和数据。静态链接和动态链接是不同的链接方式,每种方式都有它的优缺点。在实际的开发中,需要根据情况选择适合的链接方式,以便程序能够更好地运行和维护。

🍊 初始化

初始化是指在使用一个类之前,为该类的静态变量分配内存并为其赋初值的过程。在Java中,静态变量的初始化是在类加载阶段完成的。静态变量包括静态成员变量和静态代码块。

静态成员变量的初始化可以在声明时直接赋值,也可以在静态代码块中完成赋值操作。例如,在类中声明一个静态变量a,可以在声明时直接赋值为12,如下所示:

public static final int a = 12;

在类加载阶段的准备阶段,会为静态变量a分配内存并赋予默认值0,而在初始化阶段,就会将a的值修改为12,以满足代码要求。

静态代码块也是初始化阶段的一部分,它可以包含一些在类加载时需要执行的代码块。静态代码块中的代码会在类加载阶段进行执行,并且只会执行一次。例如,可以声明一个静态代码块,在其中输出一段信息,如下所示:

static {
    System.out.println("初始化静态代码块");
}

静态代码块会在类加载时执行,输出相应的信息。

类中的静态变量还可以是一个对象,在类加载时进行实例化。例如,在类中声明一个静态变量user,可以在静态代码块中进行实例化,如下所示:

public static User user;

static {
    user = new User();
}

静态代码块中的代码会在类加载时执行,将user对象进行实例化。

总之,初始化是Java中一个非常重要的阶段,它会为静态变量分配内存并赋初值,也可以执行一些在类加载时需要执行的代码块,以确保类的正常使用。

🍊 使用和卸载

Java 类加载机制的最后一步:使用和卸载,这一步是整个加载流程的结束。在这一步中,Java 虚拟机会将已经加载的类进行初始化,然后将其交给应用程序使用。但是,当这些类不再被应用程序使用时,Java 虚拟机会将其卸载,以释放内存空间。

这里的“使用”是指当应用程序需要使用一个类时,Java 虚拟机会先检查该类是否已经被加载。如果已经加载,就直接将其返回给应用程序使用;如果没有加载,就会按照一定的加载流程进行加载,并将其交给应用程序使用。比如,当应用程序需要使用一个 java.util.Date 类的实例时,Java 虚拟机会先检查该类是否已经被加载,如果没有加载,就会按照加载流程加载该类,并将其返回给应用程序使用。

而“卸载”则是指当应用程序不再使用某个类时,Java 虚拟机会将其卸载。这个过程中,Java 虚拟机会自动判断该类是否还有被其他类所引用的对象。如果没有,就会将该类从内存中卸载,以释放内存空间,从而提高程序的性能和效率。

值得注意的是,类的使用和卸载并不是一成不变的,具体情况还要考虑多种因素。比如,如果某个类被频繁使用,Java 虚拟机就不会将其卸载,以避免频繁加载该类带来的性能损失。另外,Java 虚拟机并不会一定在某个时刻立即卸载某个类,而是根据一定的条件和策略进行卸载,以保证程序的正常运行和稳定性。

优快云

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

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

📥博主的人生感悟和目标

探寻内心世界,博主分享人生感悟与未来目标

  • 🍋程序开发这条路不能停,停下来容易被淘汰掉,吃不了自律的苦,就要受平庸的罪,持续的能力才能带来持续的自信。我本身是一个很普通程序员,放在人堆里,除了与生俱来的盛世美颜,就剩180的大高个了,就是我这样的一个人,默默写博文也有好多年了。
  • 📺有句老话说的好,牛逼之前都是傻逼式的坚持,希望自己可以通过大量的作品、时间的积累、个人魅力、运气、时机,可以打造属于自己的技术影响力。
  • 💥内心起伏不定,我时而激动,时而沉思。我希望自己能成为一个综合性人才,具备技术、业务和管理方面的精湛技能。我想成为产品架构路线的总设计师,团队的指挥者,技术团队的中流砥柱,企业战略和资本规划的实战专家。
  • 🎉这个目标的实现需要不懈的努力和持续的成长,但我必须努力追求。因为我知道,只有成为这样的人才,我才能在职业生涯中不断前进并为企业的发展带来真正的价值。在这个不断变化的时代,我必须随时准备好迎接挑战,不断学习和探索新的领域,才能不断地向前推进。我坚信,只要我不断努力,我一定会达到自己的目标。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Java程序员廖志伟

赏我包辣条呗

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

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

打赏作者

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

抵扣说明:

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

余额充值