双亲委派模型
- 类加载器用来把类加载到Java虚拟机中,从JDK1.2版本开始,类的加载过程采用双亲委派机制,这种机制能更好地保证Java平台的安全
- 定义:如果一个类加载器在接到加载类的请求时,它首先不会自己尝试去加载这个类,而是把这个请求任务委托给父类加载器去完成,依次递归,如果父类加载器可以完成类加载任务,就成功返回,只有父类加载器无法完成此加载任务时,才自己去加载
- 本质:规定了类加载的顺序是:引导类加载器先加载,若加载不到,由扩展类加载器加载,若还加载不到,才会由系统类加载器或自定义的类加载器进行加载
优势
- 避免类的重复加载,确保一个类的全局唯一性
- 保护程序安全,防止核心API被随意篡改
- Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要让子ClassLoader再加载一次
代码支持
- 双亲委派机制在
java.lang.ClassLoader.loadClass(String,boolean)
接口中体现
- 在当前加载器的缓存中查找有无目标类,如果有,直接返回
- 判断当前加载器的父加载器是否为空,如果不为空,则调用
parent.loadClass(name,false)
接口进行加载 - 反之,如果当前加载器的
父类加载器为空
,则调用findBootstrapClassorNull(name)
接口,让引导类加载器进行加载 - 如果通过以上3条路径都
没能成功加载
,则调用findClass(name)
接口进行加载,该接口最终会调用java.lang.ClassLoader
接口的defineClass系列的native接口
加载目标Java类
- 双亲委派的模型就隐藏在这第2和第3步中
- JDK还为核心类库提供了一层保护机制,不管是自定义的类加载器,还是系统类加载器抑或扩展类加载器最终都必须调用
java.lang.ClassLoader.defineclass(String,byte[],int,int,ProtectionDomain)
方法 - 而该方法会执行
preDefineClass()
接口,该接口中提供了对JDK核心类库的保护
劣势
- 检查类是否加载的委托过程是单向的,这个方式虽然从结构上说比较清晰,使各个
ClassLoader
的职责非常明确,但是同时会带来一个问题 - 顶层的ClassLoader
无法访问底层的
ClassLoader`所加载的类
- 通常情况下,启动类加载器中的类为系统核心类,包括一些重要的系统接口,而在应用类加载器中,为应用类
- 按照这种模式,应用类访问系统类自然是没有问题,但是系统类访问应用类就会出现问题
- 比如在系统类中提供了一个接口,该接口需要在应用类中得以实现,该接口还绑定一个工厂方法,用于创建该接口的实例,而接口和工厂方法都在启动类加载器中。这时,就会出现该工厂方法
无法创建由应用类加载器加载的应用实例
的问题
结论
- 由于Java虚拟机规范并没有明确要求类加载器的加载机制一定要使用双亲委派模型
- 只是建议采用这种方式而已
破坏双亲委派机制
-
双亲委派模型并不是一个具有强制性约束的模型,而是Java设计者推荐给开发者们的类加载器实现方式
-
在Java的世界中大部分的类加载器都遵循这个模型,但也有例外的情况,直到Java模块化出现为止,双亲委派模型主要出现过3次较大规模被破坏的情况
-
只要有明确的目的和充分的理由,突破旧有原则无疑是一种创新
loadClass方法被覆盖
双亲委派模型的第一次“被破坏”其实发生在双亲委派模型出现前
-
由于双亲委派模型在JDK 1.2之后才被引入,但是类加载器的概念和抽象类java.lang.ClassLoader则在Java的第一个版本中就已经存在
-
面对已经存在的用户自定义类加载器的代码,Java设计者们引入双亲委派模型时不得不做出一些妥协
-
为了兼容这些己有代码,无法再以技术手段避免loadClass方法被子类覆盖的可能性,只能在JDK1.2之后的java.lang.ClassLoader中添加一个新的protected方法findClass()
-
并引导用户编写的类加载逻辑时尽可能去重写这个方法,而不是在loadClass()中编写代码
-
双亲委派的具体逻辑就实现在loadClass方法里
线程上下文类加载器的使用
- 双亲委派模型的第二次“被破坏”是由这个模型自身的缺陷导致的
- 双亲委派很好地解决了各个类加载器协作时基础类型的一致性问题——越基础的类由越上层的加载器加载
- 基础类型之所以被称为“基础”,是因为它们总是作为被用户代码继承、调用的API存在,但程序设计往往没有绝对不变的完美规则
- 基础类型可能要调用用户代码
- 一个典型的例子便是JNDI服务,JNDI现在已经是Java的标准服务,它的代码由启动类加载器来完成加载(在JDK 1.3时加入到rt.jar的),肯定属于Java中很基础的类型了
- 但JNDI存在的目的就是对资源进行查找和集中管理,它需要调用由其他厂商实现并部署在应用程序的ClassPath下的JNDI服务提供者接口(Service Provider Interface,SPI)的代码(在Java平台中,通常把核心类rt.jar中提供外部服务、可由应用层自行实现的接口称为SPI)
- 为了解决这个困境,Java的设计团队只好引入了线程上下文类加载器
- 这个类加载器可以通过java.lang.Thread类的setContextClassLoader()方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器
- 这是一种
父类加载器去请求子类加载器完成类加载的行为
,这种行为实际上是打通了双亲委派模型的层次结构来逆向使用类加载器,己经违背了双亲委派模型的一般性原则 - JDK6时,JDK提供了
java.util.ServiceLoader
类,以META-INF/services中的配置信息,辅以责任链模式,这才算是给SPI的加载提供了一种相对合理的解决方案
默认上下文加载器就是应用类加载器,这样以上下文加载器为中介,使得启动类加载器中的代码也可以访问应用类加载器中的类
程序动态性的追求
- 代码热替换(Hot Swap)
- 模块热部署(Hot Deployment)
- 它自定义的类加载器机制的实现,每一个程序模块(OSGi中称为Bundle)都有一个自己的类加载器,当需要更换一个Bundle时,就把Bundle连同类加载器一起换掉以实现代码的热替换
- 在OSGi环境下,类加载器不再双亲委派模型推荐的树状结构,而是进一步发展为更加复杂的网状结构
热替换
-
热替换是指在程序的运行过程中,不停止服务,只通过替换程序文件来修改程序的行为
-
基本上大部分脚本语言都是天生支持热替换的,比如:PHP,只要替换了PHP源文件,这种改动就会立即生效,而无需重启Web服务器
-
对Java来说,热替换并非天生就支持,如果一个类已经加载到系统中,通过修改类文件,并无法让系统再来加载并重定义这个类
-
在Java中实现这一功能的一个可行的方法就是灵活运用ClassLoader
-
由不同ClassLoader加载的同名类属于不同的类型,不能相互转换和兼容。即两个不同的ClassLoader加载同一个类,在虚拟机内部,会认为这2个类是完全不同的
沙箱安全机制
- 保护Java原生的JDK代码、保证程序安全
- Java安全模型的核心就是Java沙箱(sandbox)
- 沙箱是一个限制程序运行的环境
- 通过这样的措施来保证对代码的有限隔离,防止对本地系统造成破坏
- 沙箱主要限制系统资源访问,CPU、内存、文件系统、网络
- 不同级别的沙箱对这些资源访问的限制也可以不一样
- 所有的Java程序运行都可以指定沙箱,可以定制安全策略
- Java中将执行程序分成本地代码和远程代码两种,本地代码默认视为可信任的,而远程代码则被看作是不受信的。对于授信的本地代码,可以访问一切本地资源
- 而对于非授信的远程代码在早期的Java实现中,安全依赖于沙箱(Sandbox)机制
-
JDK1.0中如此严格的安全机制也给程序的功能扩展带来障碍,比如当用户希望远程代码访问本地系统的文件时候,就无法实现
-
因此在后续的Java1.1版本中,针对安全机制做了改进,增加了安全策略。允许用户指定代码对本地资源的访问权限
- Java1.2版本中,再次改进了安全机制,增加了代码签名
- 不论本地代码或是远程代码,都会按照用户的安全策略设定,由类加载器加载到虚拟机中权限不同的运行空间,来实现差异化的代码执行权限控制
-
当前最新的安全机制实现,则引入了域(Domain)的概念
-
虚拟机会把所有代码加载到不同的系统域和应用域
-
系统域部分专门负责与关键资源进行交互
-
各个应用域部分则通过系统域的部分代理来对各种需要的资源进行访问
-
虚拟机中不同的受保护域(Protected Domain),对应不一样的权限(Permission)存在于不同域中的类文件就具有了当前域的全部权限
自定义类加载器
隔离加载类
在某些框架内进行中间件与应用的模块隔离,把类加载到不同的环境。比如:阿里内某容器框架通过自定义类加载器确保应用中依赖的jar包不会影响到中间件运行时使用的jar包。再比如:Tomcat这类Web应用服务器,内部自定义了好几种类加载器,用于隔离同一个Web应用服务器上的不同应用程序
修改类的加载方式
类的加载模型并非强制,除Bootstrap外,其他的加载并非一定要引入,或者根据实际情况在某个时间点进行按需进行动态加载
扩展加载源
比如从数据库、网络、甚至是电视机机顶盒进行加载
防止源码泄露
Java代码容易被编译和篡改,可以进行编译加密。那么类加载也需要自定义,还原加密的字节码
常见场景
-
实现类似进程内隔离,类加载器实际上用作不同的命名空间,以提供类似容器、模块化的效果,例如,
两个模块依赖于某个类库的不同版本
,如果分别被不同的容器加载,就可以互不干扰
,这个方面的集大成者是JavaEE和OSGI、JPMS等框架 -
应用需要从不同的数据源获取类定义信息,例如网络数据源,而不是本地文件系统。或者是需要自己操纵字节码,动态修改或者生成类型
-
一般情况下,使用不同的类加载器去加载不同的功能模块,会提高应用程序的安全性但是,如果涉及Java类型转换,则加载器反而容易产生不美好的事情
-
在做Java类型转换时,
只有两个类型都是由同一个加载器所加载,才能进行类型转换
,否则转换时会发生异常
实现方式
- Java提供了抽象类java.lang.ClassLoader,所有用户自定义的类加载器都应该继承ClassLoader类
- 重写
loadClass方法
、重写findclass方法
对比
- 两种方法本质上差不多,毕竟loadClass()也会调用findClass()
- 逻辑上讲我们最好不要直接修改loadClass()的内部逻辑
- 建议的做法是只在findClass()里重写自定义类的加载方法,根据参数指定类的名字,返回对应的Class对象的引用
- loadclass()这个方法是实现双亲委派模型逻辑的地方,擅自修改这个方法会导致模型被破坏,容易造成问题
-
最好是在双亲委派模型框架内进行小范围的改动,不破坏原有的稳定结构
-
避免了自己重写loadClass()方法的过程中必须写双亲委托的重复代码,从代码的复用性来看,不直接修改这个方法始终是比较好的选择
-
编写好自定义类加载器后,便可以在程序中调用loadClass()方法来实现类加载操作
-
自定义加载器的父类加载器是系统类加载器
-
JVM中的
所有类加载都会使用java.lang.ClassLoader.loadClass(String)方法
(自定义类加载器重写java.lang.ClassLoader.loadClass(String)方法的除外
),连JDK的核心类库也不能例外
JDK9变化
- 为了保证兼容性,JDK9没有从根本上改变
三层类加载器架构
和双亲委派模型
,但为了模块化系统的顺利运行,仍然发生了一些值得被注意的变动 - 扩展机制被移除,
扩展类加载器
由于向后兼容性的原因被保留,不过被重命名为平台类加载器
(platform class loader) - 可以通过classLoader的新方法getPlatformClassLoader()来获取
- JDK9时基于模块化进行构建(原来的rt.jar和tools.jar被拆分成数十个JMOD文件)
- 其中的Java类库就已天然地满足了可扩展的需求,那自然无须再保留
<JAVA_HOME>\lib\ext
目录 - 此前使用这个目录或者java.ext.dirs系统变量来
扩展JDK功能的机制已经没有继续存在的价值
平台类加载器、应用程序类加载器不再继承自java.net.URLClassLoader
,现在启动类加载器
、平台类加载器
、应用程序类加载器
全都继承于jdk.internal.loader.BuiltinClassLoader
- 如果有程序直接依赖了这种继承关系,或依赖了URLClassLoader类的特定方法,那代码
很可能会在JDK9及更高版本的JDK中崩溃
- 在Java9中,类加载器有了名称,该名称在构造方法中指定,可以通过getName()方法来获取
- 平台类加载器的名称是platform,应用类加载器的名称是app。类加载器的名称在调试与类加载器相关的问题时会非常有用
启动类加载器
现在是在jvm内部和java类库共同协作实现的类加载器
(以前是C++实现),但为了与之前代码兼容,在获取启动类加载器
的场景中仍然会返回null
,而不会得到BootClassLoader实例- 类加载的委派关系也发生了变动,当平台及应用程序类加载器收到类加载请求,在委派给父加载器加载前,要
先判断该类是否能够归属到某一个系统模块
中,如果可以找到这样的归属关系,就要优先委派给负责那个模块的加载器
完成加载