前言

在我们日常开发中,应用遇到过 java.lang.ClassNotFoundException 这个异常,追溯的话就需要谈一谈类加载器。

类加载的过程

类加载包括3个阶段:加载链接初始化,其中链接又包括验证、准备和解析。

1、加载:查找定义类的二进制字节流,将其加载到虚拟中的方法区(类中静态存储结构转成方法区运行时数据结构),并生成一个Class对象供访问。这是 ClassLoader做的事,也叫类加载器。
2、验证:确保Class文件中的字节流包含的信息符合虚拟机规范,不会危害其安全。
3、准备:为类变量分配内存并设置类变量的初始值(初始值不是=号后面的值,而是如int是0,boolean是false),类变量的内存在方法区中配置。
4、解析:虚拟机将常量池内的符号引用替换成直接引用。

符号引用:虚拟机规范中的任何形式的字面量,在Class文件中的如CONSTANT_Class_info等
直接引用:对内存中的内容的一个指向,可以是指针、相对偏移量或间接定位到目标的句柄。

5、初始化:执行类构造器 <init>方法,完成类变量的赋值,和类静态代码块的执行。

补充下,类的生命周期:除了类加载的3个阶段,还有 使用和卸载两个阶段。

类加载器分类

上面提到的类加载的过程中,“加载”这个阶段是有类加载器完成的,这个类加载器是的设计是一项创新,允许用户自定义来决定类从哪里加载。

类加载器一共包括了:启动类加载器、扩展类加载器、应用类加载器。同时可以实现自定义类加载器,如下结构:

1、启动类加载器
启动类加载器主要负责加载JDK内部类,通常是rt.jar包和$JAVA_HOME/jre/lib目录下的jar(如java.*开头),包括其它2个类加载器。
除了特定的目录,也可以指定环境变量-Xbootclasspath来使用启动类加载器加载。

启动程序类加载器是Java虚拟机的一部分,用本机代码编写(比如,HotSpot使用C++),不同的平台的实现可能有所不同。

2、扩展类加载器
扩展类加载器是sun.misc.Launcher$ExtClassLoader实现的,继承于URLClassLoader,URLClassLoader又间接继承于CLassLoader。
它主要负责加载JDK\jre\lib\ext目录中,或者由java.ext.dirs系统变量指定的路径中的所有类库(如javax.*开头的类)。

3、应用类加载器
应用类加载器是sun.misc.Launcher$AppClassLoader实现的,同样继承于URLClassLoader。该加载器是ClassLoader类中getSystemClassLoader()方法返回的加载器。
它主要负责加载用户类路径(ClassPath)上所指定的类库或-Djava.class.path指定的类。

有个点需要注意:启动类不能被java应用直接引用,它是本地语言实现的,没有对象之类的获取,如果通过ExtClassLoader的parent获取的为null。

示例:

@Test
public void testClassLoad() {
    // 系统类加载器(应用程序类加载器):sun.misc.Launcher$AppClassLoader@18b4aac2
    System.out.println(ClassLoadTest.class.getClassLoader());

    // 扩展类加载器:sun.misc.Launcher$ExtClassLoader@50040f0c
    System.out.println(Logger.class.getClassLoader());

    // 启动类加载器:null(显示null)
    System.out.println(ArrayList.class.getClassLoader());
    
    // sun.misc.Launcher$AppClassLoader@18b4aac2
    System.out.println(ClassLoader.getSystemClassLoader());
}

类加载机制

上面图中展示的类加载器的关系图,叫做类的“双亲委派”模型。
除了最顶层的启动类加载器,都有自己的父类加载器。这里面所谓的父类并不是父子,而是以一种组合的方式实现的。可以看到ClassLoader抽象类有个ClassLoader parent的成员变量。

所谓双亲委派模型意思是:
1、当应用程序类加载器(AppClassLoader)加载类时,会交由ExtClassLoader进行加载;
2、扩展类加载器ExtClassLoader不会自己加载,会交由BootstrapClassLoader进行加载;
3、如果BootStrapClassLoader加载失败(例如在$JAVA_HOME/jre/lib里未查找到该class),会使用ExtClassLoader来尝试加载;
4、如果ExtClassLoader加载失败,会使用AppClassLoader进行加载,如果没找到对应的class,则会抛出ClassNotFoundException

如果有自定义加载器是一样的,会先委派其父类加载器,一层层直到启动类加载器,都找不到,再有自己进行加载。

实现双亲委派的代码:

// java.lang.ClassLoader
// resolve参数是否进行类解析,默认不解析
protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        // 首先,检测该类是不是已经当前类加载器加载到虚拟机中,它是一个native方法。
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                // 如果没有被加载,且存在父类加载器,则使用父类加载器进行加载
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
                	// 如果没有parent,则到顶层启动类加载器了,进行查询
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // ClassNotFoundException thrown if class not found
                // from the non-null parent class loader
            }

            if (c == null) {
                // findClass是个关键方法
                // 如果父类加载器和启动类加载器都加载不到,则通过对应类加载器从ClassPath中找到对应的.class
                long t1 = System.nanoTime();
                c = findClass(name);
            }
        }
        // 如果需要解析,再解析
        if (resolve) {
            resolveClass(c);
        }
        // 返回查找到的Class对象
        return c;
    }
}

注意
1、loadClass方法最后使用的是findClass方法,通过ClassPath查找,AppClassLoader和ExtClassLoader都是继承了URLClassLoader的findClass方法。由此也可以知道,在自定义类加载器时,建议通过实现该方法完成类的查找加载,以便loadClass破坏了双亲委派的模型。
2、不同的ClassLoader加载的相同的字节码类,并不是相等的,Class对象不等。因为不同的类加载器会指定不同的名称空间。

双亲委派的优势
1、防止系统中出现多份相同的字节码,例如Object类,不管哪个类加载器加载,都是同一个。
2、保证java程序安全稳定运行。

自定义类加载器

自定义类加载器,意思是我们程序自定义类加载器来加载Class类,使用的场景如本地磁盘的Class类或者网络中的文件。
实现自定义的类加载器需要以下几步:
1、继承java.lang.ClassLoader类;
2、重写其findClass方法;
3、在方法中,找到Class文件再调用defineClass方法将其加载到虚拟机中,获得Class对象;

实现自定义类加载器,不建议重写其loadClass方法,以免破坏掉双亲委派模型。

演示步骤:
1、首先再本地磁盘中创建一个测试类:Test.java

// pageage + 类名,组成加载的名称

package top.xudj;
public class Test {
    public void say(){
      	System.out.println("Say Hello");
    }
}

2、定义CustomClassLoader类,继承ClassLoader

public class CustomClassLoader extends ClassLoader {
  private String path;

  @Override
  protected Class<?> findClass(String name) throws ClassNotFoundException {
      String fileName = getFileName(name);
      File file = new File(path, fileName);

      FileInputStream inputStream = null;
      ByteArrayOutputStream bos = null;
      try {
          inputStream = new FileInputStream(file);
          bos = new ByteArrayOutputStream();
          int len;
          while ((len = inputStream.read()) != -1) {
              bos.write(len);
          }

          byte[] data = bos.toByteArray();
          // 调用defineClass 生成Class对象
          return defineClass(name, data, 0, data.length);
      } catch (Exception e) {
          e.printStackTrace();
      } finally {
          try {
              if (inputStream != null) {
                  inputStream.close();
              }
              if (bos != null) {
                  bos.close();
              }
          } catch (IOException e) {
              e.printStackTrace();
          }
      }

      return super.findClass(name);
  }

  /**
   * 获取要加载的Class文件
   *
   * @param name:类全限定名
   * @return
   */
  private String getFileName(String name) {
      int index = name.lastIndexOf('.');
      if (index == -1) {
          return name + ".class";
      } else {
          return name.substring(index + 1) + ".class";
      }
  }
}

3、使用自定义类加载器加载类

public static void main(String[] args) throws ClassNotFoundException {
    // 自定义类加载器
    CustomClassLoader customClassLoader = new CustomClassLoader("/Users/xudj");

    // 查找要加载的Class文件;调用loadClass方法,双亲委派。
    Class<?> clazz = customClassLoader.loadClass("top.xudj.Test");

    // 创建对象
    if (clazz != null) {
        try {
            Object instance = clazz.newInstance();
            Method method = clazz.getDeclaredMethod("say", null);
            // 调用方法,输出:Hello
            method.invoke(instance);
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
    }
}

输出:

Hello

补充:
1、在自定义加载器加载Calss类时,使用loadClass方法进行加载,保证其双亲委派模型。
2、使用 Class.forName(Clazz.class.getName()); 进行加载类时,默认是使用当前调用类的加载器,不进行类初始化;可以指定是否初始化并指定类加载器进行查找。

总结

类加载器在类的生命周期中,负责将Class文件加载到内存中,并获得Class对象。加载遵循双亲委派模型,保证Java稳定运行。

当然也有些场景可能会破坏类加载器的双亲委派模型,例如线程上下文类加载的配置和使用,有可能会是父类加载器请求之类加载器进行加载,如JNDI场景。

不同的类加载器都有自己的加载类的范围,程序如果想加载磁盘或者网络中的类,如果使用启动或扩展类或应用程序类加载器加载会报java.lang.ClassNotFoundException,这个加载在某种意义上来说就是查找的意思。