Java — 举例浅谈类加载器和双亲委派机制机制

Java — 举例浅谈类加载器和双亲委派机制机制

1. 双亲委派机制

首先,说一下双亲委派机制的定义:java在进行未加载过的类加载的过程中,首先会向上进行委派,在顶层依次往下决定是否进行加载,最后将类的class文件加载到虚拟器中。当然了Java的类加载是一个复杂的过程,这里多个classloader实际影响的是读取二进制流的动作和范围,具体后面会说明。

// 缺少图例

图中详细介绍了双亲委派机制的过程
以HotSpot虚拟机为例说,Java 自带的有三个类加载器,分别是BootstrapClassLoader,ExtClassLoader,AppClassLoader(注,限JDK1.8,在jdk11中已经发生了变化),其中BootstrapClassLoader非常神秘,它是C++ Native实现的,对java不可见,负责加载jre/目录下的核心库。其他的ClassLoader,都是由它加载出来的,并且保持了上述的加载规则,另外提一点,ExtClassLoader和AppClassLoader并非父子关系,而是同样继承于URLClassLoader的“兄弟”,一起看下ClassLoader中对LoadClass的定义:

// 缺少图例

可以看出其实这里和上面的图对应是同样的意思,总结一下双亲委派机制是作用于Java类加载过程的一种机制,且实际差异在于loadClass|findClass中。接下来再带着两个问题回顾一下:

1.不同的类加载器,是否只有读取二进制流的动作和范围不同,后续的动作是否一样。

其实是一样的,可以看到在ClassLoader这个类中,相关的resolveClass和defineClass都是由final修饰的方法,这也说明了无法影响到将二进制流变成.class加载到内存中的这个过程。

2.遇到限定名相同的类,是否会出现重复加载而导致混乱。

答案是通常不会,因为很明显的是在loadClass中可以看出,当一个类被加载后,它就不会被再次加载,而是直接通过findLoadedClass进行寻找,java中越核心的类库越被上层的类加载器加载,这样的方式被动地避免了混乱。但是如果开发者进行了骚操作,自定义ClassLoader并重写loadClass强行加载类,破坏了双亲委派机制,那么内存中可能会出现一个限定名对应多个不同类的情况,在这种情况下,由于不同ClassLoader有不同命名空间,这些类还是会被认为是不同的类。

2. 双亲委派机制的破坏–原因

看完上面的讲解,不知你是否有一个疑惑,既然java设定并推荐使用双亲委派机制来管理类的加载过程,那为什么loadClass方法不直接改为final呢,这样的话就可以避免混乱的出现,严格遵循这样的委派机制就万无一失了呀。这样的思考非常好,如果定义成final也确实可以如所想的解决问题,但由于Java使用类加载器加载代码的历史久远,可双亲委派机制是jdk1.2引入的特性,此时世界上已经存在许多类似于重写loadClass加载类的代码逻辑了,只能保持这种模式设定loadClass为普通方法,这样默默接受这种“自立门户”来保证新的jdk向下兼容。这被称为第一次破坏双亲委派。

对此,补救措施是引入了新的findClass方法,并推荐使用findClass而不是loadClass来保证双亲委派机制正常工作。

3. 双亲委派机制的破坏–后续

那么看完上面的破坏过程,既然有第一次,那当然有第二次破坏了。其实第二次破坏是java SPI发展的过程,关于SPI大家可以自行搜索,笔者对此也是一知半解。

这里使用JDBC来举例,JDBC也是SPI的一种,是用于数据库相关操作的服务接口。那么数据库的种类很多,JDK不可能能够根据所有不同数据库的实现细节来实现所有种类数据库的操作方法。合理的方式就是JDK提供一种类似DriverManager的一组接口规范,各数据库厂商根据接口自行实现驱动细节,提供服务。在这种情况下,对JDK接口的类的加载是通过上层的类加载器,而各第三方类库则需要通过下层类加载器进行加载,这样由上层指派任务给下层的情况和双亲委派机制自下而上的过程不符合,上层类加载器需要“放下身段”直接请求下层类加载器加载对应类库,因此称为第二次破坏双亲委派机制。

写在最后:

大家可以思考一下,如果自己写一个限定名为java.lang.String的类,能否在程序中进行使用?