类加载器

  • 类加载器(ClassLoader)是JVM给应用程序实现类和接口字节码数据的技术
  • 本地接口JNI允许Java调用其他语言编写的方法, 在hotspot类加载器中, 主要用于调用JVM中使用CPP编写的方法

应用

  1. SPI机制
  2. 类的热部署
  3. Tomcat类的隔离
  4. 类的双亲委派机制(怎么打破双亲委派机制)
  5. 自定义类加载器
  6. 使用Arthas不停机解决线上故障

分类

  1. Java代码中实现的 or JDK默认提供 or 自定义的, 所有实现的类加载器都需要继承抽象类ClassLoader
  2. JVM底层源码实现的, 跟虚拟机实现语言一致, 比如Hotspot使用cpp实现
    • 作用是加载运行时的基础类, 保证运行中基础类可以被正确的加载, 比如java.lang.String, 确保可靠性

JDK8之前

  • JVM底层实现的类加载器, 启动类加载器Bootstrap, 加载Java最核心的类, 在jre/lib目录下的所有类文件, 比如rt.jar, tools.jar, resources.jar
  • 如果打印看到类加载器为null, 则就是启动类加载器. 因为为了安全性考虑, 不允许在代码中获得启动类加载器
  • 用户自己如果写了一些底层的jar包可以由启动类加载器加载, 有两种方式
    1. 放在jre/lib目录下(但是不推荐这么做)
    2. 使用-Xbootclasspath/a:<jar包目录>/<jar包名>进行扩展
  • Java代码中实现了扩展类加载器Extension, 允许扩展Java中比较通用的类
  • 应用程序类加载器Application, 加载应用使用的类, 比如由第三方jar包引入的类
    • 源码都位于sun.misc.Launcher中, 是一个静态内部类, 继承自URLClassLoader, 可以通过目录或指定jar包将字节码文件加载到内存中
      extends
    • 扩展类加载器Extension ClassLoader默认加载jre/lib/ext下的类文件
      1. 所以同样可以将自定义的jar包放在jre/lib/ext目录下(但是不推荐)
      2. 使用-Djava.ext.dirs=<jar包目录>进行扩展, 会覆盖原始目录
      3. windows上可以使用分号分隔, macos或者linux上使用冒号分隔, 再追加上原始目录
    • 应用程序类加载器Application加载classpath下的类文件, 项目中的类或者是第三方依赖中的类
  • Arthas中使用classloader命令可以查看
    • classloader -l 查看所有类加载器的哈希值
    • classloader -c <hash> 其中<hash>表示某一个类加载器的哈希值, 就可以把所有加载的jar包都打印出来
    • 可以看到扩展类加载器加载的都是扩展类加载器的jar包, 但是应用程序类加载器可以加载maven第三方jar包, 还加载了扩展类加载器, 以及启动类加载器加载的jar包的内容, 这部分是因为双亲委派机制

双亲委派机制

解决了一个类到底由谁来加载的问题

作用

  1. 保证类加载的安全性, 避免恶意代码替换JDK的核心类库, 确保核心类库的完整性安全性
  2. 避免重复加载

含义

  • 一个类加载器接收到加载类的任务时, 会自底向上查找是否加载过, 再自顶向下加载
  • 自顶向下依次是启动类加载器, 扩展类加载器, 应用程序类加载器
    • 先自底向上查找一个类是否被加载过
      • 如果已经加载过, 则这个类的加载任务就结束了
      • 如果没有加载过, 则会自顶向下逐个加载
    • 双亲委派机制就是向上查找和向下加载的过程
      • 比如一个定义的类A, 先由应用程序类加载器查找, 没有加载过则再由扩展类加载器查找, 没有加载过则再由启动类加载器进行查找, 如果启动类加载器加载过了, 则加载过程结束
      • 比如一个类B, 没有被三个类加载器加载, 则由启动类加载器加载, 查看加载路径, 发现不在启动类加载器的加载路径中, 则会由扩展类加载器尝试加载, 判断路径, 如果依然不在路径中, 最后由应用程序类加载器加载
    • 所以向上查找的过程避免了重复加载, 向下加载的过程起到了加载优先级的作用
  1. 如果一个类重复出现在三个类加载器的加载位置处, 则由谁来加载?
    • 根据双亲委派机制, 应该是由启动类加载器加载的, 因为他的优先级是最高的
  2. String类能否被覆盖? 如果自定义了一个java.lang.String类, 那么这个类是否会被加载
    • 启动类加载器在向上查找阶段已经会发现rt.jar包中的String类已经加载过了, 所以不会加载自定义的String类, 会返回已经加载过的String
  3. 如何使用代码主动加载一个类?(两种方法)
    • 使用Class.forName(), 使用当前类的类加载器去加载指定的类
    • 通过getClassLoader()获取到类加载器, 通过类加载器的loadClass()指定某个类加载器的加载, 这种方式更加明确一些, 比如:
      1
      2
      3
      4
      5
      6
      // 获取main方法所在类的类加载器, 应用程序类加载器
      ClassLoader cl = D.class.getClassLoader();
      System.out.println(cl);
      // 使用应用程序类加载器加载指定的类
      Class<?> clazz = cl.loadClass("类名");
      System.out.println(clazz.getClassLoader());
  4. 如果一个类无法被三个类加载器加载呢?
    • 会报类无法找到的错误
  • 每一个类加载器都有一个父类加载器, 在代码中都是parent, 类型是ClassLoader, 比如应用程序类加载器的parent=扩展类加载器, 但是扩展类加载器的parent=null, 这是因为启动类加载器是无法获得的, 所以是null
    • Arthas使用classloader -t查看父子关系

总结-类的双亲委派机制是什么

  1. 类加载器加载某个类的时候, 会自底向上向父类查找是否加载过, 如果加载过就直接返回, 如果一直到最顶层的类加载器都没有加载, 再由自顶向下进行加载
  2. 应用程序类加载器的父类加载器是扩展类加载器, 扩展类加载器的父类加载器是启动类加载器
  3. 双亲委派机制的好处有两点:
    • 避免恶意代码替换JDK中核心类, 比如java.lang.String, 确保安全性, 完整性
    • 避免重复加载类

打破双亲委派机制

  1. 自定义类加载器
  2. 线程上下文类加载器
  3. Osgi框架的类加载器

场景

  • 有两个Web应用, 都具有一个名为MyServlet的类, 只有名字相同, 内容不同
    • Tomcat想去加载这两个类, 这两个类都位于classpath上, 应用1的类可以正常加载, 但是应用2的类, Tomcat无法加载
    • 因为这个时候MyServlet类已经存在了, 这样只能加载一个类
    • 实际上Tomcat对每个应用都有一个类加载器, 这样就不会走双亲委派机制
    • 应用1和应用2都加载自己的类, 因此实现了使用自定义类加载器打破双亲委派机制, 实现应用类的隔离

自定义类加载器

  • ClassLoader中包含了四个核心方法, 双亲委派机制的核心代码在loadClass()
1
2
3
4
5
6
7
8
9
public Class<?> loadClass(String name) 
// 类加载的入口, 提供了双亲委派机制, 内部会调用findClass
protected Class<?> findClass(String name)
// 抽象类, 加载字节码二进制信息的核心方法, 由类加载器子类实现, 获取二进制数据调用defineClass
// 比如URLClassLoader会根据文件路径获取类文件中的二进制数据
protected final Class<?> defineClass (String name, byte[] b, int off, int len)
// 调用了虚拟机底层的方法, 做一些类名的校验, 将字节码信息加载到虚拟机内存当中
protected final void resolveClass(Class<?> c)
// 执行类生命周期的连接阶段
  • 双亲委派机制
1
2
3
4
5
6
7
8
9
// parent == null 说明父类加载器是启动类加载器, 直接调用findBootstrapClassOrNull
// 否则调用父类加载器的加载方法
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
// 父类加载器不行的话, 就自己加载
if (c == null) c = findClass(name);
  • 自定义的类加载器, 如果不手动指定父类加载器, 则默认指向应用程序类加载器
  • 两个自定义类加载器, 加载相同的限定名的类, 是否会冲突?
    • 不会冲突, 只有相同的类加载器+相同的类限定名才会被认为是同一个类, 在Arthas中使用sc -d 类名可以查看具体情况
  • 为了实现多种不同的渠道加载字节码的方式, 不应该打破双亲委派机制, 应该重写findClass方法

线程上下文类加载器

JDBC为例, 使用了DriverManager来管理项目中引入的不同数据库的驱动, 比如MySQL驱动, 或者Oracle驱动

  • DriverManager类位于rt.jar中, 所以由启动类加载器加载
  • 但是不同数据库的驱动是在用户的jar包中, 由应用程序类加载器加载, 因此违反了双亲委派机制
  • 所以在初始化DriverManager时, 通过SPI机制加载jar包中的MySQL驱动
  • SPI利用了线程上下文类加载器(应用程序类加载器)加载类并创建对象
    spi

DriverManager如何知道驱动类已经引入

SPI(Service Provider Interface)机制, 是JDK内置的服务提供发现机制, 类似于Spring的依赖注入

SPI原理

  1. ClassPath路径下的META-INF/services目录中, 以接口的全限定名来命名文件, 对应的文件里面写了接口的实现
  2. 使用ServiceLoader类加载实现类, ServiceLoader<Driver> lD = ServiceLoader.load(Driver.class);获取Driver对象

SPI如何获取应用程序类加载器

使用了线程上下文中保存的类加载器进行类的加载, 一般是应用程序类加载器

1
2
3
4
public static <S> ServiceLoader<S> load(Class<S> service){
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
  • 在《深入理解Java虚拟机》一书中认为JDBC打破了双亲委派机制, 因为由启动类加载器加载的类, 委派应用程序类加载器去加载类

  • 也有一种说法, 认为JDBC没有打破双亲委派机制, 因为单独的启动类加载器在DriverManager加载完了之后, 通过初始化阶段触发了驱动类的加载, 依然遵循的是双亲委派机制. 因为驱动类加载器自底向上查找后, 再自顶向下进行判断, 发现启动类加载器和扩展类加载器都无法加载, 所以由应用程序类加载器进行加载, 这个过程也没有打破双亲委派机制. 这两个类加载器都没有重写loadClass(), 打破双亲委派机制的唯一方法就是重写loadClass()

OSGI模块化

JDK9之后有另一种模块化方法, 因此不再使用了. OSGI也使用了类加载器实现了热部署的功能, 即在服务不停止的情况下, 动态更新字节码文件到内存中. 现在可以使用Arthas实现热部署, 本质上还是类加载器

  • Arthas不停机解决线上问题
  1. 出问题服务器上部署一个Arthas
  2. jad --source-only <类全限定名> > <dir/文件名.java> jad命令反编译, 用其他编译器, 比如vim来修改源代码
  3. mc -c <类加载器的hash> <dir/文件名.java> -d <输出目录> mc命令用来编译修改过的代码
    • 这里的类加载器的hash可以使用sc -d <对应的类名>来查看
  4. retransform class文件所在的<目录/xxx.class>retransform命令加载新的字节码

注意事项:

  1. 如果程序重启, 则字节码文件会恢复, 因为只是通过retransform这个命令将字节码文件放在了内存中. 因此要将字节码文件放在jar包中进行更新
  2. 使用retransform不能添加方法或者字段, 也不能更新正在执行中的方法

JDK9之后

引入了module概念

  1. 启动类加载器由Java编写了, 不再由CPP编写, 位于jdk.internal.loader.ClassLoaders类中
    • Java中的BootClassLoader继承自BuiltinClassLoader实现从模块中找到要加载的字节码资源文件
    • 但是启动类加载器依然无法通过Java代码获取, 这是为了保证统一, 所以返回的内容依然是null
      891
  2. 扩展类加载器被替换成了平台类加载器(Platform ClassLoader)
    • 平台类加载器遵循模块化方式加载字节码文件, 所以继承关系从URLClassLoader变成了BuiltinClassLoader, 实现了从模块中加载字节码文件
    • 平台类加载器的存在更多是为了与老版本兼容, 自身没啥特殊的逻辑. 如果使用了模块化的思想, 平台类加载器基本上就不需要了
      892

总结

  1. 类加载器的作用
    • 类加载器(CLassLoader)负责在类加载的过程中的字节码获取并加载到内存, 通过加载字节码数据放入内存转换成byte[], 接下来调用虚拟机底层方法将byte[]转换成方法区和堆中的数据
  2. 有几种类加载器
    • 启动类加载器Bootstrap 加载核心类
    • 扩展类加载器Extension 加载扩展类
    • 应用程序类加载器Application 加载应用classpath中的类
    • 自定义类加载器, 重写findClass方法
    • JDK9之后从扩展类加载器变成了平台类加载器
      cl
  3. 什么是双亲委派机制
    • 每个Java实现的类加载器中保存了一个成员变量叫做父 类加载器, 自底向上查找是否加载过, 再自顶向下加载, 避免核心类被应用程序重写覆盖的问题, 提升安全性
  4. 如何打破双亲委派机制
    • 重写loadClass方法
    • JNDI, JDBC, JCE, JBI等框架使用了SPI + 线程上下文类加载器
    • OSGI实现了一整套类加载逻辑, 允许同级类加载器之间互相调用