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 Predicate接口定义详解

《JavaPredicate接口定义详解》Predicate是Java中的一个函数式接口,它代表一个判断逻辑,接收一个输入参数,返回一个布尔值,:本文主要介绍JavaPredicate接口的定义... 目录Java Predicate接口Java lamda表达式 Predicate<T>、BiFuncti

Spring Security基于数据库的ABAC属性权限模型实战开发教程

《SpringSecurity基于数据库的ABAC属性权限模型实战开发教程》:本文主要介绍SpringSecurity基于数据库的ABAC属性权限模型实战开发教程,本文给大家介绍的非常详细,对大... 目录1. 前言2. 权限决策依据RBACABAC综合对比3. 数据库表结构说明4. 实战开始5. MyBA

Spring Security方法级安全控制@PreAuthorize注解的灵活运用小结

《SpringSecurity方法级安全控制@PreAuthorize注解的灵活运用小结》本文将带着大家讲解@PreAuthorize注解的核心原理、SpEL表达式机制,并通过的示例代码演示如... 目录1. 前言2. @PreAuthorize 注解简介3. @PreAuthorize 核心原理解析拦截与

一文详解JavaScript中的fetch方法

《一文详解JavaScript中的fetch方法》fetch函数是一个用于在JavaScript中执行HTTP请求的现代API,它提供了一种更简洁、更强大的方式来处理网络请求,:本文主要介绍Jav... 目录前言什么是 fetch 方法基本语法简单的 GET 请求示例代码解释发送 POST 请求示例代码解释

Java图片压缩三种高效压缩方案详细解析

《Java图片压缩三种高效压缩方案详细解析》图片压缩通常涉及减少图片的尺寸缩放、调整图片的质量(针对JPEG、PNG等)、使用特定的算法来减少图片的数据量等,:本文主要介绍Java图片压缩三种高效... 目录一、基于OpenCV的智能尺寸压缩技术亮点:适用场景:二、JPEG质量参数压缩关键技术:压缩效果对比

Java调用C++动态库超详细步骤讲解(附源码)

《Java调用C++动态库超详细步骤讲解(附源码)》C语言因其高效和接近硬件的特性,时常会被用在性能要求较高或者需要直接操作硬件的场合,:本文主要介绍Java调用C++动态库的相关资料,文中通过代... 目录一、直接调用C++库第一步:动态库生成(vs2017+qt5.12.10)第二步:Java调用C++

springboot+dubbo实现时间轮算法

《springboot+dubbo实现时间轮算法》时间轮是一种高效利用线程资源进行批量化调度的算法,本文主要介绍了springboot+dubbo实现时间轮算法,文中通过示例代码介绍的非常详细,对大家... 目录前言一、参数说明二、具体实现1、HashedwheelTimer2、createWheel3、n

Java利用docx4j+Freemarker生成word文档

《Java利用docx4j+Freemarker生成word文档》这篇文章主要为大家详细介绍了Java如何利用docx4j+Freemarker生成word文档,文中的示例代码讲解详细,感兴趣的小伙伴... 目录技术方案maven依赖创建模板文件实现代码技术方案Java 1.8 + docx4j + Fr

SpringBoot首笔交易慢问题排查与优化方案

《SpringBoot首笔交易慢问题排查与优化方案》在我们的微服务项目中,遇到这样的问题:应用启动后,第一笔交易响应耗时高达4、5秒,而后续请求均能在毫秒级完成,这不仅触发监控告警,也极大影响了用户体... 目录问题背景排查步骤1. 日志分析2. 性能工具定位优化方案:提前预热各种资源1. Flowable

基于SpringBoot+Mybatis实现Mysql分表

《基于SpringBoot+Mybatis实现Mysql分表》这篇文章主要为大家详细介绍了基于SpringBoot+Mybatis实现Mysql分表的相关知识,文中的示例代码讲解详细,感兴趣的小伙伴可... 目录基本思路定义注解创建ThreadLocal创建拦截器业务处理基本思路1.根据创建时间字段按年进