上文说了类加载过程的 5 个阶段, 着重介绍了各个阶段做的工作. 在本文中, 我们对执行加载阶段的主体进行探讨, 学习类加载器的模型和逻辑, 以及我们该如何自定义一个类加载器.
定义
前面说过加载阶段是一个可以让设计人员高度自控的模块, 因为类文件的源头可以是多种多样的, 代码生成, 反射生成或从网络中生成等. 因此类加载器作为对这些文件的处理就显得尤为重要.
但类加载器的功能不仅如此, 其还有一个重要的功能就是和一个类的全限定名唯一确定一个类. 通俗来说, 要说两个类是相同的, 不仅其全限定名要一样, 其对应的类加载器也必须相同, 才能说明两个类是相等的.
正因为类加载器的功能角色如此重要, 因此虚拟机对其的实现规范也十分重视. 在 Java 虚拟机中, 对其的实现模型是双亲委派模型.
模型
双亲委派模型的主要执行过程示意图如上所示, 其分为启动类加载器(Bootstrap Class-loader), 拓展类加载器(Extension Class-loader), 应用程序类加载(Application Class-loader).
其中启动类加载器主要负责加载 JRE 的核心类库, 如 JRE 目录下的 rt.jar. 但其实根据《深入分析 Java web 技术内幕》上所说, 启动类加载器并不严格符合双亲委派模型, 因为 Bootstrap Class-loader 并不属于 JVM 的类等级层次. Bootstrap Class-loader 是没有子类的, Extension Class-loader 也是没有父类的. 不过在这里我们并不深究, 只要知道有这一点就可以了.
Extension Class-loader 主要负责加载 JRE 拓展目录 ext 下的类.
Application Class-loader 主要负责用户类路径 (Class-path) 下的类, 这个类加载器是使用的最多的, 因为大大多数情况下, 一般开发者并没有实现自定义的类加载器, 那么 JVM 就会使用这个来加载类大部分类.
执行过程
上图就是双亲委派模型的执行过程, 当类开始加载的时候, 先检查是否已经被加载过, 如果没有被加载过, 则调用父类的加载方法, 如果父类加载失败, 抛出异常, 则调用自身的 findClass() 方法进行加载.
JDK 中加载过程的源码分析:
- protected Class<?> loadClass(String name,boolean resolve) throws ClassNotFoundException
- {
- synchronized (getClassLoadingLock(name)) {
- // First,check if the class has already been loaded
- // 如果加载过了, 就不要加载直接返回
- Class c = findLoadedClass(name);
- if (c == null) {
- long t0 = System.nanoTime();
- try {
- // 判断是否有父加载器
- if (parent != null) {
- // 有父加载器则调用父加载器加载
- c = parent.loadClass(name, false);
- } else {
- // 无父亲就调用 bootstarp 加载器来加载
- c = findBootstrapClassOrNull(name);
- }
- } catch (ClassNotFoundException e) {
- // ClassNotFoundException thrown if class not found
- // from the non-null parent class loader
- }
- // 父加载器和 bootstarp 加载器都没有找到指定类, 调用当前类的 findClass() 来完成类加载
- // 因此, 自定义类加载器, 就是重写 findClass() 方法
- if (c == null) {
- // If still not found, then invoke findClass in order
- // to find the class.
- long t1 = System.nanoTime();
- c = findClass(name);
- // this is the defining class loader; record the stats
- sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
- sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
- sun.misc.PerfCounter.getFindClasses().increment();
- }
- }
- if (resolve) {
- resolveClass(c);
- }
- return c;
- }
- }
从源码中, 我们可以看到其实符合规范要求的 双亲委派模型 的. 而当我们要自定义一个类加载器的时候就是通过重写 findClass() 来实现的.
自定义类加载器
- /**
- * 1. 自定义类加载器通过集成 ClassLoader 来实现, 主要通过重写 findClass 方法
- * 2. findClass 方法首先通过自定义的 loadByte()方法将 Class 文件转换成 byte[]字节流
- * 3. 然后通过 defineClass()方法将其转换为 Class 对象
- */
- public class SelfClassLoader extends ClassLoader {
- private String classPath;
- public SelfClassLoader(String classPath) {
- this.classPath = classPath;
- }
- /**
- * 通过 difineClass, 将一个字节数组转换为 Class 对象
- * @param name
- * @return
- * @throws ClassNotFoundException
- */
- @Override
- protected Class<?> findClass(String name) throws ClassNotFoundException {
- try {
- byte[] data = loadByte(name);
- return defineClass(name, data, 0, data.length);
- } catch (Exception e) {
- e.printStackTrace();
- throw new ClassNotFoundException();
- }
- }
- /**
- * 根据路径将指定的文件读取为 byte 流
- * @param name
- * @return
- * @throws IOException
- */
- private byte[] loadByte(String name) throws IOException {
- name = name.replaceAll("\\.", "/");
- FileInputStream fis = new FileInputStream(classPath + "/" + name
- + ".class");
- int len = fis.available();
- byte[] data = new byte[len];
- fis.read(data);
- fis.close();
- return data;
- }
- }
另一个种实现自定义类加载器的方法:
- /**
- * 1. 加载指定 packageName 下的类
- * 2. 用自定义类加载器进行加载, 如果加载失败, 再交给父加载器进行加载
- */
- public class UrlSelfClassloader extends URLClassLoader {
- private String packageName = "";
- public UrlSelfClassloader(URL[] urls, ClassLoader parent) {
- super(urls, parent);
- }
- @Override
- protected Class<?> findClass(String name) throws ClassNotFoundException {
- Class<?> aClass = findLoadedClass(name);
- if (Objects.nonNull(aClass)){
- return aClass;
- }
- if (!packageName.startsWith(name)){
- return super.loadClass(name);
- }else {
- return findClass(name);
- }
- }
- }
如何使用自定义的类加载器
- public static void main(String args[]) throws Exception {
- MyClassLoader classLoader = new MyClassLoader("");
- Class clazz = classLoader.loadClass("");
- Object obj = clazz.newInstance();
- Method helloMethod = clazz.getDeclaredMethod("hello", null);
- helloMethod.invoke(obj, null);
- }
总结
在本文中, 我们讲解了类加载器的实现模型, 分析了在 JDK 中类加载器的源码实现, 并根据源码中的代码实现, 自定义了一个类加载器的实现.
此外相信经过五和六两篇文章的学习, 大家应该对如何将类加载入虚拟机中有了系统的理解.
后面的文章中, 我们就要进入 JVM 的内部了, 从下篇文章开始, 我们就开始逐步讲解 JVM 的内存布局, 了解 JVM 中的各个逻辑上划分的存储结构以及其作用, 欢迎各位读者浏览.
文章在公众号「iceWang」第一手更新, 有兴趣的朋友可以关注公众号, 第一时间看到笔者分享的各项知识点, 谢谢! 笔芯!
本系列文章主要借鉴《深入分析 Java Web 技术内幕》和《深入理解 Java 虚拟机 - JVM 高级特性与最佳实践》.
来源: https://www.cnblogs.com/JRookie/p/11100926.html