概述

技术的尽头是面试,本篇文章主要针对类加载器ClassLoader相关的问题做了一个整理汇总,下面的面试题,你会几道?

常见面试题

谈谈类加载器再整个类加载器过程中起到的作用

类加载器系列(四)——类加载器相关面试题整理 - 图1
整个类加载的过程包括加载、连接、以及初始化阶段。其中类加载器是在类的加载阶段起作用。
类加载器ClassLoader将class文件的二进制数据读入到内存中,将其放在运行时数据区中的方法区内,然后在堆上创建一个java.lang.Class对象,指向方法区,借用这个对象可以去访问方法区中这个类的各种数据。
类加载器获ClassLoader获取class的来源不单单是classpath下编译后的.class文件,也可以是网络中的二进制流、数据库中存储的数据等,只要是符合类规范的二进制都可以通过类加载器加载到JVM中。
类加载器加载二级制class文件后,交由后面阶段进行连接、初始化。

说说JDK中内置的类加载器以及他们的职责

类加载器有不同的种类,不同的类加载器承担了不同的职责,jdk中内置了3个类加载器,分别如下:
类加载器系列(四)——类加载器相关面试题整理 - 图2

  1. BootStrap ClassLoader(启动类加载器):

最顶层的类加载器, 它是由C/C++实现的,它没有父类加载器。主要负责加载jvm的核心库(JAVAHOME/jre/lib/rt.jar或sun.boot.class.path路径下的内容),比如我们场景的String, Integer等类。

  1. Extension ClassLoader(扩展类加载器):

继承于ClassLoader类, 由sun.misc.Launcher$ExtClassLoader实现。它的父类加载器为启动类加载器。它主要负责加载JDK的安装目录jre/lib/ext下的jar, 同时也可以加载系统属性java.ext.dirs所指定的目录下的jar。

  1. Application ClassLoader(应用类加载器):

它的父类加载器为扩展类加载器,主要负责加载境变量classpath和系统属性java.class.path 指定路径下的类库,比如我们在idea工程中写的java类。
以上这些是在jdk8中适用,在jdk9中由于模块化,发生了一些变化,比如去掉了扩展类加载器,但是为了保持3层的结构,做了兼容。

谈谈你对类加载器的双亲委派模型的认识

所谓双亲委派模式就是说当类加载器收到加载类或资源的请求时,首先判断之前是否加载过该class, 如果加载过直接返回。如果没有加载。就会委托给父类加载器加载去加载器,层层递归,一直到顶层父类加载器,启动类加载器,如果父类加载器也找不到,则自己去加载,最终自己也没有加载到会抛出ClassNotFoundException
打个比方,我们系统默认的类加载器是应用类加载器,我们首次加载一个自己写的类User.class,它会委托给扩展类加载器加载,扩展类加载器还有父类加载器启动类加载器(BootStrapClassLoader), 启动类加载器没有父类加载器,它会尝试加载User.class, 发现加载不到,就会回到扩展类加载器,扩展类加载尝试去加载,发现也加载不到,就还给应用类加载器加载,应用类加载器可以将User.class加载到JVM中。
整个类加载流程的如下图:
类加载器系列(四)——类加载器相关面试题整理 - 图3

为什么要采用双亲委派模型

双亲委派模型在我看来有两个个好处:

  1. 通过这种方式,可以避免类的重复加载。当父加载器已经加载过某一个类时,子加载器就不会重新加载这个类。
  2. 出于安全性的考虑。比如我们的核心类库rt.jar是由启动类加载器(BootstrapClassLoader)加载的,采用双亲委派模型,可以避免有人自定义一个有破坏功能的java.lang.Integer被加载。这样可以有效的防止核心Java API被篡改。

    所有的类加载都是用的双亲委派模型吗?

    不是的,双亲委派模型并不是万能的。
    根据双亲委派模型,顶层的ClassLoader是无法访问底层ClassLoader加载的类。这样就会造成一个问题,比如JDBC、JNDI这种SPI的接口是在核心类库中定义,由启动类加载器加载,但是他们的实现往往是由第三方jar实现,如果按照双亲委派模型就无法加载到。那怎么解决?通过引入线程上下文类加载器,线程上下类加载器默认是应用类加载器,在核心类中通过传入线程上下文类加载器,去加载三方的jar。
    又比如tomcat这种web容器中,会部署多个应用程序,不同的程序要隔离类,也就是A应用加载的类,B应用也能加载。为了实现隔离性,优先加载 Web 应用自己定义的类,所以没有遵照双亲委派的约定,每一个应用自己的类加载器——WebAppClassLoader负责加载本身的目录下的class文件,加载不到时再交给CommonClassLoader加载,这和双亲委派刚好相反。
    所以,可以通过自定义类加载器的方式满足实际需求。

    你有没有自定义过类加载器?

    在我所经历的项目中没有自定义过类加载器。
    比如你想要扩展类的加载源,比如从数据库、或者网络中加载类,这时候需要自定义一个类加载器,或者为了防止源码泄露,对编译的类文件进行了加密,我们需要通过自定义类加载器进行解密还原等。上面就是需要自定义类加载器的一些场景,但是我的项目中没有这样的场景存在。
    一般自定义类加载器,是继承ClassLoader这个接口,通过重写loadClass()方法或者findClass()方法实现自定义。如果想打破双亲委派模型,那么就重写整个loadClass方法,loadClass()中封装了双亲委派模型的核心逻辑,但是大部分情况下,们建议的做法还是重写findClass()自定义类的加载方法,根据指定的类名,返回对应的Class对象的引用。因为任意打破双亲委派模型可能容易带来问题,我们重写findClass()是在双亲委派模型的框架下进行小范围的改动。