一、概述
ClassLoader是java核心组件,所有Class都是有ClassLoader进行加载的,ClassLoader负责通过各种方式将Class信息的二进制流数据读入JVM内部,转换为一个目标类对应的java.lang.Class对象实例。然后交给JVM进行链接、初始化等操作。因此,ClassLoader在整个装载阶段,只能影响到类的加载,而无法通过ClassLoader去改变链接、初始化的行为。至于他是否可以执行,则由Execution Engine(执行引擎)决定。
类的加载方式:显示加载VS隐式加载
- 显示加载是指在代码中通过调用ClassLoader加载Class对象,如直接使用Class.forName()或者this.getClass().getClassLoader().loadClass()加载Class对象
- 隐式加载则是不在代码中调用ClassLoader的方法加载Class对象,而是通过虚拟机自动记载到内存中,如在加载某个类的class文件时,该类的class文件中引用了另一个类的对象,JVM会自动把该类加载到内存中
两个类是否相等,需要考虑到是否用一个类加载器进行加载的,如果同一个class文件由不同的ClassLoader加载到内存中,就是不同的类。在大型应用中,往往借助这一特性,来运行同一个类的不同版本。
类加载机制的基本特性
- 双亲委派模型,但不是所有类加载都遵守这个模型,启动类所加载的类,可能需要加载用户代码的,比如JDK内部的ServiceProvider/ServiceLoader机制,用户可以在标准的API框架上,提供自己的实现
- 可见性,子类加载器可以访问父加载器加载的类型,但是反过来是不允许的。
- 单一性,父加载器类型对于子机载器是可见的,所以父加载器加载过的类型,就不会在子加载器中重复加载。但是注意,加载器邻居间是可以多次加载的
二、类的加载器的分类
1、引导类加载器(Bootstrap ClassLoader)
- 这个类加载器使用C/C++语言实现的,嵌套在JVM内部
- 它用来加载Java核心库(JAVA_HOME/jre/lib/rt.jar 或 sun.boot.class.path路径下的内容)。用于提供JVM自身需要的类
- 并不继承java.lang.ClassLoader,没有父加载器
- 处于安全考虑,Bootstrap启动类加载器值加载包名为java、javax、sun开头的类
- 扩展类加载器和应用程序加载器,并指定他们的父类加载器
- 使用-XX:+TraceClassLoading参数查看类加载情况
2、扩展类加载器(Extension ClassLoader)
- 由java语言编写,sun.misc.Launcher$ExtClassLoader
- 继承于ClassLoader类
- 父类加载器为启动类加载器
- 从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/lib/ext子目录下加载类库,如果用户创建jra放在此目录下,也会由扩展类加载器加载
3、系统类加载器
- java语言编写,sun.smisc.Launcher$AppClassLoader
- 继承于ClassLoader类
- 父类加载器为扩展类加载器
- 负责加载环境变量classpath或系统属性java.class.path指定路径下的类库
- 引用程序中的类加载器默认是系统类加载器
- 是用户自定义类加载器的默认父加载器
- 通过ClassLoader的getSystemClassLoader()方法获取获取到该类加载器
4、用户自定义加载器
- 在Java的日常引用程序开发中,类加载器几乎由上述三种类加器相互配合执行的。在必要的时候,开发人员可以自定义类加载器,来制定类的加载方法
- 体现java语言强大生命力和魅力的关键因素之一,便是java开发者可以自定义类加载器来实现类库的动态加载,可以是本地的jar,也可以是网络上的资源
- 通过类加载器可以实现非常绝妙的插件机制。类加载器为应用程序提供类一种动态增加新功能的机制。
- 自定义类加载器能够实现应用隔离
- 自定义类加载通过需要继承于ClassLoader
三、测试不同的类加载器
四、ClassLoader源码解析
ClassLoader继承关系:
ClassLoad中的方法:
- loadClass(String):根据类全路径加载类,返回类的Class对象,双亲委派
- resolveClass(Class<?>)
- findClass(String):查找二进制名称为name的类,返回这个类的java.lang.Class的实例
- defineClass(byte[],inti,int) :根据给定的二进制数据,返回这个类的java.lang.Class的实例
- getParent():获取父类加载器(不是继承关系,而是一个属性关系)
Class.forName()与ClassLoader.loadClass()的区别:
- Class.forName():是一个静态方法,根据传递类的全限定名返回一个Class对象。该方法在将Class加载到内存的同时,会进行类的初始化
- ClassLoader.loaderClass():是一个实例化方法,需要一个ClassLoader对象调用。该方法将Class加载到内存的同时,不会进行类的初始化,直到类第一次被使用
五、双亲委派模型
1、定于与本质
如果一个类加载器在请求加载时,它首先不会自己尝试加载这个类,而是把这个请求先委托给父类加载器去完成,以此递归,如果父类加载器可以完成类加载的任务,就成功返回。只有父类加载器无法完成加载任务,才自己去加载。这样保证了平台的安全。
引导类加载器加载->扩展类加载器加载->系统类加载器加载
2、优势与劣势
在java.lang.ClassLoader.loadClass(String, Boolean)接口中体现。
- 现在当前类加载器缓存中查询目标类,有直接返回
- 判断当前类加载器的父类加载器是否为空,如果不为空,则调用parent.loadClass()进行加载
- 反之则调用findBootstrapClassOrNull()接口让引导类加载器进行加载
- 通过上述都没有加载完成,则调用findClass()进行记载,最终会使用java.lang,ClassLoader.defineClass加载此类
优势:
- 避免类的俯冲加载,确保类的全局唯一性
- 保护程序安全,防止核心API被随意篡改
劣势
- 顶层的ClassLoader无法访问底层的ClassLoader加载的类
3、如何破坏
- JDK1.2才出现双亲委派机制,所以在JDK1.2之前不支持,所以定义JDK1.2之前的ClassLoader没有刷新委派
- 线程上下文类加载器(Thread Context ClassLoader)
- 代码热替换、模块热部署(OSGi),每个模块都有自己的类加载器,发展成网状结构。
4、热替换实现
不关闭程序下,直接替换修改的程序,并且立即表现在正在运行的系统之中。
六、沙箱安全机制
- 保护程序安全
- 保护java原生JDK代码
- 限定程序在JVM中特定的运行范围之中,并且严格限制本地代码对本地系统资源的访问
1、JDK1.0
2、JDK1.1
3、JDK1.2
3、JDK1.6
七、自定义类的加载器
1、为什么自定义类的加载器
- 隔离加载类,在某些框架内进行中间件与应用模块之间的隔离,把类加载到不同的环境中。如TOMCAT自定义了几种类加载器,把同一Web服务器上的不同应用进行隔离
- 修改类加载的方式,除Bootstrap加载器之外,其它加载器不用强制引用
- 扩展加载资源
- 防止源码泄漏,可以对代码编译进行加密,那么类加载器就要解密还原字节码
2、如何实现
1、实现方式
- java提供了抽象类java.lang.ClassLoader,所有用户自定义的类加杂气都应该继承ClassLoader类
- 在自定义ClassLoader的子类时候,我们常见的有2种做法:
- 重写loadClass()方法
- 重写findClass()方法
上面两种方法本质上相差不多,毕竟loadClass也会调用findClass,但是逻辑上我们最好不要修改loadClass方法,因为loadClass中实现了双亲委派机制。
八、java9的新特性
上述几个章节都是在jdk8下讲解。下面说一下jdk8变化。
1、扩展类加载器修改名称为平台类加载器:Platform ClassLoader,JDK9将rt.jar和tool.jar拆分成多个JMOD文件,可以使用ClassLoader.getPlatformClassLoader()获取
2、平台类加载器和系统类加载器都不在继承与 URLClassLoader,而是BuiltinClassLoader
3、在java 中,类加载器有了名称,该名称可以在构造方法中指定,通过getName()来获取。
4、启动加载器也在JDM内部有所体现:BootClassLoader,但是为了与之前兼容,获取还是返回null
5、类加载器委派关系也发生变化,当平台和系统类加载器收到加载器请求的时候,在委派父加载器前,要先判断该类是否能够归属到某个系统模块中,如果可以找到这样的归属关系,就要有限委派给负责那个模块的类加载器进行加载(java9 因为模块化,所以每个模块有自己的类加载器)