Java类加载的故事-修正终结版

2023-10-17 04:32

本文主要是介绍Java类加载的故事-修正终结版,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

文章目录

    • 故事起源:
    • 故事内容:
      • JAVA的类加载机制:
    • 故事背景:
    • 故事序幕:
    • 第一章:代码拆分
    • 第二章:class代码混淆
    • 第三章:实现热加载
    • 第四章:到底加载的是哪个类?
    • 第五章:实现同类多版本共存
    • 第六章:引入JAVA提供的SPI机制实现工资计算服务加载
    • 总结:

带你玩转不一样的JAVA.
== 楼兰:神秘Java宝藏 ==

之前发过的两篇类加载的故事由于当时实力不够,颇有错误,这次重新整理了一个修正终结版,并配合视频讲解。
博文配合视频:https://www.bilibili.com/video/BV11a4y1p7eP
如果觉得有帮助,烦请点赞鼓励下。

故事起源:

​ java指令到底干了些什么?我们写的java代码是如何被加载到jvm内存中执行的?

故事内容:

​ 回顾java类加载机制。 实战自定义的类加载器。实现自己的热加载。实现同类多个版本共存。

JAVA的类加载机制:

​ 1、java的类加载体系:

BootStrap Classloader > ExtClassLoader > AppClassLoader

​ 每种类加载器都有他自己的加载目录。

​ 2、双亲委派:一个java类加载进JVM内存的过程:

  • 每个类加载器对他加载过的类都有一个缓存。

  • 向上委托查找,向下委托加载。

​ 3、JDK的类加载对象:

ClassLoader -> SecureClassLoader ->  URLClassLoader -> ExtClassLoader,AppClassLoader

故事背景:

​ 有一个OA系统, 每个月需要定时的计算大家的工资。

故事序幕:

​ 有一个程序员,想要修改工资的计算方法。偷偷加工资。

​ 他偷偷的修改OA系统中计算工资的方法源码,给自己加了两成的工资。

第一章:代码拆分

​ 程序员偷偷加了工资,但是,肯定会被经理发现。OA系统的源码,经理也可以看到。

​ 把计算工资的方法,从OA系统的源码中抽出来,放到另外一个jar包中。

这样的jar包文件可以放在哪些地方?放到网络地址、maven仓库(drools规则引擎)

第二章:class代码混淆

​ 我们的jar包最终都可以通过反编译的方式,被发现。需要对jar包进行混淆。

​ 第一个想法:对class文件做手脚:

​ 修改.class的文件后缀,改为.myclass.

​ 自定义一个类加载,读取.myclass文件。

怎么实现一个自定义类加载? 1 继承一个系统类加载器 SecureClassLoader ; 2 覆盖父类的findClass方法, 在方法中,调用defineClass方法在JVM内存中定义一个类。

扩展:虽然改了文件后缀,但是文件的内容没有改。所以更安全的方式,是把class文件里面的内容也稍微做下改动。

​ 程序员可以通过简单修改二进制文件的方式,对class文件的内容做少量的修改,这样class文件的安全性得到进一步提高。

​ 最终这种处理方法还是要集成到jar包中。所以还是要实现一个从jar包中加载class类的自定义类加载器。

​ 第二种更加完善的方式:自定义一个类加载器,从jar包中去找到对应的class文件,加载到JVM中。

​ 把上面的两种方式结合起来,

通过这种方式,我们可以自定义class文件的加载逻辑,最终实现class文件的代码混淆。

代码的安全性得到进一步的提高。

第三章:实现热加载

​ 总公司临时需要核算工资。程序员需要赶紧将工资计算的方式还原回去。又希望在发工资的时候,将工资计算的方式改回来。

​ 这时候,程序员发现, 每次修改计算工资方法的jar包,都需要重启OA系统才能生效。这样显然更容易让别人起疑心。这时,程序员就需要实现热加载。我们计算工资方法的jar包,更新后,立即生效,不用重启OA系统。

​ 回到我们的问题:JAVA里的每一个类加载器,对他加载过的类,都会保留一个缓存。正是这个缓存,导致我们无法实现热加载。

​ 我们通过每一次new一个SalaryJarLoader的方式,实现了热加载。

​ 热加载既然很好用,为什么很少用到呢?因为热加载机制有一个加载的过程,很容易出错。还有个更大的问题,热加载必然产生非常多的垃圾对象。

​ 在ClassLoader的loadClass方法中,还传入了一个Boolean的resolve参数,这个是干什么的?

一个类的类加载过程通常分为 加载、连接、初始化 三个部分,具体的行为在java虚拟机规范中都有详细的定义,这里只是大致的说明一下。

  • 加载Loading: 这个过程是Java将字节码数据从不同的数据源读取到JVM中,并映射成为JVM认可的数据结构。而如果输入的Class不符合JVM的规范,就会抛出异常。这个阶段是用户可以参与的阶段,我们自定义的类加载器,就是工作在这个过程。
  • 连接Linking:这个是核心的步骤。又可以大致分为三个小阶段:1、验证:检查JVM加载的字节信息是否符合Java虚拟机规范,否则就会报错。这一阶段是JVM的安全大门,防止黑客大神的恶意信息或者不合规信息危害JVM的正常运行。2、准备:这一阶段创建类或接口的静态变量,并给这些静态变量赋一个初始值(不是最终指定的值),这一部分的作用更大的是预分配内存。3、解析:这一步主要是将常量池中的符号引用替换为直接引用。例如我们有个类A调用了类B的方法,这些在代码层次还好只是一些对计算机没有意义的符号引用,在这一阶段就会转换成计算机所能理解的堆栈、引用等这些直接引用。
  • 初始化Initialization:这一步才是真正去执行类初始化的代码逻辑。包括执行static静态代码块,给静态变量赋值等。

实际上resolve这个参数就是表示需不需要进行连接阶段。从这里也能看出热加载机制的另一个很大的问题:热加载机制将一些在编译阶段就可以检查出来的问题全都延迟到了运行时,这对整个程序的安全性是一个很大的威胁。

第四章:到底加载的是哪个类?

程序员在某一次调试的过程当中,不小心在OA系统里留下了一个SalaryCaler类。这时,每次加载的都是OA系统内的这个SalayrCaler类,而不是我们预期的jar包里的计算类。这样就导致了我们之前的热加载机制全部失败了。

经过分析,问题就出在了双亲委派机制。

通过打破双亲委派机制,我们就实现了工资计算类优先从jar包中加载,而不取OA系统内的SalaryCaler类。

我们来想一下,我们这种方式有什么问题?

​ 我们把com.roy这样的包名,硬编码方式写到SalaryJarLoader中,这肯定是给系统以后的扩展留下了一个很大的隐患。所以,我们接下来,必须要找到一个方式,把com.roy这样的硬编码从程序中移除。

第五章:实现同类多版本共存

经过之前的分享,我们知道了在AppClassLoader和SalaryJarLoader的缓存中,都有一个com.roy.SalaryCaler。那我们可不可以把这两个类都拿出来?同时打印原价计算的salary和修改后的salary。

当程序员想要加载出SalaryJarLoader中的SalaryCaler类时,出现了一个神奇的异常

com.roy.SalaryCaler cannot be cast to com.roy.SalaryCaler

SalaryCaler我是谁?谁是我?我怎么不能转换成我自己?

其实这就是我们打破双亲委派机制之后,出现的问题。问题的根源就在于打破双亲委派机制后,AppClassLoader和SalaryJarLoader都分别加载出了一个SalaryCaler的类,而两个ClassLoader中的SalaryCaler类是无法进行类型转换的。

既然类型无法强转,那我们就只能通过反射的方式,来执行SalaryJarLoader中的SalaryCaler类的cal方法。通过这样的方式,我们实现了同类多版本共存。

​ 但是这跟我们上一章节提到的消灭com.roy硬编码,有什么关系呢?

​ 我们可以覆盖父类的双亲委派机制。优先从本地目录加载类,本地目录加载不到,再走双亲委派机制进行加载。通过这种方式,就解决了我们上一章节留下的要消灭com.roy硬编码的问题。

​ 但是我们又遇到一个让人非常不爽的问题:工资计算类到现在只能通过反射的方式去操作,而没办法声明成一个正常的类。

第六章:引入JAVA提供的SPI机制实现工资计算服务加载

​ 目的:是要让工资计算类SalaryClaer能够像一个正常类一样声明、使用。所谓的这个正常声明,其实就是说要把SalaryClaer转换成一个由AppClassLaoder加载出来的对象。

​ 分析: 就只能使用java的多态来表示这个问题。就是在OA系统里声明一个接口,而SalaryJarLoader提供接口的实现类。

​ 方案:引入java提供的SPI机制实现服务加载。

​ 我们就通过JAVA的SPI机制ServiceLoader.load(SalaryCalService.class,classloader); 实现了把工资计算类声明成一个对象的方式。

​ 通过调整线程向下文类加载器的方式,实现了工资计算类的稳定加载。

总结:

​ JAVA类加载机制,是从JDK源码向JVM底层学习的一个门户。

​ 第四章、第五章实现的加载流程,模拟的tomcat的类加载机制。

​ 第六章,SPI机制。 JDBC、 ShardingSphere、Dubbo

快乐学习,爱上JAVA。

这篇关于Java类加载的故事-修正终结版的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/222974

相关文章

Java实现检查多个时间段是否有重合

《Java实现检查多个时间段是否有重合》这篇文章主要为大家详细介绍了如何使用Java实现检查多个时间段是否有重合,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录流程概述步骤详解China编程步骤1:定义时间段类步骤2:添加时间段步骤3:检查时间段是否有重合步骤4:输出结果示例代码结语作

Java中String字符串使用避坑指南

《Java中String字符串使用避坑指南》Java中的String字符串是我们日常编程中用得最多的类之一,看似简单的String使用,却隐藏着不少“坑”,如果不注意,可能会导致性能问题、意外的错误容... 目录8个避坑点如下:1. 字符串的不可变性:每次修改都创建新对象2. 使用 == 比较字符串,陷阱满

Java判断多个时间段是否重合的方法小结

《Java判断多个时间段是否重合的方法小结》这篇文章主要为大家详细介绍了Java中判断多个时间段是否重合的方法,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录判断多个时间段是否有间隔判断时间段集合是否与某时间段重合判断多个时间段是否有间隔实体类内容public class D

IDEA编译报错“java: 常量字符串过长”的原因及解决方法

《IDEA编译报错“java:常量字符串过长”的原因及解决方法》今天在开发过程中,由于尝试将一个文件的Base64字符串设置为常量,结果导致IDEA编译的时候出现了如下报错java:常量字符串过长,... 目录一、问题描述二、问题原因2.1 理论角度2.2 源码角度三、解决方案解决方案①:StringBui

Java覆盖第三方jar包中的某一个类的实现方法

《Java覆盖第三方jar包中的某一个类的实现方法》在我们日常的开发中,经常需要使用第三方的jar包,有时候我们会发现第三方的jar包中的某一个类有问题,或者我们需要定制化修改其中的逻辑,那么应该如何... 目录一、需求描述二、示例描述三、操作步骤四、验证结果五、实现原理一、需求描述需求描述如下:需要在

Java中ArrayList和LinkedList有什么区别举例详解

《Java中ArrayList和LinkedList有什么区别举例详解》:本文主要介绍Java中ArrayList和LinkedList区别的相关资料,包括数据结构特性、核心操作性能、内存与GC影... 目录一、底层数据结构二、核心操作性能对比三、内存与 GC 影响四、扩容机制五、线程安全与并发方案六、工程

JavaScript中的reduce方法执行过程、使用场景及进阶用法

《JavaScript中的reduce方法执行过程、使用场景及进阶用法》:本文主要介绍JavaScript中的reduce方法执行过程、使用场景及进阶用法的相关资料,reduce是JavaScri... 目录1. 什么是reduce2. reduce语法2.1 语法2.2 参数说明3. reduce执行过程

如何使用Java实现请求deepseek

《如何使用Java实现请求deepseek》这篇文章主要为大家详细介绍了如何使用Java实现请求deepseek功能,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录1.deepseek的api创建2.Java实现请求deepseek2.1 pom文件2.2 json转化文件2.2

Java调用DeepSeek API的最佳实践及详细代码示例

《Java调用DeepSeekAPI的最佳实践及详细代码示例》:本文主要介绍如何使用Java调用DeepSeekAPI,包括获取API密钥、添加HTTP客户端依赖、创建HTTP请求、处理响应、... 目录1. 获取API密钥2. 添加HTTP客户端依赖3. 创建HTTP请求4. 处理响应5. 错误处理6.

Spring AI集成DeepSeek的详细步骤

《SpringAI集成DeepSeek的详细步骤》DeepSeek作为一款卓越的国产AI模型,越来越多的公司考虑在自己的应用中集成,对于Java应用来说,我们可以借助SpringAI集成DeepSe... 目录DeepSeek 介绍Spring AI 是什么?1、环境准备2、构建项目2.1、pom依赖2.2