Java类加载器和双亲委派机制

362 阅读7分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第1天,点击查看活动详情

Java 类加载器

类加载就是将.class文件通过IO读入内存,并生成一个Class对象的过程。

类加载通常由JVM提供默认的类加载器,这些被称之为系统加载器,还可以通过继承ClassLoader来实现自定义加载器。

一个类被加载到JVM中后,通常不会被加载第二次,因为通过双亲委派模型可以很好的避免重复加载的问题。

一、类加载器种类

Java中默认的类加载器有三种,分别为:

  • BootStrap ClassLoader (启动类加载器)
    • 执行优先级最高的类加载器
    • 用于加载JRE核心类库的.jar文件,如rt.jar
    • 使用C语言实现,双亲委派顶级祖宗加载器
  • Extensions ClassLoader 也称 (Ext ClassLoader) (扩展类加载器)
    • 用于加载JRE扩展目录下的.jar文件
    • 使用Java实现
  • Application ClassLoader (应用程序类加载器)
    • 用于加载用户自己编写的类文件,和项目中引用其他.jar包中的类文件
  • Custom ClassLoader (自定义加载器)
    • 用于加载指定目录下的类,通常用于自定义实现
  • 关系类图如下
类加载器

二、双亲委派模型

**1. ** 双亲委派模型其表达的意思是,类加载器与父亲加载器嵌套,当需要加载类的时候回去现寻找父亲加载器,如果有父亲加载器的话就优先使用父亲加载器,如果父亲加载器无法完成加载的话,再使用自己进行加载。

说人话:现在有一个苹果(Class),你不吃你给你妈妈吃,你妈妈不吃又给你奶奶吃,你奶奶不吃有给你妈妈吃,你妈妈不吃又给你吃。

2. 使用双亲委派模型有以下好处

  • 可以避免重复加载类
    • 虽然有多个类加载器,但是因为每个类都是唯一的,且都会通过父亲加载器加载,所以可以避免类加载重复。
  • 可以避免Java核心类被用户篡改
    • 如果用户自定义了一个类为java.lang.String,那么会一层一层向上加载,直到BootStrap ClassLoader祖宗类加载器(入口加载器),但是在这个加载器里已经将JDK包下的java.lang.String加载过了,所以会直接跳过加载用户编写的String类。
    • 但是此处有个BUG,就是双亲委派可以被破坏!

3. 双亲委派模型图


三、破坏双亲委派

双亲委派有两个弊端:

  • 不可以不委派
  • 不可以向下委派

但是这两个弊端都可以被打破

  • 通过自定义类加载器可以破坏:不可以不委派
  • 通过SPI机制可以破坏:不能向下委派

源码中可以得知,双亲委派机制是由此处进行调用的

// AppClassLoader  ||  ExtClassLoader 
public Class<?> loadClass(String name, boolean resolve)
   throws ClassNotFoundException
 {
   int i = name.lastIndexOf('.');
   // 权限验证
   if (i != -1) {
     SecurityManager sm = System.getSecurityManager();
     if (sm != null) {
       sm.checkPackageAccess(name.substring(0, i));
     }
   }

   // 判断类在classPath中是否存在
   if (ucp.knownToNotExist(name)) {
     // The class of the given name is not found in the parent
     // class loader as well as its local URLClassPath.
     // Check if this class has already been defined dynamically;
     // if so, return the loaded class; otherwise, skip the parent
     // delegation and findClass.
     // 查找此类是否已经被加载过了,如果是的话则返回Class<?>对象,否则返回null,则调用父类loadClass()方法进行加载
     Class<?> c = findLoadedClass(name);
     if (c != null) {
       if (resolve) {
         resolveClass(c);
       }
       return c;
     }
     throw new ClassNotFoundException(name);
   }
	 // 调用父类的loadClass,内部实现了双亲委派
   return (super.loadClass(name, resolve));
 }

父类的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 {
          // 如果父亲加载器等于null的话,则代表执行当前方法的加载器为BootStrap ClassLoader,并使用此加载器进行加载
          // native方法,由C实现
          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;
  }
}

1. 不委派父亲加载器

从上述源码中可以得知,双亲委派机制是由ClassLoader -> loadClass()方法进行实现的,而且此方法非final修饰,则代表此方法是可以被我们的自定义加载器重写的。

双亲委派的核心源码为:

// 查找当前加载器是否有父亲加载器,(双亲委派机制)
if (parent != null) {
  // 调用父亲加载器进行加载
  c = parent.loadClass(name, false);
}

如果我们将loadClass()方法进行重写,且不执行这段逻辑的话,则双亲委派机制就会被打破,从而遭到破坏!

2. 为什么建议重写findClass()方法

因为是出于历史原因,且JDK是向上兼容的,所以该方法并不能随便进行更改,但是JDK开发者也意识到了这个问题,所以在ClassLoader抽象类中新增了findClass()方法,供开发者进行重写,和实现自定义类加载器。

JDK源码中的loadClass()方法上有一段注释说明了此方法不建议被重写:

/*加载具有指定二进制名称的类。该方法的默认实现按照以下顺序搜索类:调用findLoadedClass(String)检查类是否已经加载。在父类装入器上调用loadClass方法。如果父类为空,则使用虚拟机内置的类装入器。调用findClass(String)方法来查找类。如果使用上述步骤找到了类,并且resolve标志为真,则该方法将对生成的class对象调用resolveClass(class)方法。ClassLoader的子类被鼓励重写findClass(String),而不是这个方法。除非被重写,否则这个方法在整个类加载过程中同步getClassLoadingLock方法的结果。*/
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {...}

2. 向下委派子加载器

Java中通过SPI机制可以实现向下委派子加载器,至于为什么这么做也是因为一些历史原因

JDK中引入了SPI机制后,例如JDBC有个驱动管理类DriverManager,他是在rt.jar下的,由BootStrap ClassLoader进行加载,但是由于这个类里需要加载所有的Driver驱动,但是,像是MySQLOracle等数据库驱动都是由各家数据库厂商实现的,是以外部依赖的模式进行引入的,此时如果SPI机制想要顺利的加载驱动,就不能使用BootStrap ClassLoader因为各个数据库驱动并不在rt.jar下,所以无法加载,因此JDK搞了一个线程上下文类加载器,和setContextClassLoader() 来手动设置类加载器,才解决了这个问题,此时其实已经是调用了线程上下文类加载器,一般为Application ClassLoader,进行加载驱动的动作了。


四、自定义Java内部类

如果在自己的工程目录中自定义了一个和JDK中同包,同类名的一个类,例如:java.lang.String,会有什么样的结果?

首先我们了解类加载器和类加载器的双亲委派机制,因此得知,要加载这个类,可以使用Application ClassLoaderCustom ClassLoader进行加载,当其中一个类加载器加载时,会调用父亲的loadClass()方法,最终调用到BootStrap ClassLoader中去,但是由于java.lang.StringJDK核心类,并且已经被祖宗加载器加载过了,所以会跳过自己工程下的java.lang.String。从而避免了核心类篡改和重复加载的问题。

此时如果调用自定义java.lang.String类的方法,会报**NoSuchMethodError**异常。因为自定义的类并没有被加载。


五、附

类加载图解

img