sayi.github.com icon indicating copy to clipboard operation
sayi.github.com copied to clipboard

JVM(四)ClassLoader类加载机制

Open Sayi opened this issue 7 years ago • 0 comments

本文将探讨类加载器是如何寻找且加载Class文件,它的应用场景以及我们如何自定义自己的类加载器。

类加载器ClassLoader

类加载器是将字节码文件加载到JVM,它具有 延迟加载 的特性,需要某个类时才会去加载。如果 同一份字节码文件 被不同类加载器加载后的Class对象,它们是不相等的:即它们不是同一个类。

回到《JVM(三)类和对象的生命周期》文章开头写的代码,打印出了每个类型的加载器,主要分为三种类型:Bootstrap classes、Extension classes和User classes。

Apple.class: sun.misc.Launcher$AppClassLoader@2a139a55
class [Lcom.deepoove.java8.def.Apple;: sun.misc.Launcher$AppClassLoader@2a139a55
Gson.class classloader:
	sun.misc.Launcher$AppClassLoader@2a139a55
	sun.misc.Launcher$ExtClassLoader@4aa298b7
String.class: null

Bootstrap Class Loader

Bootstrap启动类加载器负责加载实现了Java平台的类,包括JDK的lib目录、rt.jar等。启动类的路径是由sun.boot.class.path属性决定的,这个属性只能被引用无法被修改。

下面是我本机的启动类路径:

sun.boot.class.path=/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/resources.jar\:
/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/rt.jar\:
/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/sunrsasign.jar\:
/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/jsse.jar\:
/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/jce.jar\:
/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/charsets.jar\:
/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/jfr.jar\:
/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/classes

所以String.class是由Bootstrap加载器加载,由于这个加载器不是Java编写的(C++编写),所以在上文的输出中显示为null

所以,java.lang.ClassLoader也是由启动类加载器加载的。

Extension Class Loader

sun.misc.Launcher$ExtClassLoader扩展类加载器负责加载JDK目录下lib/ext目录中的包,它是由Java实现的加载器,继承URLClassLoader。注意,扩展类加载器加载的必须是jar或者zip文件,不能是.class文件

System Class Loader

sun.misc.Launcher$AppClassLoader称为应用加载器或者系统加载器,负责加载用户路径下的类,它可以通过静态方法java.lang.ClassLoader.getSystemClassLoader()获取。

双亲委派机制

我们试想一下,如果在用户路径下定义了JDK提供的某个类,如java.lang.String,那么对于整个应用来说这是糟糕的:系统加载器和启动类加载器同时都加载了一样的类,应用的行为是不确定的。

为了解决这种问题,类加载器之间有一种层次关系,每个加载器都有一个父加载器,null表示启动类加载器,可以通过加载器的getParent()方法获取父类加载器。

image

如上图,当我们需要加载String.class时,就会优先向上委托到Bootstrap去加载,这样保证了用户的类无法覆盖JDK的类。所有的类加载都会委托父加载器加载,当父加载器无法完成加载时,子加载器才会尝试自己去加载,如果最终子加载器无法加载该类,则会抛出找不到类的异常,这就是双亲委派机制(Parents Delegation Model)。

我们来看看java.lang.ClassLoader的loadClass源码:

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 {
          c = findBootstrapClassOrNull(name);
        }
      } catch (ClassNotFoundException e) {
        // ClassNotFoundException thrown if class not found
        // from the non-null parent class loader
      }

      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;
  }
}

当我们加载类时会优先判断是否已加载findLoadedClass,然后通过双亲委派机制parent.loadClass(name, false);加载,如果加载不到,再通过findClass(name)加载,接下来会在自定义加载器类中详细描述。

需要注意的是,如果多线程尝试去加载同一个类,在类加载实现中通过互斥锁来实现并发安全。

双亲委派模型带来了两个特性:唯一性和可见性。

  • 唯一性
    唯一性已经说过,类被父类加载器加载,子类无法再加载。

  • 可见性
    被父类加载器加载的类,子类加载器是可见的,这个很容易懂,启动类加载的类在应用中是可见的。

自定义ClassLoader

Java以java.lang.ClassLoader抽象类的形式对外开放了类加载的能力,ClassLoader默认构造方法设置了父类加载器为系统加载器,loadClass方法实现了委托模型,我们只需要重写findClass方法实现自己的类加载逻辑:

protected Class<?> findClass(String name) throws ClassNotFoundException {
    byte[] b = loadClassData(name);
    return defineClass(name, b, 0, b.length);
}
private byte[] loadClassData(String name) {
    // load the class data from the file or network
}

实现大体上分为两步,第一步获取字节码的字节数组byte[],然后通过ClassLoader提供的defineClass方法将字节码定义成Class对象。

java.lang.ClassLoader提供了public URL getResource(String name)protected URL findResource(String name) 方法获取资源,正如同loadClass和findClass方法。

除了继承java.lang.ClassLoader,我们还以继承或者直接使用URLClassLoader,它支持从某个目录或者jar加载类classes或者资源resources

写到这里,我们可能会问自己:自定义类加载器的场景在哪里?下面罗列一些场景,暂时我还没有接触到:

  1. 字节码加密和解密处理
  2. class文件版本号机制,根据版本号加载不同的类
  3. 热部署
  4. 根据JNDI动态加载JDBC驱动,此方法已被SPI替代
  5. agent
  6. 隔离

反双亲委派

双亲委派是一个约定,但是某些场景下未必好使,所以出现了一些违反双亲委派的例外,最简单违反的方法是重写loadClass方法,这样就破坏了默认通过父类加载器加载的约定。

Context ClassLoader

JNDI的核心功能是由Bootstrap启动类加载器加载,但是JNDI需要加载不同厂商提供的JNDI的实现,这样就要求Bootstrap要去加载用户类路径下的类,显然这是无法做到的。

设计者为java.lang.Thread 提供了一个方法:getContextClassLoader()为每个线程返回一个Context ClassLoader,可以通过setContextClassLoader()方法进行设置上下文类加载器,在BootStrap启动类加载JNDI时的线程里就可以获取到Context ClassLoader,如果没有设置,它就是AppClassLoader(实际上是设置了当前线程上下文类加载器为系统类加载器),然后通过它来加载用户路径下的类。

WebApp ClassLoader

web应用容器的类加载模型和双亲委派机制的原则大体上相同,但是也有不一样的地方,jetty和tomcat都有个WebAppClassLoader,为了做到应用之间隔离它们没有遵从双亲委派,这里我们以tomcat为例:

image

上面这张图摘自tomcat官方文档,可以看出来它也是一个父子树结构,遵循唯一性和可见性,但是webapp类加载器不遵循双亲委派机制,它没有优先委托父加载器加载,而是 优先从自己的web应用类路径加载,这样做的原因是每个webapp都希望相互之间隔离,每个应用只对自己 /WEB-INF/classes/WEB-INF/lib 目录下类可见,避免相互之间影响而不对其它应用可见。注意的是,WebappClassLoader会最先委托BootStrap加载JRE提供的类,因为这些类不该被重新定义。,下面是它的加载器加载顺序:

  • Bootstrap classes of your JVM
  • /WEB-INF/classes of your web application
  • /WEB-INF/lib/*.jar of your web application
  • System class loader classes
  • Common class loader classes

具体源码在org.apache.catalina.loader.WebappClassLoaderBase.loadClass(String, boolean)方法中,这里不再赘述。

参考资料

总结

ClassLoader提供了很多方法,ClassLoader默认执行了方法ClassLoader.registerAsParallelCapable,子类如果需要支持并发加载也需要调用这个方法,类加载器不仅仅可以加载类,还可以获取资源Resource,我们可以充分利用类加载器的特性去使用它,比如指定一个包名,然后加载所有的类并且扫描类的注解,我们也可以实现自己的类加载器,从而实现更多更有用的功能。

Sayi avatar Dec 10 '18 17:12 Sayi