细究Java类加载机制和Tomcat类加载机制

顾名思义,ClassLoader 是用来加载 Class 的。它负责将 Class 的字节码形式转换成内存形式的 Class 对象。字节码可以来自于磁盘文件 *.class,也可以是 jar 包里的 *.class,也可以来自远程服务器提供的字节流,字节码的本质就是一个字节数组 []byte,它有特定的复杂的内部格式。

每个 Class 对象的内部都有一个 classLoader 字段来标识自己是由哪个 ClassLoader 加载的。ClassLoader 就像一个容器,里面装了很多已经加载的 Class 对象。

class Class {
  ...
  private final ClassLoader classLoader;
  ...
}

【1】JVM类加载机制概述与分类

细究Java类加载机制和Tomcat类加载机制_第1张图片

类加载器主要分为两类,一类是 JDK 默认提供的,一类是用户自定义的。

① JDK 默认提供三种类加载器

  • Bootstrap ClassLoader 启动类加载器:每次执行 java 命令时都会使用该加载器为虚拟机加载核心类。该加载器是由 native code 实现,而不是 Java 代码,加载类的路径为 /jre/lib或者被-Xbootclasspath参数所指定的路径中的并且是虚拟机识别的(仅按照文件名识别,如rt.jar,名字不符合的类库即使放在lib目录中也不会被加载)。特别的 /jre/lib/rt.jar 中包含了 sun.misc.Launcher类, 而sun.misc.Launcher$ExtClassLoader 和 sun.misc.Launcher$AppClassLoader 都是 sun.misc.Launcher的内部类,所以拓展类加载器和系统类加载器都是由启动类加载器加载的。
    细究Java类加载机制和Tomcat类加载机制_第2张图片

这个加载器很特殊,它不是Java类,因此它不需要被别人加载。它嵌套在Java虚拟机内核里面,也就是JVM启动的时候Bootstrap就已经启动,它是用C++写的二进制代码(不是字节码),它可以去加载别的类。另外需要注意的是可以通过启动jvm时指定-Xbootclasspath和路径来改变Bootstrap ClassLoader的加载目录。

Java字节码文件是经过编译器预处理过的一种文件,是JAVA的执行文件存在形式,它本身是二进制文件,但是不可以被系统直接执行,而是需要虚拟机解释执行。

启动类加载器无法被Java程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给引导类加载器,那直接使用null代替即可。


  • Extension ClassLoader, 拓展类加载器:用于加载拓展库中的类。拓展库路径为 /jre/lib/ext/。实现类为 sun.misc.Launcher$ExtClassLoader。还可以加载-D java.ext.dirs选项指定的目录。
    细究Java类加载机制和Tomcat类加载机制_第3张图片

我们先前的内容有说过,可以指定-D java.ext.dirs参数来添加和改变ExtClassLoader的加载路径。这里我们通过可以编写测试代码。

System.out.println(System.getProperty("java.ext.dirs"));

结果如下:

C:\Program Files\Java\jdk1.8.0_101\jre\lib\ext;C:\WINDOWS\Sun\Java\lib\ext

  • System ClassLoader 系统类加载器:用于加载 CLASSPATH 中的类。实现类为 sun.misc.Launcher$AppClassLoader。一个ClassLoader创建时如果没有指定parent,那么它的parent默认就是AppClassLoader。

其类继承关系示意图如下:
细究Java类加载机制和Tomcat类加载机制_第4张图片

AppClassLoader加载的就是java.class.path下的路径。

System.out.println(System.getProperty("java.class.path"));

Custom ClassLoader, 一般都是 java.lang.ClassLoder 的子类。正统的类加载机制是基于双亲委派的,也就是当调用类加载器加载类时,首先将加载任务委派给双亲,若双亲无法加载成功时,自己才进行类加载。在实例化一个新的类加载器时,我们可以为其指定一个 parent,即双亲,若未显式指定,则 System ClassLoader–AppClassLoader 就作为默认双亲。


java.net.URLClassLoader:那些位于网络上静态文件服务器提供的 jar 包和 class文件,jdk 内置了一个 URLClassLoader,用户只需要传递规范的网络路径给构造器,就可以使用 URLClassLoader 来加载远程类库了。URLClassLoader 不但可以加载远程类库,还可以加载本地路径的类库,取决于构造器中不同的地址形式。ExtensionClassLoader 和 AppClassLoader 都是 URLClassLoader 的子类,它们都是从本地文件系统里加载类库。

// 尝试通过 URLClassLoader 来加载桌面下的 Test 类。
public static void main(String[] args) {
     try {
           URL[] urls = new URL[1];
           URLStreamHandler streamHandler = null;
           File classPath = new File("C:\\Users\\Administrator\\Desktop\\");
           String repository = new URL("file", null,  classPath.getCanonicalPath() + File.separator).toString();
           urls[0] = new URL(null, repository, streamHandler);	 
           ClassLoader loader = new URLClassLoader(urls);
           Class testClass = loader.loadClass("Test");
           //output:  class Test
           System.out.println(testClass);
           // output:  java.net.URLClassLoader@7f31245a
           System.out.println(testClass.getClassLoader());
       } catch (MalformedURLException e) {
           e.printStackTrace();
       } catch (IOException e) {
           e.printStackTrace();
       } catch (ClassNotFoundException e) {
           e.printStackTrace();
       }
   }

AppClassLoader 可以由 ClassLoader 类提供的静态方法 getSystemClassLoader() 得到,它就是我们所说的「系统类加载器」,我们用户平时编写的类代码通常都是由它加载的。当我们的 main 方法执行的时候,这第一个用户类的加载器就是 AppClassLoader。


② java.lang.ClassLoader

ClassLoader 是一个抽象类,负责加载类,给定类的二进制名称,类加载器应该尝试定位或生成构成类的定义的数据。典型的策略是将名称转换为文件名,然后从文件系统中读取该名称的“类文件”。即通过类名从文件系统中找到对应的*.class文件并读取。

每一个类的Class对象都包含一个getClassLoader() 方法引用定义该类的ClassLoader。但是数组并非由类加载器创建,而是由JVM在需要的时候创建。调用数组对象的getClassLoader()实际上返回的时数组元素对象的类加载器。如果数组元素是基本类型,那么就没有关联的类加载器。

应用程序实现了ClassLoader的子类,以扩展Java虚拟机动态加载类的方式。类加载程序通常可以由安全管理器使用来指示安全域。

ClassLoader使用一个委派模型来搜索类和资源。 ClassLoader 的每个实例都有一个关联的父类加载器。当请求查找类或资源时,ClassLoader实例将在自身试图查找类或资源本身之前,将对类或资源的搜索委托给其父类加载器。虚拟机的内置类加载器("bootstrap class loader")本身没有父类,但是可以作为ClassLoader实例的父类。

支持类并发加载的类加载器被称为具有并行能力类加载器,需要在类初始化时通过调用ClassLoader.registerAsllelCapable方法注册自己。需要注意的是,这是默认行为。但是其子类如果是具有并行能力的,仍然需要注册自己。

在委托模型不是严格分层的环境中,类加载器需要具有并行能力,否则类加载可能导致死锁,因为加载器锁在类加载过程的持续时间内被保持(参见loadClass方法)。

通常,Java虚拟机以平台依赖的方式从本地文件系统加载类。例如,在UNIX系统中,虚拟机从CLASSPATH环境变量定义的目录加载类。然而,有些类可能不源自文件,它们可能源自其他途径,例如网络,或者它们可以由应用程序构造。方法defineClass(String,byte[],int,int)将字节数组转换为类Class的实例。这个新定义的类的实例可以使用Class.newInstance创建。

类加载器创建的对象的方法和构造函数可能引用其他类。为了确定所引用的类,Java虚拟机调用最初创建引用类的类加载器的loadClass方法。

例如,应用程序可以创建一个网络类加载器来从服务器下载类文件。示例代码如下:

ClassLoader loader= new NetworkClassLoader(host,port);
Object main= loader.loadClass("Main", true).newInstance();

网络类加载器子类必须定义方法findClassloadClassData,以便从网络加载类。一旦下载了组成类的字节,它就应该使用defineClass方法来创建类实例。示例实现是:

class NetworkClassLoader extends ClassLoader {
		 String host;
		 int port;
		 public Class findClass(String name) {
			 byte[] b = loadClassData(name);
			 return defineClass(name, b, 0, b.length);
		 }
		 
		 private byte[] loadClassData(String name) {
              // load the class data from the connection
		        //...      		          
	 	}
}

需要注意的是,ClassLoader类中方法的参数(类名)必须是符合Java语言规范定义的二进制名称The Java™ Language Specification。实例如下:

java.lang.String
javax.swing.JSpinner$DefaultEditor
java.security.KeyStore$Builder$FileBuilder$1
java.net.URLClassLoader$3$1

③ 类加载器执行顺序

执行顺序为:

  1. Bootstrap CLassloder
  2. Extention ClassLoader
  3. AppClassLoader

看sun.misc.Launcher,它是一个java虚拟机的入口应用:

public class Launcher {
    private static URLStreamHandlerFactory factory = new Launcher.Factory();
    private static Launcher launcher = new Launcher();
    //这里获取bootClassPath -BootStarp ClassLoader加载路径
    private static String bootClassPath = System.getProperty("sun.boot.class.path");
    private ClassLoader loader;
    private static URLStreamHandler fileHandler;

    public static Launcher getLauncher() {
        return launcher;
    }

    public Launcher() {
        Launcher.ExtClassLoader var1;
        try {
         // Create the extension class loader
            var1 = Launcher.ExtClassLoader.getExtClassLoader();
        } catch (IOException var10) {
            throw new InternalError("Could not create extension class loader", var10);
        }

        try {
         // Now create the class loader to use to launch the application
            this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
        } catch (IOException var9) {
            throw new InternalError("Could not create application class loader", var9);
        }
 //设置AppClassLoader为线程上下文类加载器
        Thread.currentThread().setContextClassLoader(this.loader);
        String var2 = System.getProperty("java.security.manager");
        if(var2 != null) {
            SecurityManager var3 = null;
            if(!"".equals(var2) && !"default".equals(var2)) {
                try {
                    var3 = (SecurityManager)this.loader.loadClass(var2).newInstance();
                } catch (IllegalAccessException var5) {
                    ;
                } catch (InstantiationException var6) {
                    ;
                } catch (ClassNotFoundException var7) {
                    ;
                } catch (ClassCastException var8) {
                    ;
                }
            } else {
                var3 = new SecurityManager();
            }

            if(var3 == null) {
                throw new InternalError("Could not create SecurityManager: " + var2);
            }

            System.setSecurityManager(var3);
        }

    }
 /*
     * Returns the class loader used to launch the main application.
     */
    public ClassLoader getClassLoader() {
        return this.loader;
    }

   /*
     * The class loader used for loading installed extensions.
     */
    static class ExtClassLoader extends URLClassLoader {//...}

/**
     * The class loader used for loading from java.class.path.
     * runs in a restricted security context.
     */
    static class AppClassLoader extends URLClassLoader {//...}

源码有精简,我们可以得到相关的信息。

  1. Launcher初始化了ExtClassLoader和AppClassLoader。
  2. 使用ExtClassLoader创建AppClassLoader。
  3. Launcher中并没有看见BootstrapClassLoader,但通过System.getProperty(“sun.boot.class.path”)得到了字符串bootClassPath,这个应该就是BootstrapClassLoader加载的jar包路径。

我们可以先代码测试一下sun.boot.class.path是什么内容。

System.out.println(System.getProperty("sun.boot.class.path"));

得到的结果是:

C:\Program Files\Java\jdk1.8.0_101\jre\lib\resources.jar;
C:\Program Files\Java\jdk1.8.0_101\jre\lib\rt.jar;
C:\Program Files\Java\jdk1.8.0_101\jre\lib\sunrsasign.jar;
C:\Program Files\Java\jdk1.8.0_101\jre\lib\jsse.jar;
C:\Program Files\Java\jdk1.8.0_101\jre\lib\jce.jar;
C:\Program Files\Java\jdk1.8.0_101\jre\lib\charsets.jar;
C:\Program Files\Java\jdk1.8.0_101\jre\lib\jfr.jar;
C:\Program Files\Java\jdk1.8.0_101\jre\classes

可以看到,这些全是JRE目录下的jar包或者是class文件。自此我们已经知道了BootstrapClassLoader、ExtClassLoader、AppClassLoader实际是查阅相应的环境属性sun.boot.class.path、java.ext.dirs和java.class.path来加载资源文件的。

  • JVM启动时通过Bootstrap类加载器加载rt.jar等核心jar包中的class文件。
  • JVM初始化sun.misc.Launcher并创建Extension ClassLoader和AppClassLoader实例。
  • 并将ExtClassLoader设置为AppClassLoader的父加载器。

④ 每个类加载器实例都有一个父加载器

AppClassLoader的父加载器是ExtClassLoader,而ExtClassLoader并没有显示指定parent–null,但是Bootstrap CLassLoader是其父加载器。

如上面贴过得Launcher类源码所示:

public class Launcher {
    private static URLStreamHandlerFactory factory = new Launcher.Factory();
    private static Launcher launcher = new Launcher();
    private static String bootClassPath = System.getProperty("sun.boot.class.path");
    private ClassLoader loader;
    private static URLStreamHandler fileHandler;

    public static Launcher getLauncher() {
        return launcher;
    }

    public Launcher() {
        Launcher.ExtClassLoader var1;
        try {
        //获取ExtClassLoader
            var1 = Launcher.ExtClassLoader.getExtClassLoader();
        } catch (IOException var10) {
            throw new InternalError("Could not create extension class loader", var10);
        }

        try {
        //使用ExtClassLoader获取AppClassLoader
            this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);

private final ClassLoader parent;定义在ClassLoader中,有关parent的赋值在其构造方法中:

private ClassLoader(Void unused, ClassLoader parent) {
        this.parent = parent;
        if (ParallelLoaders.isRegistered(this.getClass())) {
            parallelLockMap = new ConcurrentHashMap<>();
            package2certs = new ConcurrentHashMap<>();
            domains =
                Collections.synchronizedSet(new HashSet());
            assertionLock = new Object();
        } else {
            // no finer-grained lock; lock on the classloader instance
            parallelLockMap = null;
            package2certs = new Hashtable<>();
            domains = new HashSet<>();
            assertionLock = this;
        }
    }

需要注意的是父加载器和父类不同,如下所示AppClassLoader和ExtClassLoader均继承于URLClassLoader:

static class ExtClassLoader extends URLClassLoader {}
static class AppClassLoader extends URLClassLoader {}

ClassLoader 相当于类的命名空间,起到了类隔离的作用。位于同一个 ClassLoader 里面的类名是唯一的,不同的 ClassLoader 可以持有同名的类。ClassLoader 是类名称的容器,是类的沙箱。

类加载器双亲委派模型示意图如下:
细究Java类加载机制和Tomcat类加载机制_第5张图片
不同的 ClassLoader 之间也会有合作,它们之间的合作是通过 parent 属性和双亲委派机制来完成的。parent 具有更高的加载优先级。除此之外,parent 还表达了一种共享关系,当多个子 ClassLoader 共享同一个 parent 时,那么这个 parent 里面包含的类可以认为是所有子 ClassLoader 共享的。这也是为什么 BootstrapClassLoader 被所有的类加载器视为祖先加载器,JVM 核心类库自然应该被共享。


⑤ 双亲委派模型

一个类加载器查找class和resource时,是通过“委托模式”进行的,它首先判断这个class是不是已经加载成功,如果没有的话它并不是自己进行查找,而是先通过父加载器,然后递归下去,直到Bootstrap ClassLoader,如果Bootstrap classloader找到了,直接返回,如果没有找到,则一级一级返回,最后到达自身去查找这些对象。这种机制就叫做双亲委托。

步骤如下:

  • 一个AppClassLoader查找资源时,先看看缓存是否有,缓存有从缓存中获取,否则委托给父加载器。
  • 递归,重复第1部的操作。
  • 如果ExtClassLoader也没有加载过,则由Bootstrap ClassLoader出面,它首先查找缓存,如果没有找到的话,就去找自己的规定的路径下,也就是sun.mic.boot.class下面的路径。找到就返回,没有找到,让子加载器自己去找。
  • Bootstrap ClassLoader如果没有查找成功,则ExtClassLoader自己在java.ext.dirs路径中去查找,查找成功就返回,查找不成功,再向下让子加载器找。
  • ExtClassLoader查找不成功,AppClassLoader就自己查找,在java.class.path路径下查找。找到就返回。

如果没有找到就让子类找,如果没有子类会怎么样?抛出各种异常。

细究Java类加载机制和Tomcat类加载机制_第6张图片
双亲委托机制的核心思想分为两个步骤。其一,自底向上检查类是否已经加载;其二,自顶向下尝试加载类。


⑥ ClassLoader.loadClass()方法

该方法即为类加载的过程。如下所示:

  • 通过 findLoadedClass() 检查该类是否已经被加载。该方法为 native code 实现,若该类已加载则返回。
  • 若未加载则委派给双亲,parent.loadClass(),若成功则返回。
  • 若未成功,则调用 findClass() 方法加载类。java.lang.ClassLoader 中该方法只是简单的抛出一个 ClassNotFoundException 所以,自定义的 ClassLoader 都需要 Override findClass() 方法。
  • 如果class在上面的步骤中找到了,参数resolve又是true的话,那么loadClass()又会调用resolveClass(Class)这个方法来生成最终的Class对象。

loadClass方法源码实例如下(通过指定的全限定类名加载class,它通过同名的loadClass(String,boolean)方法):

protected Class loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // 判断是否已经加载了
            Class c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                	//如果父类不为null则使用父类加载
                    if (parent != null) {
                    //交给双亲
                        c = parent.loadClass(name, false);
                    } else {
                    	//Returns a class loaded by the bootstrap class loader;
                    	// or return null if not found.
                    	//findBootstrapClass同样是一个native 方法
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    //如果双亲都不行,只能靠自己了
                    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;
        }
    }

ClassLoader.findClass方法(交给子类自己去实现):

protected Class findClass(String name) throws ClassNotFoundException {
        throw new ClassNotFoundException(name);
    }

⑦ 为什么使用双亲委派模型

双亲委派模型有这样几个机制:

  • 父类加载器能加载的一定不让子类加载器加载;
  • 类只被某个类加载器记载一次;

双亲委派模型是为了保护核心类的安全!

比如,你自定义一个java.util.List并自定义方法,将会编译通过但是运行抛异常的。因为java.util.List在程序运行时已经被根类加载器加载进JVM内存了,JVM就是使用双亲委派模型来保护核心类的安全!


⑧ 延迟加载

JVM 运行并不是一次性加载所需要的全部类的,它是按需加载,也就是延迟加载。程序在运行的过程中会逐渐遇到很多不认识的新类,这时候就会调用 ClassLoader 来加载这些类。加载完成后就会将 Class 对象存在 ClassLoader 里面,下次就不需要重新加载了。

比如你在调用某个类的静态方法时,首先这个类肯定是需要被加载的,但是并不会触及这个类的实例字段,那么实例字段的类别 Class 就可以暂时不必去加载,但是它可能会加载静态字段相关的类别,因为静态方法会访问静态字段。而实例字段的类别需要等到你实例化对象的时候才可能会加载。


⑨ ClassLoader 传递性

程序在运行过程中,遇到了一个未知的类,它会选择哪个 ClassLoader 来加载它呢?虚拟机的策略是使用调用者 Class 对象的 ClassLoader 来加载当前未知的类。何为调用者 Class 对象?就是在遇到这个未知的类时,虚拟机肯定正在运行一个方法调用(静态方法或者实例方法),这个方法挂在哪个类上面,那这个类就是调用者 Class 对象。前面我们提到每个 Class 对象里面都有一个 classLoader 属性记录了当前的类是由谁来加载的。

因为 ClassLoader 的传递性,所有延迟加载的类都会由初始调用 main 方法的这个 ClassLoader 全全负责,它就是 AppClassLoader。


⑩ Class.forName

当我们在使用 jdbc 驱动时,经常会使用 Class.forName 方法来动态加载驱动类。

Class.forName("com.mysql.cj.jdbc.Driver");

其原理是 mysql 驱动的 Driver 类里有一个静态代码块,它会在 Driver 类被加载的时候执行。这个静态代码块会将 mysql 驱动实例注册到全局的 jdbc 驱动管理器里。

class Driver {
  static {
    try {
       java.sql.DriverManager.registerDriver(new Driver());
    } catch (SQLException E) {
       throw new RuntimeException("Can't register driver!");
    }
  }
  ...
}

forName 方法同样也是使用调用者 Class 对象的 ClassLoader 来加载目标类。不过 forName 还提供了多参数版本,可以指定使用哪个 ClassLoader 来加载。

Class forName(String name, boolean initialize, ClassLoader cl)

通过这种形式的 forName 方法可以突破内置加载器的限制,通过使用自定类加载器允许我们自由加载其它任意来源的类库。根据 ClassLoader 的传递性,目标类库传递引用到的其它类库也将会使用自定义加载器加载。


(11)Class.forName vs ClassLoader.loadClass

这两个方法都可以用来加载目标类,它们之间有一个小小的区别,那就是 Class.forName() 方法可以获取原生类型的 Class,而 ClassLoader.loadClass() 则会报错。

Class x = Class.forName("[I");
System.out.println(x);

x = ClassLoader.getSystemClassLoader().loadClass("[I");
System.out.println(x);

---------------------
class [I

Exception in thread "main" java.lang.ClassNotFoundException: [I
...

(12)ClassLoader隔离问题

每个类装载器都有一个自己的命名空间用来保存已装载的类。当一个类装载器装载一个类时,它会通过保存在命名空间里的类全局限定名(Fully Qualified Class Name)进行搜索来检测这个类是否已经被加载了。

JVM 及 Dalvik 对类唯一的识别是 ClassLoader id + PackageName + ClassName,所以一个运行程序中是有可能存在两个包名和类名完全一致的类的。并且如果这两个”类”不是由一个 ClassLoader 加载,是无法将一个类的示例强转为另外一个类的,这就是 ClassLoader 隔离。


【2】Tomcat 8.5.15类加载机制

细究Java类加载机制和Tomcat类加载机制_第7张图片

Tomcat 使用正统的类加载机制(双亲委派),但部分地方做了改动。

Bootstrap classLoader 和 Extension classLoader 的作用不变。

System classLoader 正常情况下加载的是 CLASSPATH 下的类,但是 Tomcat 的启动脚本并未使用该变量,而是从以下仓库下加载类:

  • $CATALINA_HOME/bin/bootstrap.jar 包含了 Tomcat 的启动类。在该启动类中创建了 Common classLoader、Catalina classLoader、shared classLoader。因为 $CATALINA_BASE/conf/catalina.properties 中只对 common.loader 属性做了定义,server.loader 和 shared.loader 属性为空,所以默认情况下,这三个 classLoader 都是 CommonLoader。具体的代码逻辑可以查阅 org.apache.catalina.startup.Bootstrap 类的 initClassLoaders() 方法和 createClassLoader() 方法。
  • $CATALINA_BASE/bin/tomcat-juli.jar 包含了 Tomcat 日志模块所需要的实现类。
  • $CATALINA_HOME/bin/commons-daemon.jar

Common classLoader 是位于 Tomcat 应用服务器顶层的公用类加载器。由其加载的类可以由 Tomcat 自身类和所有应用程序使用。扫描路径由 $CATALINA_BASE/conf/catalina.properties文件中的 common.loader 属性定义。默认是 $CATALINA_HOME/lib。

catalina classLoader 用于加载服务器内部可见类,这些类应用程序不能访问。

shared classLoader 用于加载应用程序共享类,这些类服务器不会依赖。

Webapp classLoader 。每个应用程序都会有一个独一无二的 webapp classloader,他用来加载本应用程序/WEB-INF/classes 和 /WEB-INF/lib 下的类。

Webapp classLoader 的默认行为会与正常的双亲委派模式不同:
* 从 Bootstrap classloader 加载。
* 若没有,从 /WEB-INF/classes 加载。
* 若没有,从 /WEB-INF/lib/*.jar 加载。
* 若没有,则依次从 System、Common、shared 加载(该步骤使用双亲委派)。

当然了,我们也可以通过配置来使 Webapp classLoader 严格按照双亲委派模式加载类:

  • 通过在工程的 META-INF/context.xml(和 WEB-INF/classes 在同一目录下) 配置文件中添加
  • 因为 Webapp classLoader 的实现类是 org.apache.catalina.loader.WebappLoader,他有一个属性叫 delegate, 用来控制类加载器的加载行为,默认为 false,我们可以使用 set 方法,将其设为 true 来启用严格双亲委派加载模式。

严格双亲委派模式加载步骤:

  • 从 Bootstrap classloader 加载。
  • 若没有,则依次从 System、Common、shared 加载。
  • 若没有,从 /WEB-INF/classes 加载。
  • 若没有,从 /WEB-INF/lib/*.jar 加载。

【3】自定义ClassLoader

自定义类加载器不易破坏双亲委派规则,不要轻易覆盖 loadClass 方法。否则可能会导致自定义加载器无法加载内置的核心类库。在使用自定义加载器时,要明确好它的父加载器是谁,将父加载器通过子类的构造器传入。如果父类加载器是 null,那就表示父加载器是「根加载器」。

步骤如下:

  • 编写一个类继承自ClassLoader抽象类。
  • 复写它的findClass()方法。
  • 在findClass()方法中调用defineClass()。

假设我们需要一个自定义的classloader,默认加载路径为D:\hh下的jar包和资源。

① 编写测试类

如下所示,并将其编译过的class文件放在D:\hh下。

package com.jane.controller;

public class TestClassLoader {
    
    public void say(){
        System.out.println("Hello");
    }
}

② 编写DiskClassLoader

如下所示:

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;

/**
 * Created by Janus on 2018/12/9.
 */
public class DiskClassLoader extends ClassLoader {
    private String mLibPath;

    public DiskClassLoader(String path) {
        // TODO Auto-generated constructor stub
        mLibPath = path;
    }

    @Override
    protected Class findClass(String name) throws ClassNotFoundException {
        // TODO Auto-generated method stub
        String fileName = getFileName(name);

        File file = new File(mLibPath,fileName);
	 System.out.println("DiskClassLoader.findClass "+fileName);
        try {
            FileInputStream is = new FileInputStream(file);

            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            int len = 0;
            try {
                while ((len = is.read()) != -1) {
                    bos.write(len);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }

            byte[] data = bos.toByteArray();
            is.close();
            bos.close();

            return defineClass(name,data,0,data.length);

        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

        return super.findClass(name);
    }

    //获取要加载 的class文件名--name为类限定全类名
    private String getFileName(String name) {
        // TODO Auto-generated method stub
        int index = name.lastIndexOf('.');
        if(index == -1){
            return name+".class";
        }else{
            return name.substring(index+1)+".class";
        }
    }
}

在findClass()方法中定义了查找class的方法,然后数据通过defineClass()生成了Class对象。


③ 编写测试代码

测试代码如下:

public class ClassLoaderTest {
    public static void main(String[] args){
        DiskClassLoader diskLoader = new DiskClassLoader("D:\\hh");
        try {
            //加载class文件--参数为完全限定类名
            Class c = diskLoader.loadClass("com.jane.controller.TestClassLoader");

            if(c != null){
                try {
                    Object obj = c.newInstance();
                    Method method = c.getDeclaredMethod("say",null);
                    //通过反射调用TestClassLoader类的say方法
                    method.invoke(obj, null);
                } catch (InstantiationException | IllegalAccessException
                        | NoSuchMethodException
                        | SecurityException |
                        IllegalArgumentException |
                        InvocationTargetException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
        } catch (ClassNotFoundException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

    }
}

运行结果如下(测试的时候将TestClassLoader 从IDE中移除掉,否则AppClassLoader将会从classpath下加载):

DiskClassLoader.findClass TestClassLoader.class
Hello

另外,如下图所示,可以看到DiskClassLoader的parent为AppClassLoader
细究Java类加载机制和Tomcat类加载机制_第8张图片


【4】Thread.contextClassLoader

Thread 实例字段中发现有一个字段非常特别:

class Thread {
  ...
  private ClassLoader contextClassLoader;

  public ClassLoader getContextClassLoader() {
    return contextClassLoader;
  }

  public void setContextClassLoader(ClassLoader cl) {
    this.contextClassLoader = cl;
  }
  ...
}

contextClassLoader「线程上下文类加载器」,这究竟是什么东西?

首先 contextClassLoader 是那种需要显示使用的类加载器,如果你没有显示使用它,也就永远不会在任何地方用到它。你可以使用下面这种方式来显示使用它:

Thread.currentThread().getContextClassLoader().loadClass(name);

这意味着如果你使用 forName(string name) 方法加载目标类,它不会自动使用 contextClassLoader。那些因为代码上的依赖关系而懒惰加载的类也不会自动使用 contextClassLoader来加载。

其次线程的 contextClassLoader 默认是从父线程那里继承过来的,所谓父线程就是创建了当前线程的线程。程序启动时的 main 线程的 contextClassLoader 就是 AppClassLoader。这意味着如果没有人工去设置,那么所有的线程的 contextClassLoader 都是 AppClassLoader。

那这个 contextClassLoader 究竟是做什么用的?我们要使用前面提到了类加载器分工与合作的原理来解释它的用途。

它可以做到跨线程共享类,只要它们共享同一个 contextClassLoader。父子线程之间会自动传递 contextClassLoader,所以共享起来将是自动化的。

如果不同的线程使用不同的 contextClassLoader,那么不同的线程使用的类就可以隔离开来。

如果我们对业务进行划分,不同的业务使用不同的线程池,线程池内部共享同一个 contextClassLoader,线程池之间使用不同的 contextClassLoader,就可以很好的起到隔离保护的作用,避免类版本冲突。

如果我们不去定制 contextClassLoader,那么所有的线程将会默认使用 AppClassLoader,所有的类都将会是共享的。

JDK9 增加了模块功能之后对类加载器的结构设计做了一定程度的修改,不过类加载器的原理还是类似的,作为类的容器,它起到类隔离的作用,同时还需要依靠双亲委派机制来建立不同的类加载器之间的合作关系。

参考博文:
Java ClassLoader 详解
https://blog.csdn.net/briblue/article/details/54973413。

你可能感兴趣的:(深入浅出JVM,深入浅出JVM)