ClassLoader类加载器工作原理分析

前言

最近在看公司RPC框架Pegion的源码,里面很多地方用到了Thread.currentThread().getContextClassLoader()。不太清楚Thread.currentThread().getContextClassLoader()与 Class.getClassLoader()两者获取到的ClassLoader的区别。然后便有了本文。

什么是类加载器

简单的讲就是一个专门负责把我们写的java代码编译后生成的字节码(.class文件)加载到JVM的内存中的类(ClassLoad)。哪问题来了ClassLoad本身对应的类又由谁加载,答案是BootstrapClassLoader。其被称作启动类加载器,是最顶层的类加载器,主要用来加载Java核心类,如rt.jar、resources.jar、charsets.jar等。它不是 java.lang.ClassLoader的子类,而是由JVM自身实现的该类c 语言实现,Java程序访问不到该加载器。程序启动时JVM会利用该加载器加载rt.jar、resources.jar、charsets.jar等文件。

通过一下例子看一下类载器之间的关系。

public class App {
    public static void main( String[] args ) {
       ClassLoader loader = App.class.getClassLoader();
       while ( null != loader) {
           System.out.println(loader.getClass().getName());
           loader = loader.getParent();
       }
    }
}

结果:

sun.misc.Launcher$AppClassLoader
sun.misc.Launcher$ExtClassLoader

可以看到App的父类加载器为AppClassLoader,AppClassLoader的父类加载器为ExtClassLoader,ExtClassLoader的父类加载器为null。实际是ExtClassLoader类是由BootstrapClassLoader加载的,所有可以把BootstrapClassLoader作为ExtClassLoader的父类加载器。

ExtClassLoader 扩展类加载器,主要负责加载Java的扩展类库,默认加载JAVA_HOME/jre/lib/ext/目下的所有jar包或者由java.ext.dirs系统属性指定的jar包。

AppClassLoader 系统类加载器,又称应用加载器,它负责在JVM启动时,加载来自在命令java中的-classpath或者java.class.path系统属性或者 CLASSPATH操作系统属性所指定的JAR类包和类路径。

ExtClassLoader与AppClassLoader类加载器的构建在sun.misc.Launcher类中完成。

   public Launcher(){  
        ExtClassLoader localExtClassLoader;  
        try 
        {  //首先创建了ExtClassLoader
          localExtClassLoader = ExtClassLoader.getExtClassLoader();  
        }  
        catch (IOException localIOException1)  
        {  
          throw new InternalError("Could not create extension class loader");  
        }  
        try 
        {  //然后以ExtClassloader作为父加载器创建了AppClassLoader
          this.loader = AppClassLoader.getAppClassLoader(localExtClassLoader);  
        }  
        catch (IOException localIOException2)  
        {  
          throw new InternalError("Could not create application class loader");  
        }  //这个是个特殊的加载器后面会讲到,这里只需要知道默认下线程上下文加载器为appclassloader
        Thread.currentThread().setContextClassLoader(this.loader);  
 
        ................
      }

类加载器之间的关系

7991e7da89d189783944de0348d8fcf552f.jpg

这个关系指的是对应的类加载器是由谁加载过来的,或者理解为通过ClassLoader.getParent方法得到,但于BootstrapClassLoader,java程序无法访问返回的为null。

什么时候加载类

JVM什么时候触发类的加载。在这之前先简单的看一下类的生生命同期,毕竟加载类是为了使用这个类。

f55dabed62f36decf91e743bd835de30f65.jpg

类的生命周期有如下几个阶段,加载、连接(验证、准备、解析)、初始化、使用、卸载,其中加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的,类的加载过程必须按这个顺序来,而解析的阶段则不一定:它在某些情况下可以在初始化化阶段之后再开始,这是为支持java语言的运行时绑定。什么情况下需要开始第一阶段中类加载。java虚拟机规范中并没有强制要求,具体的JVM实现可以不一样。但对于初始化阶段,虚拟机规范则严格规定了只有5种情况必须立即对类进行初始化,而这个之前一定要完成加载、验证、准备。初始化阶段会在以下5种情况下执行

  1. 碰到new、getstatic、putstatic、或invoikestatic这个字节码指令时,如果没有对类进行初始化则对类进行初始化。对应java代码常用分别为使用new关键字实例化一个对象、读取或设置一个static类型的变量、调用static类型的方法。
  2. 使用java.lang.reflect包的方法对类进行反射操作的时候,如果没有进行过初始化,则需要初始化。
  3. 当初始化一个类的时候,如果发现其父类还没有初始化,则会触发其父类初始化。
  4. 当虚拟机启动时,用户需要指定一个要执行的主类,虚拟机会行初始化这个主类。
  5. 当使用JDK7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有初始化,则会先触发其初始化。

双亲委派模式加载类

java语言中类的加载采用双亲委派模式,即一个在加载一个类的过程中当前的类加载器,会先尝试让其父类加载器进行加载,如果父类加载器无法加载到,再尝试由当前的类加载器进行加载。ClassLoader.loadClass方法

/**
     * Loads the class with the specified <a href="#name">binary name</a>.  The
     * default implementation of this method searches for classes in the
     * following order:
     *
     * <ol>
     *
     *   <li><p> Invoke {@link #findLoadedClass(String)} to check if the class
     *   has already been loaded.  </p></li>
     *
     *   <li><p> Invoke the {@link #loadClass(String) <tt>loadClass</tt>} method
     *   on the parent class loader.  If the parent is <tt>null</tt> the class
     *   loader built-in to the virtual machine is used, instead.  </p></li>
     *
     *   <li><p> Invoke the {@link #findClass(String)} method to find the
     *   class.  </p></li>
     *
     * </ol>
     *
     * <p> If the class was found using the above steps, and the
     * <tt>resolve</tt> flag is true, this method will then invoke the {@link
     * #resolveClass(Class)} method on the resulting <tt>Class</tt> object.
     *
     * <p> Subclasses of <tt>ClassLoader</tt> are encouraged to override {@link
     * #findClass(String)}, rather than this method.  </p>
     *
     * <p> Unless overridden, this method synchronizes on the result of
     * {@link #getClassLoadingLock <tt>getClassLoadingLock</tt>} method
     * during the entire class loading process.
     *
     * @param  name
     *         The <a href="#name">binary name</a> of the class
     *
     * @param  resolve
     *         If <tt>true</tt> then resolve the class
     *
     * @return  The resulting <tt>Class</tt> object
     *
     * @throws  ClassNotFoundException
     *          If the class could not be found
     */
    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);//(1)
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);//(2)
                    } else {
                        c = findBootstrapClassOrNull(name);//(3)
                    }
                } 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);//(4)

                    // 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);//(5)
            }
            return c;
        }
    }

其中(1)处判断对应类是否已加载过,加载过直接返回,(2)处调用父类加载器进行加载,(3)当没有父类加载器时调用Bootstrap类加载加载,(4)处调用当前类加载器自己定义的加载方法加载类,默认抛出异常,(5)resolveClass最终调用了一个本地方法做link,这里的link主要做了,验证Class以确保类装载器格式和行为正确;准备后续步骤所需的数据结构;解析所引用的其他类。一般情况下要息定义类加载器一般只要继承ClassLoader,然后实现他的findClass方法。

Thread.currentThread().getContextClassLoader()与 Class.getClassLoader()的区别

介绍完了前面那么多,看看Thread.currentThread().getContextClassLoader()与 Class.getClassLoader()的区别。有些场景下面要我们要打破双亲委派加载的模式,那么这个时候该如何处理呢?答案就是利用Thread.currentThread().getContextClassLoader()。

先看下源码中Thread.currentThread().getContextClassLoader()与 Class.getClassLoader()返回的是什么?

Thread.currentThread().getContextClassLoader()

   /**
     * Returns the context ClassLoader for this Thread. The context
     * ClassLoader is provided by the creator of the thread for use
     * by code running in this thread when loading classes and resources.
     * If not {@linkplain #setContextClassLoader set}, the default is the
     * ClassLoader context of the parent Thread. The context ClassLoader of the
     * primordial thread is typically set to the class loader used to load the
     * application.
     *
     * <p>If a security manager is present, and the invoker's class loader is not
     * {@code null} and is not the same as or an ancestor of the context class
     * loader, then this method invokes the security manager's {@link
     * SecurityManager#checkPermission(java.security.Permission) checkPermission}
     * method with a {@link RuntimePermission RuntimePermission}{@code
     * ("getClassLoader")} permission to verify that retrieval of the context
     * class loader is permitted.
     *
     * @return  the context ClassLoader for this Thread, or {@code null}
     *          indicating the system class loader (or, failing that, the
     *          bootstrap class loader)
     *
     * @throws  SecurityException
     *          if the current thread cannot get the context ClassLoader
     *
     * @since 1.2
     */
    @CallerSensitive
    public ClassLoader getContextClassLoader() {
        if (contextClassLoader == null)
            return null;
        SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            ClassLoader.checkClassLoaderPermission(contextClassLoader,
                                                   Reflection.getCallerClass());
        }
        return contextClassLoader;
    }

返回的是Thread的一个成员变量,没有设置时返回的是父类的装载器。

/* The context ClassLoader for this thread */
private ClassLoader contextClassLoader;

Class.getClassLoader()

   /**
     * Returns the class loader for the class.  Some implementations may use
     * null to represent the bootstrap class loader. This method will return
     * null in such implementations if this class was loaded by the bootstrap
     * class loader.
     *
     * <p> If a security manager is present, and the caller's class loader is
     * not null and the caller's class loader is not the same as or an ancestor of
     * the class loader for the class whose class loader is requested, then
     * this method calls the security manager's {@code checkPermission}
     * method with a {@code RuntimePermission("getClassLoader")}
     * permission to ensure it's ok to access the class loader for the class.
     *
     * <p>If this object
     * represents a primitive type or void, null is returned.
     *
     * @return  the class loader that loaded the class or interface
     *          represented by this object.
     * @throws SecurityException
     *    if a security manager exists and its
     *    {@code checkPermission} method denies
     *    access to the class loader for the class.
     * @see java.lang.ClassLoader
     * @see SecurityManager#checkPermission
     * @see java.lang.RuntimePermission
     */
    @CallerSensitive
    public ClassLoader getClassLoader() {
        ClassLoader cl = getClassLoader0();
        if (cl == null)
            return null;
        SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            ClassLoader.checkClassLoaderPermission(cl, Reflection.getCallerClass());
        }
        return cl;
    }

返回的是加载这个类的加载器。

Java 规定,类依赖的类也由同一个 ClassLoader 加载,结合双亲委派的加载模式,有些场景则要违反这一规则来达成扩展性。例如 jdk 核心包里的 SPI (Service Provider Interface 服务提供接口)机制 ServiceLoader。按双亲委派的原则 ServiceLoader 方法引用的类也需要由 BootstrapClassLoader 来加载,但事实上确并非如此。这其中,主要是通过线程 Thread 的 ContextClassLoader 来实现的。这里以 MySql 的 JDBC 驱动为例,Driver 是 JDK 提供的接口,mysql 提供的驱动对应的是服务供应商。提供者只需在 JDBC 实现的 Jar 的 META-INF/services/java.sql.Driver 文件里指定实现类的方式暴露驱动提供者。

08d5b38cdd1ae08816b61b4c78a535e3771.jpg

示例代码:

package com.yeyi.coremybatis;

import java.sql.Driver;
import java.util.ServiceLoader;

/**
 * @author yeweigen
 */
public class ServiceLoaderTest {

    public static void main(String[] args) {
        //Thread.currentThread().setContextClassLoader(ServiceLoaderTest.class.getClassLoader().getParent());
        ServiceLoader<Driver> drivers = ServiceLoader.load(Driver.class);
        for (Driver driver : drivers) {
            System.out.println("driver class:" + driver.getClass().getName() +" || loader:" + driver.getClass().getClassLoader());
        }
        System.out.println(ServiceLoaderTest.class.getClassLoader());
        System.out.println(Thread.currentThread().getContextClassLoader());
        System.out.println(ServiceLoader.class.getClassLoader());
    }
}

结果:

driver class:com.mysql.jdbc.Driver || loader:sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$AppClassLoader@18b4aac2
null

运行程序可以看到,ServiceLoader 的类加载器为 null 也就是 BootstrapClassLoader。Thread.currentThread().getContextClassLoader() 代指当前线程的上下文类加载器,默认的就是和当前类一样的 AppClassLoader。for 循环里面打印的也是 AppClassLoader。这里说明使用 BootstrapClassLoader 类加载器加载的 ServiceLoader 类加载了使用 AppClassLoader 类加载器的用户类。

public static <S> ServiceLoader<S> load(Class<S> service) {
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    return ServiceLoader.load(service, cl);
}

这里ServiceLoader.load(MyClass.class)换成这个会更好理解一点,Java 规定,类依赖的类也由同一个 ClassLoader 加载,ServiceLoader类由BootstrapClassLoader加载,那么其依赖的MyClass类,也应该由BootstrapClassLoader加载,但按双亲委派模式其应该由AppClassLoader加载。所以在ServiceLoader.load方法中,先通过ClassLoader cl = Thread.currentThread().getContextClassLoader()得到上下文加载器,从之前Launcher的代码中可以知道他就是AppClassLoader。然后ServiceLoader.load(service, cl),指定了加载MyClass是由AppClassLoader完成的不是由BootstrapClassLoader完成的。一句话总结,当我们想打破类的双亲委派加载模式时,可先Thread.currentThread().setContextClassLoader(),然后Thread.currentThread().getContextClassLoader(),用指定的类加载器加载。

参考:

http://www.importnew.com/24381.html

https://www.jianshu.com/p/f151cedd8f77

转载于:https://my.oschina.net/u/1421030/blog/1922512

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值