首页 > 编程知识 正文

java io面试题,java类加载器有什么用

时间:2023-05-06 08:22:00 阅读:17512 作者:129

类加载器类加载器顾名思义就是类加载。 虚拟机将描述类的数据从类代码文件加载到存储器中,进行数据的验证、转换分析和初始化,最终形成虚拟机可以直接使用的Java类型。 这就是虚拟机的类加载机制。 了解java类加载的工作原理,可以快速解决运行时的各种加载问题,快速找出其背后的本质原因,也是解决疑难杂症的工具。 因此,学好班级加载原理也很重要。

classloader加载过程类由加载、验证、准备、分析、初始化、使用和卸载类七个阶段组成,从加载到虚拟机内存中到卸载。 其中验证、准备、分析三个部分统称为连接。 现在,我们将详细介绍以下类加载的每个过程:

整个classloader加载过程仍然非常复杂,具体细节可参见《深入理解java虚拟机》详细了解。 为了便于记忆,我们可以用一句话来描述其加载的全过程。 “家宴准备了西洋料理”,也就是家宴(路宴)验证)西)解析)式(初始化)料理。 我保证你以后很快就会想起来。

虽然classloader加载过程有五个复杂的步骤,但实际上除了加载以外,其他四个步骤都由JVM虚拟机控制,我们除了根据其规范进行开发外,没有多少空间可以干预。 加载是我们控制classloader实现特殊目的的最重要手段。 也是接下来介绍的重点。

classloader父母委托机制classloader父母委托机制是指,如果多个类加载器之间存在父子关系,则某个class类具体由哪个加载器加载。 其具体过程是,一个类加载时,它首先不加载,而是委托自己的父类加载,父类委托自己的父类。 因此,所有类的加载都委托给顶级父类(Bootstrap Classloader ),子加载器仅在父类自身无法完成此加载请求时才尝试自行加载。 使用父母的代理模型,Java类与加载器具有优先级层次关系。 该层次模型避免类的重复加载,避免核心类被不同的类加载器加载到内存中引起冲突和混乱,从而确保Java核心库的安全。

如上图所示,整个java虚拟机的类加载层次关系是启动类加载器(Bootstrap Classloader )位于JAVA_HOME/lib目录下,由虚拟机识别的类库常见的基础库,如java.util. ,java.io.、java.lang.**等由根加载程序加载。

“扩展类加载器”(Extention Classloader )加载JVM扩展类,如swing系列、嵌入式js引擎和xml解析器。 这些类库以javax开头,JAVA_HOME/lib/ext目录中有一个jar包。

“APP应用程序加载器”(Application Classloader )也称为系统类加载器,用于加载由“用户路径”(ClassPath )指定的类库。 我们自己编写的代码和使用的第三方jar包,由此加载的“自定义加载器”(Custom Classloader )通常是我们为特定目的实施的自定义加载器。 稍后将详细说明其作用和使用场景。

父母委托的机制看起来很复杂,但其本身的核心代码逻辑非常明确和简单。 重点提取班路父母委托的核心代码如下,但只有20行左右。

类加载器的应用场景类加载器是java语言的创新,也是java语言流行的重要原因。 通过灵活地定义classloader加载机制,您可以执行许多操作,包括解决类冲突问题、热加载和热部署以及jar包的加密保护。 接下来,我们将逐一介绍这些特殊场景。

另一方面,依赖纠纷做多人共同开发的大项目的同学可能深有感触。 基于maven的pom进制可以方便地进行依赖管理,但maven依赖的传递性使我们的依赖错综复杂,导致类冲突的引入问题。 最典型的是NoSuchMethodException异常。

蚂蚁平时的项目开发也会遇到类似的问题吗? 答案是肯定的。 例如,蚂蚁内部也有很多成熟的中间件,由不同的中间件团队负责。 那么,如果一个项目引入了不同的中间件,该如何避免依赖冲突呢? 首先,用非常简单的场景说明为什么会发生班级冲突的问题。 有的业务引用了metaq等消息中间件和dubbo等微服务中间件,这两个中间件同时引用了fastjson-2.0和fastjson-3.0的版本,业务本身也引用了fastjson-3.0 这三个版本的区别在于类classA的方法数不同。 根据maven依赖关系处理机制,引用路径最短的fastjson-1.0是APP应用程序的最终依赖关系,而其他两个版本的fastjson将被忽略。 中间件是method2) )在调用方法时抛出方法不会发现异常。 您可能觉得将所有依赖快速强森的版本升级到3.0就能解决问题了。 确实这样可以解决问题,但在实际运用中不太现实。 首先,中间件团队和业务团队之间不是团队,无法进行有效的合作。 其次,中间件的稳定性需要得到保障,不能因为软件包冲突问题而升级版本。 此外,一个中间件依赖的软件包可能有数百个。 如果单纯依赖软件包升级,则无法解决

决,不仅稳定性难以保障,排包耗费的时间恐怕就让人窒息了。

那如何解决包冲突的问题呢?答案就是pandora(奋斗的芝麻),通过自定义类加载器,为每个中间件自定义一个加载器,这些加载器之间的关系是平行的,彼此没有依赖关系。这样每个中间件的classloader就可以加载各自版本的fastjson。因为一个类的全限定名以及加载该类的加载器两者共同形成了这个类在JVM中的惟一标识,这也是阿里pandora实现依赖隔离的基础。可能到这里,你又会有新的疑惑,根据双亲委托模型,App Classloader分别继承了Custom Classloader.那么业务包中的fastjson的class在加载的时候,会先委托到Custom ClassLoader。这样不就会导致自身依赖的fastjson版本被忽略吗?确实如此,所以奋斗的芝麻又是如何做的呢?首先每个中间件对应的ModuleClassLoader在加载中间对应的class文件的同时,根据中间件配置的export.index负责将要需要透出的class(主要是提供api接口的相关类)索引到exportedClassHashMap中,然后应用程序的类加载器会持有这个exportedClassHashMap,因此应用程序代码在loadClass的时候,会优先判断exportedClassHashMap是否存在当前类,如果存在,则直接返回,如果不存在,则再使用传统的双亲委托机制来进行类加载。这样中间件MoudleClassloader不仅实现了中间件的加载,也实现了中间件关键服务类的透出。

我们可以大概看下应用程序类加载的过程

二、热加载

在开发项目的时候,我们需要频繁的重启应用进行程序调试,但是java项目的启动少则几十秒,多则几分钟。如此慢的启动速度极大地影响了程序开发的效率,那是否可以快速的进行启动,进而能够快速的进行开发验证呢?答案也是肯定的,通过classloader我们可以完成对变更内容的加载,然后快速的启动。

常用的热加载方案有好几个,接下来我们介绍下spring官方推荐的热加载方案,即spring boot devtools。

首先我们需要思考下,为什么重新启动一个应用会比较慢,那是因为在启动应用的时候,JVM虚拟机需要将所有的应用程序重新装载到整个虚拟机。可想而知,一个复杂的应用程序所包含的jar包可能有上百兆,每次微小的改动都是全量加载,那自然是很慢了。那么我们是否可以做到,当我们修改了某个文件后,在JVM中替换到这个文件相关的部分而不全量的重新加载呢?而spring boot devtools正是基于这个思路进行处理的。

如上图所示,通常一个项目的代码由以上四部分组成,即基础类、扩展类、二方包/三方包、以及我们自己编写的业务代码组成。上面的一排是我们通常的类加载结构,其中业务代码和二方包/三方包是由应用加载器加载的。而实际开发和调试的过程中,主要变化的是业务代码,并且业务代码相对二方包/三方包的内容来说会更少一些。因此我们可以将业务代码单独通过一个自定义的加载器Custom Classloader来进行加载,当监控发现业务代码发生改变后,我们重新加载启动,老的业务代码的相关类则由虚拟机的垃圾回收机制来自动回收。其工程流程大概如下。有兴趣的同学可以去看下源码,会更加清楚。

RestartClassLoader为自定义的类加载器,其核心是loadClass的加载方式,我们发现其通过修改了双亲委托机制,默认优先从自己加载,如果自己没有加载到,从从parent进行加载。这样保证了业务代码可以优先被RestartClassLoader加载。进而通过重新加载RestartClassLoader即可完成应用代码部分的重新加载。

三、热部署

热部署本质其实与热加载并没有太大的区别,通常我们说热加载是指在开发环境中进行的classloader加载,而热部署则更多是指在线上环境使用classloader的加载机制完成业务的部署。所以这二者使用的技术并没有本质的区别。那热部署除了与热加载具有发布更快之外,还有更多的更大的优势就是具有更细的发布粒度。我们可以想像以下的一个业务场景。

假设某个营销投放平台涉及到4个业务方的开发,需要对会场业务进行投放。而这四个业务方的代码全部都在一个应用里面。因此某个业务方有代码变更则需要对整个应用进行发布,同时其它业务方也需要跟着回归。因此每个微小的发动,则需要走整个应用的全量发布。这种方式带来的稳定性风险估且不说,整个发布迭代的效率也可想而知了。这在整个互联网里,时间和效率就是金钱的理念下,显然是无法接受的。

那么我们完全可以通过类加载机制,将每个业务方通过一个classloader来加载。基于类的隔离机制,可以保障各个业务方的代码不会相互影响,同时也可以做到各个业务方进行独立的发布。其实在移动客户端,每个应用模块也可以基于类加载,实现插件化发布。本质上也是一个原理。在阿里内部像阿拉丁投放平台,以及crossbow容器化平台,本质都是使用classloader的热加载技术,实现业务细粒度的开发部署以及多应用的合并部署。

四、加密保护

众所周知,基于java开发编译产生的jar包是由.class字节码组成,由于字节码的文件格式是有明确规范的。因此对于字节码进行反编译,就很容易知道其源码实现了。因此大致会存在如下两个方面的诉求。例如在服务端,我们向别人提供三方包实现的时候,不希望别人知道核心代码实现,我们可以考虑对jar包进行加密,在客户端则会比较普遍,那就是我们打包好的apk的安装包,不希望被人家反编译而被人家翻个底朝天,我们也可以对apk进行加密。

jar包加密的本质,还是对字节码文件进行操作。但是JVM虚拟机加载class的规范是统一的,因此我们在最终加载class文件的时候,还是需要满足其class文件的格式规范,否则虚拟机是不能正常加载的。因此我们可以在打包的时候对class进行正向的加密操作,然后,在加载class文件之前通过自定义classloader先进行反向的解密操作,然后再按照标准的class文件标准进行加载,这样就完成了class文件正常的加载。因此这个加密的jar包只有能够实现解密方法的classloader才能正常加载。

我们可以贴一下简单的实现方案

这样整个jar包的安全性就有一定程度的提高,至于更高安全的保障则取决于加密算法的安全性了以及如何保障加密算法的密钥不被泄露的问题了。这有种套娃的感觉,所谓安全基本都是相对的。并且这些方法也不是绝对的,例如可以通过对classloader进行插码,对解密后的class文件进行存储;另外大多数JVM本身并不安全,还可以修改JVM,从ClassLoader之外获取解密后的代码并保存到磁盘,从而绕过上述加密所做的一切工作,当然这些操作的成本就比单纯的class反编译就高很多了。所以说安全保障只要做到使对方破解的成本高于收益即是安全,所以一定程度的安全性,足以减少很多低成本的攻击了。

总结

本文对classloader的加载过程和加载原理进行了介绍,并结合类加载机制的特征,介绍了其相应的使用场景。由于篇幅限制,并没有对每种场景的具体实现细节进行介绍,而只是阐述了其基本实现思路。或许大家觉得classloader的应用有些复杂,但事实上只要大家对class从哪里加载,搞清楚loadClass的机制,就已经成功了一大半。正所谓万变不离其宗,抓住了本质,其它问题也就迎刃而解了。

版权声明:该文观点仅代表作者本人。处理文章:请发送邮件至 三1五14八八95#扣扣.com 举报,一经查实,本站将立刻删除。