深入解析classloader:掌握Java类加载机制及完整生命周期

什么是classloader

深入解析classloader:掌握Java类加载机制及完整生命周期

类加载器,从名称来看,就是进行类加载的。虚拟机将描述类的数据,从class字节码文件加载至内存,接着对数据开展检验、转换解析以及初始化操作,最终形成能被虚拟机直接运用的Java类型,这便是虚拟机的类加载机制。了解java的类加载机制,能够快速解决运行时出现的各种加载问题,还能快速定位这些问题背后的本质原因,它是解决疑难杂症的有力工具。所以学好类加载原理十分关键。

▐classloader的加载过程

类从被加载到虚拟机内存开始,直至被卸载,这一过程涵盖了整个完整的生命周期。该生命周期包括七个阶段,分别是类加载、验证、准备、解析、初始化、使用和卸载。其中,验证、准备、解析这三个部分被统称为连接。接下来,我们能够详细了解类加载的各个过程。

classloader的整个加载过程相当复杂,具体细节可参考《深入理解java虚拟机》来深入了解 。为方便记忆,我们能用一句话表述其加载的整个过程 ,“家宴准备了西式菜” ,也就是家(加载)、宴(验证)、准备(准备)、了西(解析)、式(初始化)、菜 。确保以后能很快想起来 。

虽然类加载器的加载过程包含复杂的五个步骤,但是实际上除了加载这一步骤之外,其他四个步骤都是由JVM虚拟机控制的,我们除了依据它的规范进行开发之外,能够干预的空间并不多。而加载是我们控制类加载器实现特殊目的最重要的手段,也是接下来我们要介绍的重点。

▐classloader双亲委托机制

当多个类加载器之间存在父子关系时,就会出现某个class类具体由哪个加载器进行加载的问题,这就是classloader的双亲委托机制。其具体过程表现为:在一个类加载过程中,它首先不会自行加载,而是将加载任务委托给自己的父类,父类又会将此任务委托给自己的父类 。因此所有的类加载都会委托给顶层的父类进行加载,这个父类就是Bootstrap Classloader ,然后若父类自己无法完成这个加载请求,子加载器才会尝试自己去加载。使用双亲委派模型,Java类会随着它的加载器一起具备一种带有优先级的层次关系,通过这种层次模型,能够避免类的重复加载,还能够避免核心类被不同的类加载器加载到内存中,进而造成冲突和混乱,最终保证了Java核心库的安全。

整个java虚拟机的类加载层次关系如上图所示,启动类加载器也就是Bootstrap Classloader,它负责把/lib目录下且被虚拟机识别的类库加载到虚拟机内存中,我们常用的基础库,像java.util.**、java.io.**、java.lang.**等等,都是由根加载器加载。

扩展类加载器负责加载JVM扩展类,像swing系列、内置的js引擎、xml解析器等。这些类库以javax开头,其jar包位于/lib/ext目录中 。

应用程序加载器也被称作系统类加载器,它承担着加载用户路径上指定类库的职责,我们自己编写的代码由它加载,使用的第三方jar包也由它加载。

自定义加载器是我们为了某些特殊目的而实现的加载器,我们会在后面详细介绍它的作用,还会详细介绍它的使用场景。

双亲委托机制看上去较为复杂,然而它自身的核心代码逻辑却十分清晰简单,我们着重提取了类加载的双亲委托的核心代码如下,大概不过二十行左右。

classloader的应用场景

类加载器是Java语言的一项创新,它也是Java语言流行的重要原因之一。通过灵活定义classloader的加载机制,我们能完成很多事情,比如解决类冲突问题,实现热加载以及热部署,甚至还能实现jar包的加密保护。接下来,我们会针对这些特殊场景进行逐一介绍。

▐依赖冲突

做过多人协同开发大型项目的同学或许会有深刻感受,基于maven的pom进制能便利地进行依赖管理,然而因maven依赖的传递性,会致使我们的依赖变得错综复杂,进而引发引入类冲突的问题,最典型的便是NoSuchMethodError错误。

在阿里日常的项目开发里,是否也会碰到类似的问题呢,答案是肯定的。比如说,阿里内部有许多成熟的中间件,由不同的中间件团队负责。那么,当一个项目引入不同的中间件时,该如何避免依赖冲突的问题呢?首先,我们用一个非常简单的场景来描述出现类冲突问题的原因。

某个业务引用了消息中间件(如metaq),该业务还引用了微服务中间件(如dubbo),消息中间件和微服务中间件同时引用了fastjson - 2.0版本,消息中间件和微服务中间件同时引用了fastjson - 3.0版本,业务自身引用了fastjson - 1.0版本。这三个版本的表现存在不同之处,在classA类中,方法数目不一样,我们依据maven依赖处理的机制,引用路径最短的fastjson - 1.0会真正成为应用最终的依赖,其他两个版本的fastjson会被忽略,如此一来,中间件在调用method2()方法时,就会抛出方法找不到异常。或许你会这样说,把所有依赖fastjson的版本都进行升级,升级到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的加载机制完成业务部署,所以二者使用的技术没有本质区别。热部署除了发布速度比热加载更快之外,还有更多更大的优势,即发布粒度更细。我们可以想象一个业务场景。

假设某个营销投放平台涉及四个业务方的开发工作,要对会场业务开展投放。这四个业务方的代码都在同一个应用里。所以只要某个业务方有代码变更,就需要发布整个应用,并且其他业务方也得跟着进行回归测试。因而每有一个微小的变动,都要进行整个应用的全量发布。暂且不说这种方式带来的稳定性风险,整个发布迭代的效率也可想而知,在整个互联网秉持时间和效率就是金钱的理念下,这显然是无法接受的。

我们能够借助类加载机制,通过一个classloader来加载每个业务方。基于类的隔离机制,能保障各个业务方的代码互不影响,还能实现各个业务方独立发布。实际上在移动客户端,每个应用模块也可基于类加载实现插件化发布,本质上是同样的原理。

在阿里内部,像阿拉丁投放平台,还有crossbow容器化平台,其本质都是运用classloader的热加载技术,以此实现业务细粒度的开发部署,以及多应用的合并部署。

▐加密保护

众所周知,基于java开发编译生成的jar包由.class字节码构成,因为字节码的文件格式有明确规范。所以对字节码进行反编译,就能轻易知晓其源码实现。故而大致会有如下两方面的诉求。在服务端,当我们向他人提供三方包实现时,若不希望他人知晓核心代码实现,便可考虑对jar包进行加密。在客户端,情况较为普遍,即我们打包好的apk安装包,不希望被他人反编译从而暴露所有内容,此时也能够对apk进行加密。

jar包加密的本质,是对字节码文件做操作。然而JVM虚拟机加载class有统一规范,所以在最终加载class文件时,要满足其格式规范,不然虚拟机无法正常加载。因此,我们在打包时可对class执行正向加密操作,接着,在加载class文件前,借助自定义classloader先开展反向解密操作,随后再依照标准的class文件规范进行加载,如此便完成了class文件的正常加载。因此,这个经过加密的jar包,只有具备实现解密方法能力的classloader,才能够正常加载 。

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

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

总结

本文介绍了classloader的加载过程,还介绍了classloader的加载原理,结合类加载机制的特征,介绍了其相应的使用场景。因篇幅限制,未对每种场景的具体实现细节作介绍,仅阐述了其基本实现思路。或许大家认为classloader的应用有些繁杂,然而实际上只要大家弄明白class从何处加载,搞清楚loadClass的机制,就已经成功了一多半。正所谓万变不离其宗,抓住了本质,其他问题也就顺利解决了。

拓展阅读

作者|金雅博(行泽)

编辑|橙子君

出品|阿里巴巴新零售淘系技术

你可能想看:
« 上一篇
下一篇 »

相关推荐

java中什么是冒泡,冒泡算法

2026年09月06日

27阅读

java中冒号,java双冒号

2026年09月06日

26阅读

java中的->,java中的是什么意思

2026年09月06日

24阅读

java中的super用法,Java中super的说明

2026年09月06日

28阅读

java中反射的应用,java中反射的应用场景

2026年09月06日

26阅读

java中开关的定义,Java关键字有哪些

2026年09月06日

26阅读