深入理解JVM--虚拟机类加载机制(三)

类加载器

  • 类加载器的双亲委托模型的好处
  • 扩展类加载器要点分析
  • 启动类加载器分析
  • 更改系统类加载器
  • Launcher源码分析
  • 自定义系统类加载器源码分析
  • forName方法剖析
  • 线程上下文类加载器
  • 线程上下文类加载器的使用
  • ServiceLoader在SPI中的重要作用

类加载器的双亲委托模型的好处

       1、可以确保Java核心库的类型安全:所有的Java应用都至少引用java.lang.Object 类,也就是说在运行期,java.lang.Object 这个类会被加载到Java虚拟机中;如果这个加载过程是由自定义的类加载器去完成的话,那么很可能就会在JVM中存在多个版本的java.lang.Object类,而且这些类之间还是不兼容的,相互之间不可见的(命名空间在发挥的作用)。
       借助于双亲委托机制,Java核心类库中的类的加载工作都是由启动类加载器来统一完成的,从而确保了Java应用程序所使用的都是同一个版本的Java核心类库,他们之间是相互兼容的。

       2、可以确保java核心类库所提供的类不会被自定义的类所替代。

       3、不同的类加载器可以为相同名称(binary name)的类创建额外的命名空间。相同名称的类可以并存在Java虚拟机中,只需要用不同的类加载器来加载他们即可。不同类加载器所加载的类之间是不兼容的,这就相当于在Java虚拟机内部创建了一个又一个相互隔离的Java类空间,这类技术在很多框架中都得到了应用。

扩展类加载器要点分析

       执行下面的代码:

public class MyTest15 {

    public static void main(String[] args) {
        System.out.println(MyTest15.class.getClassLoader());

        System.out.println(MyTest1.class.getClassLoader());
    }

}

       相信你能很快知道输出结果,输出如下:

sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$AppClassLoader@18b4aac2

       在上一篇文章中说到过,我们可以通过更改对应加载器的加载路径,可以改变类加载器。那么,如果我们将拓展类加载器的加载路径,改为我们当前的目录,输出结果会是什么?

       通过上面的输出可以看到,上面的两个类依然是由AppClassLoader加载。在上一篇文章中说过,如果扩展类加载器其类路径下的class文件,需要是类库的形式。所以先进行下面的打包:

       此时可以看到MyTest1是由扩展类加载器加载。

启动类加载器分析

       在Oracle的Hotspot实现中,系统属性sun.boot.class.path如果修改错了,则运行会出错,提示如下信息:

       内建于JVM中的启动类加载器会加载java.lang.ClassLoader以及其他的Java平台类,当JVM启动时,一块特殊的机器码会运行,它会加载扩展类加载器与系统类加载器,这块特殊的机器码叫做启动类加载器(Bootstrap)。
       启动类加载器并不是Java类,而其他的加载器则是Java类,启动类加载器时特定于平台的机器指令,它负责开启整个加载过程。
       所有类加载器(除了启动类加载器)都被实现为Java类,。不过,总归由一个组件加载第一个Java类加载器,从而让整个加载过程能够顺利进行下去,加载第一个纯Java类加载器就是启动类加载器的职责。
       启动类加载器还会负责加载供JRE正常运行所需要的基本组件,包括hava.unit与java.lang包中的类等。

public static void main(String[] args) {
    System.out.println(ClassLoader.class.getClassLoader());

    // 扩展类加载器与系统类加载器也是由启动类加载器所加载的
    System.out.println(Launcher.class.getClassLoader());

    System.out.println(ClassLoader.getSystemClassLoader());

}

       输出如下:

null
null
sun.misc.Launcher$AppClassLoader@18b4aac2

       可以看到ClasssLoader以及Launcher的类加载器都是系统类加载器,因为扩展类加载器与系统类加载器属于Launcher内部类,我们不能直接访问,而Launcher的类加载会尝试加载其成员。同时可以看到系统类加载器默认是AppClassLoader。

更改系统类加载器

       我们知道系统类加载器默认是AppClassLoader,系统类加载器是默认的自定义加载器的父加载器。
运行下面的代码:

public class MyTest16 {

    public static void main(String[] args) {
        System.out.println(System.getProperty("java.system.class.loader"));

	System.out.println(ClassLoader.getSystemClassLoader());

	System.out.println(new MyTest11("loader").getParent());

    }
}

       输出如下:

null
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$AppClassLoader@18b4aac2


       将系统类加载器改为我们自己定义的加载器出错,这是因为我们需要在自定义类加载器中添加一个ClassLoader的构造器,如下所示:

public MyTest11(ClassLoader parent){
    super(parent);
}


       可以看到自定义ClassLoader的类加载器的父加载器不再是AppClassLoader。
       在我们更改默认的系统类加载器后,其是由AppClassLoader加载

Launcher源码分析

public static ClassLoader getSystemClassLoader() {
    initSystemClassLoader();
    if (scl == null) {
        return null;
    }
    SecurityManager sm = System.getSecurityManager();
    if (sm != null) {
        checkClassLoaderPermission(scl, Reflection.getCallerClass());
    }
    return scl;
}
// 用于确认系统类加载器有没有被赋值
private static boolean sclSet;

// 系统类加载器
private static ClassLoader scl;

private static synchronized void initSystemClassLoader() {
    if (!sclSet) {
        if (scl != null)
            // 如果没有被赋值,但是系统类加载器却不为空,不符合逻辑,所以出错
            throw new IllegalStateException("recursive invocation");
        // 返回一个Launcer的实例    
        sun.misc.Launcher l = sun.misc.Launcher.getLauncher();
        if (l != null) {
            Throwable oops = null;
            // 将AppClassLoader设置为系统类加载器
            scl = l.getClassLoader();
            try {
                // 系统类加载器需不需要进行改变
                scl = AccessController.doPrivileged(
                    new SystemClassLoaderAction(scl));
            } catch (PrivilegedActionException pae) {
                oops = pae.getCause();
                if (oops instanceof InvocationTargetException) {
                    oops = oops.getCause();
                }
            }
            if (oops != null) {
                if (oops instanceof Error) {
                    throw (Error) oops;
                } else {
                    // wrap the exception
                    throw new Error(oops);
                }
            }
        }
        // 表示系统类加载器设置完毕
        sclSet = true;
    }
}

       获取Launcher实例

public Launcher() { 
    // 扩展类加载器 
    Launcher.ExtClassLoader var1;
    try {
        var1 = Launcher.ExtClassLoader.getExtClassLoader();
    } catch (IOException var10) {
        throw new InternalError("Could not create extension class loader", var10);
    }

    try {
    	// 获取AppClassLoader时,将ExtClassLoader传入,这是为了将loader的父加载器设置为ExtClassLoader
        this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
    } catch (IOException var9) {
        throw new InternalError("Could not create application class loader", var9);
    }
    
    // 将刚创建好的应用类加载器设置为当前线程的上下文类加载器
    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);
    }

}

       获取ExtClassLoader 实例

static class ExtClassLoader extends URLClassLoader {
    public static Launcher.ExtClassLoader getExtClassLoader() throws IOException {
        final File[] var0 = getExtDirs();

        try {
            return (Launcher.ExtClassLoader)AccessController.doPrivileged(new PrivilegedExceptionAction<Launcher.ExtClassLoader>() {
                public Launcher.ExtClassLoader run() throws IOException {
                    int var1 = var0.length;

                    for(int var2 = 0; var2 < var1; ++var2) {
                        MetaIndex.registerDirectory(var0[var2]);
                    }

                    return new Launcher.ExtClassLoader(var0);
                }
            });
        } catch (PrivilegedActionException var2) {
            throw (IOException)var2.getException();
        }
    }

       获取系统的java.ext.dirs路径下的所有文件

private static File[] getExtDirs() {
    String var0 = System.getProperty("java.ext.dirs");
    File[] var1;
    if (var0 != null) {
        StringTokenizer var2 = new StringTokenizer(var0, File.pathSeparator);
        int var3 = var2.countTokens();
        var1 = new File[var3];

        for(int var4 = 0; var4 < var3; ++var4) {
            var1[var4] = new File(var2.nextToken());
        }
    } else {
        var1 = new File[0];
    }

    return var1;
}

       通过代码,我么便可以知道,为什么扩展类的加载路径是java.ext.dirs。
       在Launcher的构造器中创建好之后,紧接着开始创建AppClassLoader。创建过程如下:

static class AppClassLoader extends URLClassLoader {
    final URLClassPath ucp = SharedSecrets.getJavaNetAccess().getURLClassPath(this);

    public static ClassLoader getAppClassLoader(final ClassLoader var0) throws IOException {
        final String var1 = System.getProperty("java.class.path");
        final File[] var2 = var1 == null ? new File[0] : Launcher.getClassPath(var1);
        return (ClassLoader)AccessController.doPrivileged(new PrivilegedAction<Launcher.AppClassLoader>() {
            public Launcher.AppClassLoader run() {
                URL[] var1x = var1 == null ? new URL[0] : Launcher.pathToURLs(var2);
                return new Launcher.AppClassLoader(var1x, var0);
            }
        });
    }

       通过代码,可以看到AppClassLoader的加载路径同样读取java.class.path下的所有文件。

自定义系统类加载器源码分析

class SystemClassLoaderAction
    implements PrivilegedExceptionAction<ClassLoader> {
    private ClassLoader parent;

    SystemClassLoaderAction(ClassLoader parent) {
        this.parent = parent;
    }

    public ClassLoader run() throws Exception {
        String cls = System.getProperty("java.system.class.loader");
        if (cls == null) {
            // 如果我们没有设置过java.system.class.loader,那么系统类加载器为AppClassLoader
            return parent;
        }

	// 通过反射调用一个带ClassLoader参数的构造方法
	// 这就是为什么我们自定义类加载器设置为SystemClassLoader时,需要有一个ClassLoader参数的构造方法
        Constructor<?> ctor = Class.forName(cls, true, parent)
            .getDeclaredConstructor(new Class<?>[] { ClassLoader.class });
            
        // 系统类加载器的父加载器设置为parent
        ClassLoader sys = (ClassLoader) ctor.newInstance(
            new Object[] { parent });
        // 将用户自定义的类加载器设置为上下文类加载器    
        Thread.currentThread().setContextClassLoader(sys);
        return sys;
    }
}

forName方法剖析

// name 全限定类名
// initialize 是否进行初始化
// loader 加载指定类的类加载器
public static Class<?> forName(String name, boolean initialize,
                               ClassLoader loader)
    throws ClassNotFoundException
{
    Class<?> caller = null;
    SecurityManager sm = System.getSecurityManager();
    if (sm != null) {
        // 获取调用forName()方法的类的class对象        
        caller = Reflection.getCallerClass();
        if (sun.misc.VM.isSystemDomainLoader(loader)) {
            // 获取caller的classLoader对象
            ClassLoader ccl = ClassLoader.getClassLoader(caller);
            if (!sun.misc.VM.isSystemDomainLoader(ccl)) {
                sm.checkPermission(
                    SecurityConstants.GET_CLASSLOADER_PERMISSION);
            }
        }
    }
    return forName0(name, initialize, loader, caller);
}

       该方法的作用是返回使用给定的类加载器加载给定字符串名称的类或接口的Class对象

       Class.forName(“Foo”) = Class.forName(“Foo”, true, this.getClass().getClassLoader())

线程上下文类加载器

public class MyTest17 {

    public static void main(String[] args) {
        System.out.println(Thread.currentThread().getContextClassLoader());
        // sun.misc.Launcher$AppClassLoader@18b4aac2
        System.out.println(Thread.class.getClassLoader());
        // null
    }

}
  • 当前类加载器(Current ClassLoader)
    每个类都会使用自己的类加载器(即加载自身的类加载器)来去加载其他类(指的是所依赖的类),
    如果ClassX引用了ClassY,那么ClassX的类加载器就会去加载ClassY(前提是ClassY尚未被加载)

  • 线程上下文类加载器(Context ClassLoader)
    线程上下文类加载器从JDK1.2开始引入的,类Thread中的getContextClassLoader()与setContextClassLoader(ClassLoader cl)分别用来获取和设置上下文类加载器。
    如果没有通过setContextClassLoader(cl) 设置的话,线程将继承其父线程的上下文类加载器。
    Java应用运行时的初始化线程的上下文类加载器就是系统类加载器。在线程中运行的代码可以通过该类加载器来加载类于资源。

  • 线程上下文类加载器的重要性
    SPI(Service Provider Interface)
    父ClassLoader可以使用当前线程Thread.currentThread().getContextClassLoader()所指定的classloader加载的类。这就改变了父Classloader不能使用子ClassLoader或是其他没有直接父子关系的classLoader加载的类的情况。即改变了双亲委托模型

  • 在双亲委托模型下,类加载器是由下至上的,即下层的类加载器会委托上层进行加载。但是对于SPI来说,有些接口时Java核心库所提供的,而Java核心库是由启动类加载器来加载的,而这些接口的实现却来自于不同的jar包(厂商提供),Java的启动类加载器是不会加载其他来源的jar包,这样传统的双亲委托模型就无法满足SPI的要求(这样导致接是启动类加载,而实现是由系统类加载器或应用类加载器加载)。而通过给当前线程设置上下文类加载器,就可以由设置的上下文类加载器来实现对于接口实现类的加载。

       以JDBC为例,JDBC的接口是启动类加载器加载,但是JDBC的接口的实现却是由数据库厂商所提供。因为JDBC的接口和其实现存在依赖关系,而其实现的jar包被我们放到了classpath,这样便导致了启动类加载器无法加载其实现,从而我们无法使用JDBC,所以传统的双亲委托机制便失效了。

public class MyTest18 implements Runnable{

    private Thread thread;

    public MyTest18(){
        thread = new Thread(this);
        thread.start();
    }

    @Override
    public void run() {
        ClassLoader classLoader = this.thread.getContextClassLoader();

        this.thread.setContextClassLoader(classLoader);

        System.out.println("Class: " + classLoader.getClass());
        System.out.println("Parent: " + classLoader.getParent().getClass());
    }

    public static void main(String[] args) {
        new MyTest18();
    }
}

       该程序的输出通过前面的分析,想必很快能知道答案,输出如下:

Class: class sun.misc.Launcher$AppClassLoader
Parent: class sun.misc.Launcher$ExtClassLoader

线程上下文类加载器的使用

       线程上下文类加载的一般使用模式 (获取 - 使用 - 还原)

       伪代码如下:

      try{
          // targetThreadContextClassLoader将要使用的ClassLoader设置为线程上下文类加载器
         Thread.currentThread().setContextClassLoader(targetThreadContextClassLoader);
          doSomething();
     }finally{
          Thread.currentThread().setClassLoader(classLoader);
     }

       doSomething()里面调用了Thread.currentThread.getContextClassLoader(),获取当前线程的上下文类加载器做某些事情。
        如果一个类由类加载器A加载,那么这个类的类加载器会加载这个类的依赖类(前提依赖类没有被加载)

       ContextClassLoader的作用是为了破坏Java的类加载器委托机制。
       当高层提供了统一的接口让低层去实现,同时又要在高层加载(或实例化)低层的类时,就必须要通过线程上下文类加载器来帮助高层的ClassLoader找到并加载该类。

ServiceLoader在SPI中的重要作用

       ServiceLoader是一个简单的服务提供商加载设施。
       服务是一组众所周知的接口和(通常是抽象的)类,例如JDBC中的接口。而服务提供商是服务的特定实现,例如数据库的驱动。提供程序中的类通常实现接口并实例化服务本身中定义的类。服务提供者可以以扩展的形式安装在Java平台的实现中,即,将jar文件放入任何通常的扩展目录中(例如我们将mysql驱动放到了classpath)。
       出于加载的目的,服务由单个类型表示,即单个接口或抽象类。服务的提供者提供一个或多个具体类,这些类使用特定于提供者的数据和代码来扩展此服务类型。提供程序类通常不是整个提供程序本身,而是包含足够信息的代理,以确定提供程序是否能够满足特定请求以及可以按需创建实际提供程序的代码。提供程序类的详细信息往往是高度特定于服务的;没有单个类或接口可以统一它们,所以这里没有定义这样的类型。此工具强制执行的唯一要求是提供程序类必须具有零参数构造函数,以便可以在加载期间实例化它们。
       通过在资源目录META-INF / services 中放置提供者配置文件来标识服务提供者。文件名是服务类型的完全限定的二进制名称,即由启动类加载器加载接口。该文件包含具体提供程序类的完全限定二进制名称列表,每行一个。
       提供者可以按需提供。服务加载器维护到目前为止已加载的提供程序的缓存。每次调用iterator方法都会返回一个迭代器,它首先以实例化的顺序生成缓存的所有元素,然后懒惰地定位并实例化任何剩余的提供者,依次将每个提供者添加到缓存中。可以通过reload方法清除缓存。

       以JDBC为例,执行下面的代码:

public class MyTest19 {
	public static void main(String[] args) {
	    ServiceLoader<Driver> loader = ServiceLoader.load(Driver.class);
	    Iterator<Driver> iterator = loader.iterator();
	
	    while (iterator.hasNext()){
	        Driver driver = iterator.next();
	        System.out.println("driver: "+driver.getClass() +",loader: "+driver.getClass().getClassLoader());
	    }
	
	    System.out.println("当前线程上下文类加载器:"+Thread.currentThread().getContextClassLoader());
	    System.out.println("ServiceLoader的类加载器:"+ServiceLoader.class.getClassLoader());
	}
}	
	

       输出如下:

driver: class com.mysql.jdbc.Driver,loader: sun.misc.Launcher$AppClassLoader@18b4aac2
driver: class com.mysql.fabric.jdbc.FabricMySQLDriver,loader: sun.misc.Launcher$AppClassLoader@18b4aac2
当前线程上下文类加载器:sun.misc.Launcher$AppClassLoader@18b4aac2
ServiceLoader的类加载器:null

       通过输出可以看到,ServiceLoader是由启动类加载器加载,但是其内部的Driver却是由应用类加载器加载。而且我们只提供了一个Driver接口,却可以找到数据库厂商提供的驱动。

       可以看到,迭代器返回的对象与对应文件对象声明的顺序相同。

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

public static <S> ServiceLoader<S> load(Class<S> service,ClassLoader loader){
    return new ServiceLoader<>(service, loader);
}

// 清除此加载程序的提供程序缓存,以便重新加载所有提供程序。
public void reload() {
    providers.clear();
    // 延迟加载
    lookupIterator = new LazyIterator(service, loader);
}

// 缓存
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();

private class LazyIterator
    implements Iterator<S>
{

    Class<S> service;
    ClassLoader loader;
    Enumeration<URL> configs = null;
    Iterator<String> pending = null;
    String nextName = null;
 
    // 将service对象和上下文类加载器赋值给其内部类的成员变量
    private LazyIterator(Class<S> service, ClassLoader loader) {
        this.service = service;
        this.loader = loader;
    }

       程序运行时,首先需要加载ServiceLoader,ServiceLoader由启动类加载器加载。而对于其load()方法也应该是由启动类加载器加载,但是启动类加载器并不能够找到类路径下的实现,所以此时需要线程上下文类加载器通过应用类加载器加载。

       迭代器进行遍历时的主要加载代码如下:

private S nextService() {
    if (!hasNextService())
        throw new NoSuchElementException();
    String cn = nextName;
    nextName = null;
    Class<?> c = null;
    try {
    	// cn 为META-INF/ervices/文件中的类名
        c = Class.forName(cn, false, loader);
    } catch (ClassNotFoundException x) {
        fail(service,
             "Provider " + cn + " not found");
    }
    if (!service.isAssignableFrom(c)) {
        fail(service,
             "Provider " + cn  + " not a subtype");
    }
    try {
        S p = service.cast(c.newInstance());
        providers.put(cn, p);
        return p;
    } catch (Throwable x) {
        fail(service,
             "Provider " + cn + " could not be instantiated",
             x);
    }
    throw new Error();          // This cannot happen
}

       当我们将上下文类加载器该为扩展类加载器,那么会发生什么?

public static void main(String[] args) {
    Thread.currentThread().setContextClassLoader(MyTest19.class.getClassLoader().getParent());

    ServiceLoader<Driver> loader = ServiceLoader.load(Driver.class);
    Iterator<Driver> iterator = loader.iterator();

    while (iterator.hasNext()){
        Driver driver = iterator.next();
        System.out.println("driver: "+driver.getClass() +",loader: "+driver.getClass().getClassLoader());
    }

    System.out.println("当前线程上下文类加载器:"+Thread.currentThread().getContextClassLoader());
    System.out.println("ServiceLoader的类加载器:"+ServiceLoader.class.getClassLoader());
}

输出如下:

当前线程上下文类加载器:sun.misc.Launcher$ExtClassLoader@1540e19d
ServiceLoader的类加载器:null

可以看到,并没有执行循环,因为上下文类加载器为扩展类加载器,并不能找到Classpath下的实现,所以不执行。

以JDBC为例:https://www.2cto.com/kf/201609/551006.html

你可能感兴趣的:(深入理解JVM)