什么是类加载器?

你有没有想过我们在代码里用到的String、HashMap、Math等等类是从哪来的,你可能会说从java.util、java.lang等包或说看过下jdk源码包结构的可以说从rt.jar包里来的,或则从另外一个角度来说是从JVM的方法区中来的,因为方法区的类数据在同一个JVM进程中共享。
这些说法都没错,但没有结合起来想一下,jvm为什么有这些类?难道一开始就有吗?jvm启动过程发生了什么?这就引出一个关键词:类加载器,从名字来看,这个就是用来加载类的,我们先看下jdk中,类加载器的一些方法:
image.png

  • getResource:获取类数据来源(.class文件),我们大部分类都是从jar包中获取,jar(包括war等变种)也是java世界中最常用的类存储结构。但并不是类数据来源只有jar,此外还有网络流、运行时生成(动态代理)、JSP甚至数据库中都可以。
  • loadClass:执行加载类过程,就是上述问题的答案,将类数据放到jvm方法区,具体过程就是我们经常背的加载(读.class文件数据到内存) -> 验证(验证文件是否符合java语法树) -> 准备(准备内存空间,如果是final变量直接赋值) -> 解析(符号引用替换为直接引用) -> 初始化(给类静态变量赋值)。
  • getParent:指向父加载器,这就说明加载器有一个链式结构,作用详见下述的双亲委派机制

    类加载器的分层?

    我们都知道,不考虑自定义类加载器的话,jvm自带的类加载分三层,从高到底分别是:

  • BootstrapClassLoader :加载jdk核心类,包括rt.jar下的String、Thread、HashMap等。

  • ExtClassLoader:加载jdk扩展类,比如sun安全包下相关类如DESCrypt、ECKeyFactory等。
  • AppClassLoader:加载用户路径下的类,也就是我们日常自己开发的那些类,包括maven依赖的那些jar包里的类,都属于用户路径下的类。

我们可以试着去搜一下这几个类,可以发现第一个BootstrapClassLoader 是搜不到的,后面两个可以,分别是:
image.png
image.png
image.png
首先来看为什么BootstrapClassLoader搜不到,因为它是在jvm最早的加载器(鸡生蛋问题),它是由C++实现的。它在加载rt包时,除了jdk的一些核心类,rt下还有一个关键类:Launcher。
image.png
可以看到在Launcher的构造器里,先构造了ExtClassLoader,然后在ExtClassLoader的构造器中,去加载了jdk的扩展包下的类。此后,Launcher中又构造了AppClassLoader,并把ExtClassLoader传入设置为其父加载器,显然在AppClassLoader中去加载了用户classpath下的所有类。至此就完成了三层类加载器的串联,也完成了所有类的加载。
额外,我们注意到一点就是ExtClassLoader和AppClassLoader都是继承自URLClassLoader,这一层的抽象就是为了处理上面提到的getResource的工作,说明class文件数据是通过URL方式(URL不要直接想象成http的url,别忘了URL本名是统一资源定位符,通过文件系统+包名能够唯一定位到一个class资源文件,所以就是URL)。在URLClassLoader这层还有一个处理,就是实现了上面提到的findClass方法:
image.png
可以看到最后返回的result一定非空,因为根据双亲委派,如果继续返回null,会导致被子类加载影响类加载安全性,所以如果为空就直接抛CNFE了。也就是如果我们启动应用出现CNFE,要么就是不存在(这种一般情况会在IDE就会告诉你),要么就是类路径冲突了(这个由于是运行时才能确定,也就是以先加载的类为准,后续同名类直接丢弃,如果存在同jar包多个版本的话,如果一些类用到的了没加载到的版本就会报错)。

什么是双亲委派?

我们可以看到类加载器中有个getParent方法,明显就说明前向指针的链式结构,他的作用是什么呢?直接看代码:
image.png
可以看到,在自己findClass之前,会不断递归上溯到自己的父加载器,直到为null,也就是BootStrapClassLoader(类加载器的层级见下节),为null之后就会调findBootstrapClassOrNull,去判断该类在启动过程中加载的类集合中是否存在,如果存在就不会再加载了,如果找不到就会调findClass自己去加载。这就是双亲委派机制。
那为什么需要双亲委派呢?为了jdk核心类的安全,防止核心jdk类被篡改。想象一下,你自己写一个java.lang.String类,如果没有双亲委派,你的类就会被加载导致用户使用过程中被混用。有了这个机制,就能保证你的类不会被加载。

为什么打破双亲委派机制?

现在我们知道了双亲委派的实现逻辑及其原因,再来看打破双亲委派机制其实没那么高深,想两个问题就可以:

  • 怎么打破?
  • 为什么打破?

第一个问题,双亲委派的实现逻辑就在loadClass方法中,那么自定义一个类加载器重载掉这个方法,并去掉这段委派逻辑即可。
第二个问题,一般有两个常见的场景打破这个机制:

  • tomcat:因为一个tomcat容器可能同时运行多个项目,多项目可能都有一样报名和类名的类,但功能不一样,比如引入不同的版本的三方类库这种问题就太多了,比如说项目一引用了1.0版本的某框架,项目二引用了同一框架的2.0版本,那么某类被加载一次就不会再被加载两个项目其实用到的都是同一版本的某类,这样肯定就错错了 所以Tomcat必须打破原机制,也就是重写loadClass实现自己的一套隔离版的类加载机制。
  • jdbc:java作为生产级语言,肯定要和数据库连接,所以jdk抽象了一套jdbc的相关的包(java.sql),其中包括驱动管理(DriverManager)类,而在驱动接口上,因为不同厂商有不同厂商的实现,所以jdbc采用SPI方式留出一个口子(java.sql.Driver)让各个厂商去实现。由于驱动管理(DriverManager)是rt包中的类,也就是由BootstrapClassLoader加载,而厂商的Driver实现类属于用户的classpath下(mysql-connector-java.jar),理论上由AppClassLoader加载,但为了DriverManager能完成全量加载不至于是个半成品,在DriverManager中强行优先将Driver实现类提前加载,方式就是通过上面Launcher初始化过程中植入的ContextClassLoader(即AppClassLoader),也就是为了这个功能,让AppClassLoader提前出场,把这个类加载掉。破坏了双亲委派的顺序。类加载器的双亲委派及打破场景 - 图8image.png