4.1 概述
类加载器是JVM执行类加载机制的前提
ClassLoader是Java的核心组件,所有的Class都是由ClassLoader进行加载的,ClassLoader负责通过各种方式将Class信息上的二进制数据流读入JVM内部,转换为一个与目标类对应的java.lang.Class对象实例。然后交给Java虚拟机进行链接、初始化操作。因此,ClassLoader在整个装载阶段,只能影响到类的加载,而无法通过ClassLoader去改变类的链接和初始化行为。至于他是否可以运行,则由Execution Engine决定
4.1.1 大场面试题
- 深入分析ClassLoader,双亲委派机制及使用原因
- 类加载器的双亲委派模型是什么
- 有哪些类加载器,这些类加载器都加载哪些文件
- Class的forName(“java.lang.String”)和Class的getClassLoader()的loadClass(“java.lang.String”)有什么区别
- 什么是双亲委派模型
- 类加载器有哪些
- 什么是类加载器,都有哪些
- 双亲委派机制可以打破吗,为什么
4.1.2 类加载的分类
类的加载分类:显式加载 vs 隐式加载
class文件的显式加载与隐式加载的方式是指JVM加载class文件到内存的方式
- 显式加载指的是在代码中通过ClassLoader加载class对象,如直接使用Class.forName()或this.getClass().getClassLoader().loadClass()加载class对象
- 隐式加载不直接在代码中调用ClassLoader的方法加载class对象,而是通过虚拟机自动加载到内存中,如在加载某个类的class文件时,该类的class文件中引用了另外一个类的对象,此时额外引用的类将通过JVM自动加载到内存中
4.1.3 类加载器的必要性
- 在开发中遇到java.lang.ClassNotFoundException异常或java.lang.NoClassDefFoundError异常时,只有了解类加载器的加载机制才能在出现异常时快速根据错误异常日志定位问题和解决问题
- 需要支持类的动态加载或需要对编译后的字节码文件进行加密解密操作时,需要和类加载器打交道
- 开发人员可以在程序中编写自定义类加载器来重新定义类的加载规则,以便实现一些自定义的处理逻辑
4.1.4 命名空间
类的唯一性
对于任意一个类,都需要由加载他的类加载器和这个类本身一同确认其在Java虚拟机中毒的唯一性。每一个类加载器,都拥有一个独立的类名称空间:比较两个类是否相等,只有在这两个类是由同一个类加载器的前提下才有意义。否则,即使这两个类源自同一个class文件,被同一个虚拟机加载,只有加载他们的类加载器不同,那这两个类就必定不相等
命名空间
- 每个类加载器都有自己的命名空间,命名空间由该加载器及其所有的父加载器所加载的类组成
- 在同一命名空间中,不会出现类的完整名字相同的两个类
- 在不同的命名空间中,可能会出现类的完整名字相同的两个类
- 在大型应用中,借助这一特性,来运行同一类的不同版本
4.1.5 类加载机制的基本特性
- 双亲委派机制。但不是所有类都遵守这个模型,有的时候,启动类加载器所加载的类型,是可能要加载用户代码的,比如JDK内部的ServiceProvider/ServiceLoader机制,用户可能在标准API框架上,提供自己的实现,JDK也需要提供些默认的参考实现。例如,Java中JNDI、JDBC、文件系统、Cipher等很多方面,都是利用的这种机制,这种情况就不会用双亲委派机制模型去加载,而是利用所谓的上下文加载器
- 可见性,子类加载器可以访问父加载器加载的类型,反过来是不允许的。不然,因为缺少必要的隔离,我们就没有办法利用类加载器去实现容器的逻辑
- 单一性,由于父加载器的类型对于子加载器是可见的,所以父加载器中加载过的类型,就不会在子加载器中重复加载
4.2 复习:类的加载器分类
从概念上讲,自定义类加载器一般指的是程序中由开发人员自定义的一类类加载器,但Java虚拟机贵伐却没有这么定义,而是将所有派生于抽象类ClassLoader的类加载器都划分为自定义加载器
4.2.1 启动/引导类加载器(Bootstrap ClassLoader)
- 这个类加载使用C/C++实现的,嵌套在JVM内部
- 用来加载Java的核心库(JAVA_HOME/jre/lib/rt.jar或sun.boot.class.path路径下的)。用于JVM自身需要的类
- 并不继承自java.lang.ClassLoader,没有父加载器
- 出于安全考虑,只加载包名为java、javax、sun等开头的类
- 加载扩展类和应用程序类加载器,并指定为他们的父类加载器
- 使用-XX:+TraceClassLoading参数得到
4.2.2 扩展类加载器(Extension ClassLoader)
- Java编写,由sun.misc.Launcher$ExtClassLoader实现
- 继承于ClassLoader类
- 父类加载器为启动类加载器
- 从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/lib/ext子目录下加载类库。如果用户创建的JAR放在此目录下,也会自动由扩展类加载器加载
4.2.3 系统/应用程序类加载器(AppClassLoader)
- java编写,由sum.misc.Launcher$AppClassLoader实现
- 继承于ClassLoader类
- 父类加载器为扩展类加载器
- 负责加载环境变量classpath或系统属性java.class.path指定路径下的类库
- 应用程序中的类加载器默认是系统类加载器
- 是用户自定义类加载器的默认父加载器
- 通过ClassLoader的getSystemClassLoader()方法可以获取到该类加载器
4.2.4 用户自定义类加载器
- 在Java的日常应用程序开发中,类的加载几乎是由上述3种类加载器相互配合执行的。必要时,我们可以自定义类加载器,来定制类的加载方式
- Java开发中可以自定义类加载器实现类库的动态加载,加载源可以是本地的Jar包,也可以是网络上的远程资源
- 通过类加载器可以实现非常绝妙的插件机制。如著名的OSGI组件框架,再如Eclipse的插件机制。类加载器为应用程序提供了一种动态增加新功能的机制,这种机制无需重新打包发布应用程序就能实现
- 自定义类加载器能够实现应用隔离,如Tomcat、Spring等中间件和组件框架都再内部实现了自定义的加载器,并通过自定义加载器隔离不同的组件模块。这种机制比C/C++要好太多
- 自定义类加载器通常需要继承于ClassLoader
4.3 测试不同的类加载器
每个Class对象都会包含一个定义它的ClassLoader的一个引用
获取ClassLoader的途径
获取当前类的ClassLoader clazz.getClassLoader()
获取当前线程上下文的ClassLoader Thread.currentThread().getContextClassLoader()
获得系统的ClassLoader ClassLoader.getSystemClassLoader()
说明:
- 引导类加载器与另外两种类加载器并不是同一个层次意义上的加载器,前者是使用C++编写而成,而另外两种是Java编写。由于引导类加载器压根不是Java类,因此只能打印出null
- 数组类的Class对象,不是由类加载器去创建的,而是再Java运行期JVM根据需要自动创建的。对于数组类的类加载器来说,是通过Class.getClassLoader()返回的,与数组当中元素类型的类加载器是一样的;如果数组当中的元素类型是基本数据类型,数组类是没有类加载器的
String[] strArr = new String[6];
System.out.println(strArr.getClass().getClassLoader());
//运行结果:null
ClassLoaderTest[] test = new ClassLoaderTest[1];
System.out.println(test.getClass().getClassLoader());
//运行结果:sun.misc.Launcher$AppClassLoader@18b4aac2
int[] inst = new int[2];
System.out.println(inst.getClass().getClassLoader());
//运行结果:null
4.4 ClassLoader源码解析
ClassLoader与现有类加载器的关系:
除了以上虚拟机自带的加载器外,用户还可以定制自己的类加载器。Java提供了抽象类java.lang.ClassLoader,所有用户自定义的类加载器都应该继承ClassLoader类
4.4.1 ClassLoder的主要方法
内部没有抽象方法
- public final ClassLoader getParent()
返回该类加载器的超类加载器
- public Class<?> loadClass(String name) throws ClassNotFoundException
加载名称为name的类,返回结果为java.lang.Class类的实例。如果找不到类,则返回异常。该方法中的逻辑就是双亲委派模式的实现
- protected Class<?> findClass(String name) throws ClassNotFoundException
查找二进制名称为name的类,返回结果为java.lang.Class类的实例。这是一个受保护的方法,JVM鼓励我们重写此方法,需要自定义加载器遵循双亲委托机制,该方法会在检查完父类加载器之后被loadClass()方法调用
- protected final Class<?> defineClass(String name,byte[] b,int off,int len)
根据给定的字节数组b转换为Class的实例,off和len参数表示实际Class信息在byte数组中的位置和长度,其中byte数组b是ClassLoader从外部获取的。这是受保护的方法,只在自定义ClassLoader子类中可以使用
- protected final void resolveClass(Class<?> c)
链接指定的一个Java类,使用该方法可以使用类的Class对象创建完成的同时也被解析。链接阶段主要是字节码进行验证,为类变量分配内存并设置初始值同时将字节码文件中的符号引用转换为直接引用
- protected final Class<?> findLoadedClass(String name)
查找名称为name的已经被加载过的类,返回结果为java.lang.Class类的实例。这个方法是final方法,无法被修改
- private final ClassLoader parent
也是一个ClassLoader的实例,这个字段所表示的ClassLoader也成为这个ClassLoader的双亲,在类加载的过程中,ClassLoader可能会将某些请求交予自己的双亲处理
4.4.2 SecureClassLoader与URLClassLoader
- SecureClassLoader扩展了ClassLoader,新增了几个与使用相关的代码源和权限定义类验证,一般不会和这个类打交道
- ClassLoader是一个抽象类,很多方法是空的没有实现,如findClass()、findResource(),URLClassLoader实现了这些方法,并新增了URLClassPath类协助取得Class字节码流等功能。在编写自定义类加载器时,如果没有过于复杂的需求,可以直接继承URLClassLoder类,可以避免自己去编写findClass()方法及其获取字节码流的方式
4.4.3 ExtClassLoader与AppClassLoader
- 两个类都继承自URLClassLoader,是sun.misc.Launcher的静态内部类。sun.misc.Launcher主要被系统用于启动主应用程序
- ExtClassLoader并没有重新loadClass()方法,说明遵循双亲委派,而AppClassLoader重载了loadClass()方法,但最终还是调用父类super.loadClass(),依然遵循双亲委派
4.4.4 ClassforName()与ClassLoader.loadClass()
- Class.forName()是一个静态方法,最常用的是Class.forName(String className);根据传入的类的全限定名返回一个Class独享,该方法在将Class文件加载到内存的同时,会执行类的初始化
- ClassLoader.loadClass(),是一个实例方法,需要一个ClassLoader对象来调用该方法,该方法将Class文件加载到内存时,不会执行类的初始化,直到这个类第一次使用时才进行初始化
4.5 双亲委派模型
4.5.1 定义与本质
- 类加载器用来把类加载到Java虚拟机中。从JDK1.2版本开始,类的加载过程采用双亲委派机制,这种机制能更好地保证Java平台的安全
- 如果一个类加载器在接到加载类的请求时,他首先不会自己尝试去加载这个类,而是把这个请求任务委托给父类加载器去完成,以此递归。如果父类加载器可以完成类加载任务,就成功返回。只有父类加载器无法完成此加载任务时,才自己去加载
4.5.2 优势与劣势
优势
- 双亲委派机制在java.lang.ClassLoader.loadClass(String,boolean)接口中体现
- 避免了类的重复加载,确保一个类的全局唯一性
- 保护程序安全,防止核心API被随意篡改
劣势
- 检查类是否加载的委托过程是单向的,虽然从结构上比较清晰,使各个ClassLoader的职责非常明确,但顶层的ClassLoader无法访问底层的ClassLoader所加载的类
- Tomcat中,类加载器采用的加载机制和传统的双亲委派机制有区别。首先自行加载,当自己加载失败,才会将给他的超类加载器去执行,也是Servlet推荐的一种做法
4.5.3 破坏双亲委派机制
破坏双亲委派机制1
由于双亲委派模型在JDK1.2才被引入,但类加载器和抽象类ClassLoader在JDK1.0就已存在,面对已经存在的用户自定义类加载器的代码,Java设计者为了兼容已有代码,无法再以技术手段避免loadClass()被子类覆盖的可能性,只能在1.2后的java.lang.ClassLoader中添加一个新的protected方法findClass(),并引导用户去重写这个方法,而不是在loadClass()中编写代码
破坏双亲委派机制2:线程上下文类加载器
- 越基础的类由越上层的加载器进行加载,如果有基础类型又要调回用户代码,怎么办呢?Java引入了不太优雅的设计:线程上下文加载器(Thread Context ClassLoader)。这个类加载器可以通过java.lang.Thread类的setContextClassLoader()方法进行设置,如果创建线程时还未设置,他将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器
- 这是一种父类加载器去请求子类加载器完成类加载器的行为,实际上大同了双亲委派模型的层次结构来逆向使用类加载器,已违背了双亲委派模型的一般性原则,也是无可奈何的事情,Java中设计SPI的加载基本都采用这种方式完成,JNDI、JDBC、JCE、JAXB、JBI
破坏双亲委派机制3
用户对程序动态性的追求而导致,如代码热替换(Hot Swap)、模块热部署(Hot Deployment)等
IBM主导的OSGi实现模块化热部署的关键是他自定义的类加载器机制的实现。在OSGi环境下,不再双亲委派模型推荐的树状结构,而是更为复杂的网状结构
当收到类加载请求时,OSGi按下面的顺序进行类搜索:
- 将以java.*开头的类,委派给父类加载器加载
- 否则,将委派列表名单内的类,委派给父类加载器加载
- 否则,将Import列表中的类,委派给Export这个类的Bundle的类加载器加载
- 否则,查找当前Bundle的ClassPath,使用自己的类加载器加载
- 否则,查找类是否在自己的Fragment Bundle中,如果在,则委派给Fragment Bundle的类加载器加载
- 否则,查找Dynamic Import列表的Bundle,委派给对应Bundle的类加载器加载
- 否则,类查找失败
4.5.4 热替换的实现
- 热替换是指在程序的运行过程中,不停止服务,只通过替换程序文件来修改程序的行为。热替换的关键需求在于服务不能中断,修改必须立即表现正在运行的系统之中
- Java天生不支持热替换,如果一个类已经加载到系统中,通过修改类文件,并无法让系统再来加载并重新定义这个类。实现这一功能的可行方法就是灵活运用ClassLoder
4.6 沙箱安全机制
- 保护程序安全,保护Java原生的JDK代码
- 沙箱(sandbox)是一个限制程序运行的环境
- 将Java代码限定再JVM特定的运行范围中,并且严格限制代码对本地系统资源访问
- 沙箱主要限制系统资源访问,CPU、内存、文件系统、网络。不同级别的沙箱对这些资源访问的限制也可以不一样
- 所有的Java程序运行都可以指定沙箱,可以定制安全策略
4.6.1 JDK1.0时期
在Java中将执行程序分为本地代码和远程代码两种,本地代码默认视为可信任的,可以访问一切本地资源,而远程代码被看作不受信任的,安全依赖于沙箱
4.6.2 JDK1.1时期
针对安全机制做了改进,增加了安全策略,允许用户指定代码对本地资源的访问权限
4.6.3 JDK1.2时期
增加了代码签名。不论本地代码还是远程代码,都会按照用户的安全策略设定,由类加载器加载到虚拟机中权限不同的运行空间,来实现差异化的代码执行控制权限
4.6.4 JDK1.6时期
当前最新的安全机制实现,引入了域(Domain)的概念
虚拟机会把所有代码加载到不同的系统域和应用域。系统域部分专门负责与关键资源进行交互,而各个应用域部分则通过系统域的部分代理来对各种需要的资源进行访问。虚拟机中不同的受保护域(Protected Domain),对应不一样的权限(Permission)。存在于不同域中的类文件就具有了当前域的全部权限
4.7 自定义类的加载器
为什么要自定义类加载器
- 隔离加载类,在某些框架进行中间件与应用的模块隔离,把类加载到不同的环境。如:Tomcat,内部自定义了好几种类加载器,用于隔离同一个Web应用服务器上的不同应用程序
- 修改类加载的方式,类的加载模型并非强制,除Bootstrp外,其他的加载并非一定要引入,或根据实际情况在某个时间段进行按需进行动态加载
- 扩展加载源,比如从数据库、网络进行加载
- 防止源码泄漏,可以进行编译加密,那么类加载也需要自定义,还原加密的字节码
常见场景
- 实现类似进程内隔离,类加载器实际上用作不同的命名空间,以提供类似容器、模块化的效果。例如,两个模块依赖于某个类库的不同版本,如果分别被不同的容器加载,就可以互不干扰。Java EE、OSGi、JPMS等框架
- 应用需要从不同的数据源获取类定义信息,例如网络数据源,而不是本地文件系统。或需要自己操作字节码,动态修改或生成类型
4.7.1 实现方式
Java提供了抽象类java.lang.ClassLoader,所有用户自定义的类加载器都应该继承ClassLoader类
- 方式一:重写loadClass()方法
- 方式二:重写findClass()方法–>推荐
对比
这两种方法本质上差不多,loadClass()也调用findClass(),建议在findClass()里重写,根据参数指定类的名字,返回对应的Class对象的引用
当编写好自定义类加载器后,便可以在程序中调用loadClass()方法来实现类加载操作
4.8 Java9新特性
为了保证兼容性,JDK9没有从根本上改变三层类加载器架构和双亲委派模型,但为了模块化系统的顺利运行,仍发生了一些值得注意的改动
- 扩展机制被移除,扩展类加载器由于向后兼容性的原因被保留,不过被命名为平台类加载器(PlatformClassLoader)。可以通过ClassLoader的getPlaformClassLoader()方法获取
- 平台类加载器和应用程序类加载器都不再继承自java.net.URLClassLoader
- 在Java9中,类加载器有了名称。该名称在构造方法中指定,可以通过getName()方法来获取。平台类加载器的名称是platform,应用类加载器的名称是app
- 启动类加载器现在是在JVM和java类库实现的类加载器,以前是C++,为了与之前兼容,在获取启动类加载器仍然返回null
- 启动类加载器负责加载的模块
java.base | java.security.sasl | java.datatransfer |
---|---|---|
java.xml | java.desktop | jdk.httpserver |
java.instrument | jdk.internal.vm.ci | java.logging |
jdk.management | java.management | jdk.management.agent |
java.management.rmi | jdk.naming.rmi | java.naming |
jdk.net | java.prefs | jdk.sctp |
java.rmi | jdk.unsupported |
- 平台类加载器负责加载的模块
java.activation* | jdk.accessibility | java.compiler* |
---|---|---|
jdk.charsets | java.corba* | jdk.crypto.cryptoki |
java.scripting | jdk.crypto.ec | java.se |
jdk.dynalink | java.se.se | jdk.incubator.httpclient |
java.security.jgss | jdk.internal.vm.compiler* | java.smartcardio |
jdk.jsobject | java.sql | jdk.localedata |
java.transaction* | jdk.scripting.nashorn | java.xml.bind* |
jdk.security.auth | java.xml.crypto | jdk.security.jgss |
java.xml.ws* | jdk.xml.dom | java.xml.ws.annotation* |
jdk.zipfs |
- 应用程序类加载器负责加载的模块
jdk.aot | jdk.jdeps | jdk.attach |
---|---|---|
jdk.jdi | jdk.compiler | jdk.jdwp.agent |
jdk.editpad | jdk.jlink | jdk.hotspot.agent |
jdk.jshell | jdk.internal.ed | jdk.jstatd |