站在Java虚拟机的角度来看,只存在两种不同的类加载器:一种是启动类加载器(Bootstrap ClassLoader),这个类加载器使用 C++ 语言实现,是虚拟机自身的一部分;另外一种就是其他所有的类加载器,这些类加载器都由 Java 语言实现,独立存在于虚拟机外部,并且全都继承自抽象类 java.lang.ClassLoader

站在Java开发人员的角度来看,类加载器就应当划分得更细致一些。自JDK 1.2以来,Java一直保持着三层类加载器双亲委派的类加载架构。

启动类加载器 Bootstrap Class Loader

1.这个类加载使用 C++ 语言实现的,嵌套在JVM内部
2.它用来加载java的核心库JAVA_HOME/jre/lib/rt.jar、resources.jar或sun.boot.class.path路径下的内容),用于提供JVM自身需要的类
3.并不继承自java.lang.ClassLoader没有父加载器
4.加载拓展类和应用程序类加载器,并指定为他们的父加载器,即 ClassLoader
5.出于安全考虑,BootStrap 启动类加载器只加载包名为 java、javax、sun等开头的类

  • 启动类加载器也能加载被-Xbootclasspath参数所指定的路径中存放的,而且是Java虚拟机能够识别的(按照文件名识别,如 rt.jar、tools.jar,名字不符合的类库即使放在 lib 目录中也不会被加载)
  • 启动类加载器无法被 Java 程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给引导类加载器去处理,那直接使用 null 代替即可

    扩展类加载器 Extension Class Loader

    1.java语言编写 ,由sun.misc.Launcher$ExtClassLoader实现。
    2.派生于 ClassLoader 类
    3.从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/lib/ext子目录(扩展目录)下加载类库。如果用户创建的 JAR 放在此目录下,也会由拓展类加载器自动加载

    应用程序类加载器 Application Class Loader

    由于应用程序类加载器是ClassLoader类中的getSystem-ClassLoader()方法的返回值,所以有些场合中也称它为“系统类加载器”。

1.java语言编写, 由sun.misc.Launcher$AppClassLoader实现。
2.派生于 ClassLoader 类
3.它负责加载环境变量 ClassPath 或系统属性java.class.path指定路径下的类库
4.该类加载器是程序中默认的类加载器,一般来说,Java 应用的类都是由它来完成加载
5.通过ClassLoader#getSystemClassLoader()方法可以获取到该类加载器

用户自定义加载器

User defined classLoader
1.隔离加载类
2.修改类加载的方式
3.拓展加载源
4.防止源码泄漏

ClassLoader

ClassLoader类,它是一个抽象类,其后所有的类加载器都继承自ClassLoader(不包括启动类加载器)
类加载器 - 图1
类加载器 - 图2

双亲委派模型

JVM 对class文件采取的是按需加载的方式,只有当需要使用该类的时候才把它的 class 文件加载到内存中生成 class 对象。而加载某个类的 class 文件时,JVM采用的是双亲委派模型,即把请求交给父类处理,是一种任务委派模式。

image.png
双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应有自己的父类加载器。不过这里类加载器之间的父子关系一般不是以继承(Inheritance)的关系来实现的,而是通常使用组合(Composition)关系来复用父加载器的代码。

工作流程

  1. 如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,把这个请求委派给父类加载器去完成;
  2. 每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中;
  3. 只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载。

    实现

    java.lang.ClassLoaderloadClass()方法实现了双亲委派模型:

  4. 检查请求的类是否已经被加载了(防止类被重复加载),如果没被加载, findLoadedClass(name) 返回null

  5. 10-14行:判断类是否有父类,如果没有父类非空,则委托父类加载类;如果父类为空,那就就直接请求委派给引导类加载器去处理
  6. 15行:如果非空父类加载器返回 ClassNotFoundException,说明父类无法完成加载请求,在此处捕获异常但不进行处理
  7. 20行及以后:父类都无法加载,只能调用本身的 findClass 方法加载

(findClass最好自己重写)

  1. protected Class<?> loadClass(String name, boolean resolve)
  2. throws ClassNotFoundException
  3. {
  4. synchronized (getClassLoadingLock(name)) {
  5. // First, check if the class has already been loaded
  6. Class<?> c = findLoadedClass(name);
  7. if (c == null) {
  8. long t0 = System.nanoTime();
  9. try {
  10. if (parent != null) {
  11. c = parent.loadClass(name, false);
  12. } else {
  13. c = findBootstrapClassOrNull(name);
  14. }
  15. } catch (ClassNotFoundException e) {
  16. // ClassNotFoundException thrown if class not found
  17. // from the non-null parent class loader
  18. }
  19. if (c == null) {
  20. // If still not found, then invoke findClass in order
  21. // to find the class.
  22. long t1 = System.nanoTime();
  23. c = findClass(name);
  24. // this is the defining class loader; record the stats
  25. sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
  26. sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
  27. sun.misc.PerfCounter.getFindClasses().increment();
  28. }
  29. }
  30. if (resolve) {
  31. resolveClass(c);
  32. }
  33. return c;
  34. }
  35. }

优势

  1. 避免类重复加载

通过这种双亲委派模型的层级关系可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。

  1. 保护程序安全,防止核心api被篡改

假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。可能你会想,如果我们在classpath路径下自定义一个名为java.lang.SingleInterge类呢?该类并不存在java.lang中,经过双亲委托模式,传递到启动类加载器中,由于父类加载器路径下并没有该类,所以不会加载,将反向委托给子类加载器加载,最终会通过系统类加载器加载该类。但是这样做是不允许,因为java.lang核心API包,需要访问权限,强制加载将会报出如下异常

  1. java.lang.SecurityException: Prohibited package name: java.lang

破坏双亲委派模型

JNDI

JNDI现在已经是Java的标准服务,它的代码由启动类加载器来完成加载(在JDK 1.3时加入到rt.jar的),肯定属于Java中很基础的类型了。但JNDI存在的目的就是对资源进行查找和集中管理,它需要调用由其他厂商实现并部署在应用程序的ClassPath下的JNDI服务提供者接口(Service Provider Interface,SPI)的代码,现在问题来了,启动类加载器是绝不可能认识、加载这些代码的,那该怎么办?

JNDI服务使用这个线程上下文类加载器去加载所需的SPI服务代码,这是一种父类加载器去请求子类加载器完成类加载的行为,这种行为实际上是打通了双亲委派模型的层次结构来逆向使用类加载器,已经违背了双亲委派模型的一般性原则,但也是无可奈何的事情。

示意图

启动类加载器加载rt.jar包中的SPI核心类,而 SPI 核心类需要 jdbc.jar 包内的 SPI 接口实现类,因此启动类加载器反向委托线程上下文类加载器(Thread Context ClassLoader),请求它去加载jdbc.jar,把其中的SPI接口实现类加载。

线程上下文类加载器可以通过java.lang.Thread类的setContext-ClassLoader()方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器
破坏双亲委托模型.png

OSGi

OSGi实现模块化热部署的关键是它自定义的类加载器机制的实现,每一个程序模块(OSGi中称为Bundle)都有一个自己的类加载器,当需要更换一个Bundle时,就把Bundle连同类加载器一起换掉以实现代码的热替换

OSGi中对类加载器的运用是值得学习的,完全弄懂了OSGi的实现,就算是掌握了类加载。

判断类是否一致

在 JVM 中表示两个 Class 对象是否为同一个类存在的两个必要条件:

  • 类的完整类名必须一致,包括包名
  • 加载这个类的 ClassLoader(指ClassLoader实例对象)必须相同

    对类加载器的引用

    JVM 必须知道一个类型是有启动类加载器加载的还是由用户类加载器加载的。如果一个类型由用户类加载器加载的,那么 JVM 会将这个类加载器的一个引用作为类型信息的一部分保存在方法区中。当解析一个类型到另一个类型的引用的时候,JVM需要保证两个类型的加载器是相同的。

    参考

  • JVM_02 类加载子系统