浅谈类加载器与类加载故事实操
前言:不断学习就是程序员的宿命
一、类加载器概述
类加载器是JVM执行类加载机制的前提。
作用:ClassLoader是Java的核心组件,所有的Class都是由ClassLoader进行加载的,ClassLoader负责通过各种方式将Class信息的二进制数据流读入JVM内部,转换为一个与目标类对象的java.lang.Class对象实例,然后交给JVM进行链接、初始化等操作。因此,ClassLoader在整个装载阶段,只能影响到类的加载,而无法通过ClassLoader去改变类的链接和初始化行为。至于它是否可以运行,则由执行引擎决定。
二、类加载器分类
类的加载分类:显示加载与隐式加载
1.显示加载:在代码中通过调用ClassLoader加载class对象。例如:Class.forName(name)等
2.隐式加载:不直接在代码中调用ClassLoader方法加载class对象,而是通过虚拟机自动加载到内存中。例如:User user=new User();
JVM支持两种类型的类加载器,分别为引导类加载器(Bootstrap ClassLoader)和自定义类加载器(User-Defined ClassLoader)。从概念上来说,自定义类加载器一般指的是程序中由开发人员自定义的一类类加载器,但是Java虚拟机规范却没有这么定义,而是将所有派生于抽象类ClassLoader的类加载器都划分为自定义类加载器。无论类加载器的类型如何划分,在程序中我们常见的类加载器结构主要是如下情况(以企业常见的JDK8为例,JDK9有变化)
(1)引导类加载器(Bootstrap ClassLoader):
该类由C/C++语言实现,嵌套在JVM内部;用来加载Java核心库(JAVA_HOME/jre/lib/rt.jar或sun.boot.class.path路径下的内容);出于安全考虑,只加载包名为java、 javax、sun等开头的类
(2)扩展类加载器(Extension ClassLoader)
Java语言编写,继承于ClassLoader类,父类加载器为启动类加载器;从JAVA_HOME/jre/lib/ext子目录下加载类库
(3)系统类加载器(AppClassLoader)
Java语言编写,继承于ClassLoader类,父类记载器为扩展类加载器;它是用户自定义类加载器的默认父加载器
自定义类加载器:
(1)在Java日常应用程序开发中,类的加载几乎是由上述3类加载器相互配合执行的。在必要时,我们还可以自定义类加载器,来定制类的加载方式
(2)体现Java语言强大生命力和巨大魅力的关键因素之一便是Java开发者可以自定义类加载器来实现类库的动态加载,加载源可以是本地Jar包,也可以是网络上的远程资源。
(3)通过类加载器可以实现非常巧妙的插件机制,这方面的实际应用案例举不胜举。例如Eclipse的插件机制。类加载器为应用程序提供了一种动态增加新功能的机制,这种机制无须重新打包发布应用程序就能实现
(4)自定义类加载器能够实现应用隔离,例如tomcat、spring等中间件和组件框架都在内部定义了自定义的类加载器,并通过自定义的类加载器隔离不同的组件模块。这种机制比C/C++程序要好太多,想不修改C/C++程序就能为其新增功能,几乎是不可能的,仅仅一个兼容性便能阻挡住所有美好的设想。
(5)自定义类加载器通常需要继承于ClassLoader
三、命名空间
1.何为类的唯一性?
对应任意一个类,都需要由加载它的类加载器和这个类本身一同确认其在Java虚拟机中的唯一性。
每一个类加载都拥有一个独立的类名称空间:比较两个类是否相等,只有在这两个类是同一个类加载的前提下才有意义!
否则,即使这两个类源自同一个Class文件,被同一个虚拟机加载,只要加载他们的类加载器不同,那么这两个类就必定不相等。
2.命名空间
(1)每个类加载器都有自己的命名空间,命名空间由该加载器及所有的父加载器所加载的类组成
(2)在同一命名空间中,不会出现类的完整名字(包括类的包名)相同的两个类
(3)不同的命名空间中,有可能会出现类的完整名字(包括类的包名)相同的两个类
在大型应用中,我们往往借助这一特性,来运行同一个类的不同版本。
四、双亲委派模型
双亲委派模型。但不是所有类加载都遵守这个模型,有的时候,启动类加载器所加载的类型,是可能要加载用代码的。例如JDK内部的SPI机制,用户可以在标准API框架上,提供自己的实现,JDK也需要提供默认的参考实现。比如JDBC、JNDI等很多方面都是利用这种机制,这种情况就不会使用双亲委派模型去加载,而是利用所谓的上下文加载器。
定义:如果一个类加载器在接到加载类的请求时,它首先不会自己尝试去加载这个类,而是把这个请求任务委托给父类加载器去完成,依次递归,如果父类加载器可以完成类加载任务,就成功返回,只有父类加载器无法完成此加载任务时,才自己去加载。(“典型啃老族”)
本质:规定了类的加载顺序:引导类加载器先加载,若加载不到,由扩展类加载器加载,若还加载不到,才会由系统类加载器或者自定义的类加载器进行加载。
优势:避免了类的重复加载,确保一个类的全局唯一性;保护程序安全,防止核心API被随意篡改
源码分析体现:
结论:由于Java虚拟机规范并没有明确要求类加载器的加载机制一定要使用双亲委派模型,只是建议采用这种方式而已。比如在tomcat中,类加载器所采用的加载机制就和传统的双亲委派模型有一定区别,当缺省的类加载器接收到一个类的加载任务时,首先会由它自行加载,当它加载失败时,才会将类的加载任务委派给它的超类加载器去执行,这同时也是Servlet规范推荐的一种做法。
五、类加载实操
BB了那么多概念,那么来操练一把!此处引用一个故事来带入主题
5.1 类加载故事---背景
有一个OA系统,每个月需要定时计算大家的工资。
5.2类加载故事---序幕
有一个程序员,想要修改工资的计算方法,偷偷加工资。
他偷偷修改了OA系统中计算工资方法的源码,给自己加了两成的工资(这里不要杠!仅仅作为例子而已,杠就是你对!)
5.3类加载故事---代码拆分
程序员偷偷加了工资,但是肯定会被经理发现,OA系统的源码,经理也可以看到
那么程序员就想了:把计算工资的方法从OA系统的源码中抽出来,放到另外一个jar包中,进行代码拆分
5.4类加载故事--代码混淆
我们的jar最终都是可以通过反编译的方式被发现,为了不想被经理发现,我们需要对jar包进行混淆,如下:
第一个想法:对class文件动手脚,比较将后缀.class改为.myclass
对应类加载器:
但是以上想法似乎也不安全,只是修改后缀名,那么引出第二个想法:可以改变.class文件内容
在魔数前边添加1以后.class就不符合JVM规范了,也就不能反编译了,提升了安全性
程序正常执行如下:
类加载改动如下:
5.5类加载故事---JarLoader
基于上一步结果,最终都要集成到jar包中,接下来就需要自定义类加载器:JarLoader
至此回到我们的故事:此时经理就不知道我们程序员对代码做了手脚,我们就可以安安心心的拿工资了!
JarLoader加载器如下:
5.6类加载故事---热加载
故事:总公司临时需要核算工资,程序员需要赶紧将工资计算方法还原回去,又希望在发工资的时候,将工资计算的方法改回来,这样才能拿稳自己的工资。
但是基于以上代码,每次修改代码以后需要重新部署服务,这还原工资计算方法、修改工资计算方法是不是引起其他人怀疑?此时需要热加载,就是在不重启服务的情况下动态修改工资计算方法!
上边我们讲过:类的唯一性:由不同ClassLoader加载的同名类属于不同的类型,不能相互转换和兼容,即两个不同的ClassLoader加载同一个类,在虚拟机内部,会认为这两个类是完全不同的,基于这个特性,可以用来模拟热替换的实现,基本思路如下:
代码实操:
热加载是不是很好用?但是我们实际开发中还是很少用。。。,因为热加载机制有一个加载的过程,很容易出错,还有个更大的问题,热加载必然产生非常的垃圾对象。
同时热加载机制将一些在编译阶段就可以检查出来的问题都延迟到了运行时,这对整个程序的安全性是一个很大的威胁。Classloader.loadClass()还传入了resolve
5.7类加载故事---打破双亲委派机制
故事:程序员在某一次调试中,不小心在OA系统里留下了一个SalaryComputor类。这时,每次加载的都是OA系统内的这个SalaryComputor这个类,而不是我们预期的Jar包里的计算类。这样就导致我们之前的热加载机制全部失效了。经过分析,问题出在了双亲委派机制。
删除当前OA工程冗余类,则热加载恢复如下:
接下来通过打破双亲委派机制,实现工资计算类优先加载jar文件,而不取OA系统内部工资计算类。
5.8类加载故事---实现同类多版本共存
基于上一步打破双亲委派机制,进行如下操作:
故事:打印实际到手的工资和原来的工资
5.9类加载故事终章:SPI多版本加载
目前工资计算都是以反射形式进行计算,而无法声明成一个正常的类,所以使用JDK的SPI机制实现服务加载(Service Provider Interface)
解决上述问题优化切换上下文处理后如下:
最终代码优化如下:
六、Q
1.SPI机制全限定名路径问题
这是我开始的错误写法,导致SPI加载失败!