前言
我们知道类装载过程分为加载,连接,初始化,三个阶段,这次主要来了解一下工作在加载阶段的ClassLoader,它主要作用是从系统外部获取Class二进制数据流。所有的Class都是由ClassLoader进行加载的,ClassLoader负责通过各种方式将Class信息的二进制数据流读入系统,它在整个装载过程中,只能影响类的加载,无法对连接与初始化进行改变。
ClassLoader分类及双亲委派模式
在标准的Java程序中,Java虚拟机会创建3类ClassLoader,它们分别是BootStrap ClassLoader(启动类加载器),Extension ClassLoader(扩展类加载器),App ClassLoader(应用类加载器,也称为系统类加载器)。此外,每一个应用程序还可以拥有自定义的ClassLoader,以扩展虚拟机获取Class数据的能力。
它们的层次结构如上图,从下往上级别越高,当系统需要一个类时,优先从最上层开始加载,这样就防止了Java源码中的类被替换加载。
这些ClassLoader中,启动类加载器最特别,它完全由C语言实现,并且在Java中没有对象与其对应,系统的核心类都是由启动类加载器加载,它也是虚拟机的核心组件。扩展类加载器和应用类加载器都有对应的Java对象可用。
注:此处曾经有同事问过一个问题,如果自定义一个包名是java.lang,在这个包下面创建一个String类,那么这个类会欺骗虚拟机,突破沙箱隔离由启动类加载器加载吗?还有,如果定义一个java.lang.Xxx可以访问java.lang包下的类型和包可见的成员吗?
第一个问题,答案是不可以的。在双亲委派模式下,启动类加载器总是最优先加载系统类,如果我们的代码中定义了一个java.lang.String类,在加载时会发现java.lang.String类的Class文件在Java API中已经存在而不能成功加载,它只能使用双亲返回的类,即启动加载器已经加载的String类。
第二个问题,同样不行,如同以前一样,加载器加载时一路向上委派给启动类加载器,虽然这个启动类加载器负责加载核心Java API的class文件,包括名为java.lang的包,但它无法在java.lang中找到名为Xxx的成员,所以这个类最终会由应用类加载器或自定义加载器加载。Java虚拟机中,同一个包下彼此访问的权限仅限于由同一个类加载器加载到同一个包中的类型,由于Java API是由启动类加载器加载的,而java.lang.Xxx是由应用类加载器加载的,不属于同一个运行时包,因此Xxx就不能访问Java API包中的类型和包内可见的成员。
(运行时包的概念是Java虚拟机第二版规范中出现的,它指由同一个类加载器加载、属于同一个包的、多个类型的集合)
双亲委派模式的弊端
虚拟机检测类是否已经加载的过程是单向的,这种方式虽然从结构上说比较清晰,但是也会带来一个问题,上层的ClassLoader无法访问下层ClassLoader加载的类。
通常情况下启动类加载器加载系统核心类,包括一些重要的系统接口,如果这类接口在应用中实现,还绑定一个工厂方法,用于创建该接口的实例,而接口和工厂方法都在启动类加载器中,这时会出现工厂类无法加载应用中实现了接口的实例,有这种问题的组件不少,比如JDBC和Xml Parser。
双亲模式的类加载方式是虚拟机默认的行为,但是我们并非必须这么做,通过重载ClassLoader可以修改该行为,实际上Tomcat和OSGi框架都修改了,有各自独特的类加载顺序。