JVM探索之类加载

199 阅读11分钟

前言

Java虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这个过程被称作虚拟机的类加载机制。这篇文章主要JVM的不同类加载器、双亲委派机制来介绍类加载机制

JVM类加载机制

  • 全盘负责:当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入
  • 父类委托:先让父类加载器试图加载该类,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类
  • 缓存机制:缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。这就是为什么修改了Class后,必须重启JVM,程序的修改才会生效

类加载器

JVM设计者把类加载阶段中的通过类全名来获取定义此类的二进制字节流,这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类。实现这个动作的代码模块称为“类加载器”。对于任何一个类,都需要由加载它的类加载器和这个类来确立其在JVM中的唯一性。也就是说,两个类来源于同一个Class文件,并且被同一个类加载器加载,这两个类才相等

类加载的三种方式

  • 命令行启动应用时候由JVM初始化加载
  • 通过Class.forName()方法动态加载
  • 通过ClassLoader.loadClass()方法动态加载

类加载器的分类

在开发人员角度来看,类加载器有四类,三种是系统提供的,另一个是自定义的

  1. 启动类加载器(Bootstrap ClassLoader)

  2. 扩展类加载器(Extension ClassLoader)

  3. 应用程序类加载器(Application ClassLoader)

  4. 自定义类加载器(User ClassLoader)

image.png

类加载器介绍

  • BootStrap ClassLoader

BootStrap ClassLoader被称为启动类加载器或根类加载器。引导类加载器是使用C++语言实现的,是JVM自身的一部分,我们不能直接调用这个类加载器。它用来加载Java核心类库,如:JAVA_HOME/jre/lib/rt.jarresources.jarsun.boot.class.path路径下的包,用于提供jvm运行所需的包

注意:因为JVM是通过全限定名加载类库的,所以,如果你的文件名不被虚拟机识别,就算你把jar包丢入到lib目录下,引导类加载器也并不会加载它。出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类文件

  • Ext ClassLoader

这个类加载器是由Java编写,我们可以在程序中操作这个类加载器,他负责加载java.ext.dirs以及JAVA_HOME/jre/lib/ext目录下的jar包,我们将自己的jar包放到这个目录下也会被这个类加载器加载。他的父加载器就是BootStrap ClassLoader

  • App ClassLoader

App ClassLoader是我们系统默认的类加载器,主要加载我们的classpath目录下的jar包,他的父加载器是Ext Classloader。我们可以在代码中使用ClassLoader#getSystemClassLoader()获取并操作这个加载器

  • User ClassLoader

User ClassLoader是我们自定义的类加载器,我们有一些特殊的加载需求的时候可以自己编写类加载器,需要我们的类继承java.lang.ClassLoader类,重写findClass()方法

显示加载和隐式加载

显式加载指的是开发者手动通过调用ClassLoader加载一个类,比如Class.forName(name)或obj.getClass().getClassLoader().loadClass()方式加载class对象。而隐式加载则是指不会在程序中明确的指定加载某个类,属于被动式加载,比如在加载某个类时,该类中引用了另外一个类的对象时,JVM就会去自动加载另外一个类,而这种被动加载方式就被称为“隐式加载”

类加载器加载顺序

BootStrap类加载器是在JVM启动时初始化的,它会负责加载ExtClassLoader,并将其父加载器设置为BootstrapClassLoader。BootstrapClassLoader加载完ExtClassLoader后会接着加载AppClassLoader系统类加载器,并将其父加载器设置为ExtClassLoader拓展类加载器。而自己定义的类加载器会由系统类加载器加载,加载完成后,AppClassLoader会成为它们的父加载器。这里要注意这几个加载器之间的父子关系并不是我们Java中继承关系,BootStrap ClassLoader是C++代码编写,而其他的都是直接或间接继承自java.lang.ClassLoader

下面来看一段代码示例

public class ClassLoaderDemo extends ClassLoader{

    public static void main(String[] args) {
        ClassLoaderDemo classLoaderDemo = new ClassLoaderDemo();

        System.out.println("User ClassLoader is :" + classLoaderDemo);
        System.out.println("User ClassLoader parent ClassLoader is :" + classLoaderDemo.getParent());
        System.out.println("Java Default ClassLoader is :" + ClassLoader.getSystemClassLoader());
        System.out.println("Java Default ClassLoader parent ClassLoader is :" + ClassLoader.getSystemClassLoader().getParent());
        System.out.println("Ext ClassLoader parent ClassLoader is :" + ClassLoader.getSystemClassLoader().getParent().getParent());
    }
}

执行上面代码运行的结果是

User ClassLoader is :com.wjx.atlantis.prometheus.jvm.ClassLoaderDemo@46f7f36a
User ClassLoader parent ClassLoader is :sun.misc.Launcher$AppClassLoader@18b4aac2
Java Default ClassLoader is :sun.misc.Launcher$AppClassLoader@18b4aac2
Java Default ClassLoader parent ClassLoader is :sun.misc.Launcher$ExtClassLoader@421faab1
Ext ClassLoader parent ClassLoader is :null

上面运行的结果也验证了Java类加载之间的父子关系,这个要注意的是BootStrap ClassLoader是C++编写的所以在我们上面的代码中打印出来的结果是null

每个类加载器都拥有一个自己的命名空间,命名空间的作用是用于存储被自身加载过的所有类的全限定名,子类加载器查找父类加载器是否加载过一个类时,就是通过类的权限定名在父类的命名空间中进行匹配。而Java虚拟机判断两个类是否相同的基准就是通过ClassLoaderId + PackageName + ClassName进行判断,也就代表着,Java程序运行过程中,是允许存在两个包名和类名完全一致的class的,只需要使用不同的类加载器加载即可,这也就是Java类加载器存在的隔离性问题,而Java为了解决这个问题,JVM引入了双亲委派机制

双亲委派机制

在上面提到的JVM类加载的机制中有一个父类加载,由此出现一个双亲委派机制。正如上图所示的类加载器之间的这种层次关系,就称为类加载器的双亲委派模型(Parent Delegation Model)。该模型要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。子类加载器和父类加载器不是以继承(Inheritance)的关系来实现,而是通过组合(Composition)关系来复用父加载器的代码

双亲委派模型的工作过程为:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的加载器都是如此,因此所有的类加载请求都会传给顶层的启动类加载器,只有当父加载器反馈自己无法完成该加载请求(该加载器的搜索范围中没有找到对应的类)时,子加载器才会尝试自己去加载。使用这种模型来组织类加载器之间的关系的好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系。例如java.lang.Object类,无论哪个类加载器去加载该类,最终都是由启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。否则的话,如果不使用该模型的话,如果用户自定义一个java.lang.Object类且存放在classpath中,那么系统中将会出现多个Object类,应用程序也会变得很混乱。如果我们自定义一个rt.jar中已有类的同名Java类,会发现JVM可以正常编译,但该类永远无法被加载运行

双亲委派机制实现原理

除了BootStrapClassLoader是使用C++代码编写的其他的类加载器都是java.lang.ClassLoader的子类,我们自定义的类加载器也是需要继承自这个类

Ext ClassLoader的源码

static class ExtClassLoader extends URLClassLoader {
        private static volatile ExtClassLoader instance;

        public static ExtClassLoader getExtClassLoader() throws IOException {
            if (instance == null) {
                Class var0 = ExtClassLoader.class;
                synchronized(ExtClassLoader.class) {
                    if (instance == null) {
                        instance = createExtClassLoader();
                    }
                }
            }

            return instance;
        }

        private static ExtClassLoader createExtClassLoader() throws IOException {
            try {
                return (ExtClassLoader)AccessController.doPrivileged(new PrivilegedExceptionAction<ExtClassLoader>() {
                    public ExtClassLoader run() throws IOException {
                        File[] var1 = Launcher.ExtClassLoader.getExtDirs();
                        int var2 = var1.length;

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

                        return new ExtClassLoader(var1);
                    }
                });
            } catch (PrivilegedActionException var1) {
                throw (IOException)var1.getException();
            }
        }

        void addExtURL(URL var1) {
            super.addURL(var1);
        }

        public ExtClassLoader(File[] var1) throws IOException {
            super(getExtURLs(var1), (ClassLoader)null, Launcher.factory);
            SharedSecrets.getJavaNetAccess().getURLClassPath(this).initLookupCache(this);
        }

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

        private static URL[] getExtURLs(File[] var0) throws IOException {
            Vector var1 = new Vector();

            for(int var2 = 0; var2 < var0.length; ++var2) {
                String[] var3 = var0[var2].list();
                if (var3 != null) {
                    for(int var4 = 0; var4 < var3.length; ++var4) {
                        if (!var3[var4].equals("meta-index")) {
                            File var5 = new File(var0[var2], var3[var4]);
                            var1.add(Launcher.getFileURL(var5));
                        }
                    }
                }
            }

            URL[] var6 = new URL[var1.size()];
            var1.copyInto(var6);
            return var6;
        }

        public String findLibrary(String var1) {
            var1 = System.mapLibraryName(var1);
            URL[] var2 = super.getURLs();
            File var3 = null;

            for(int var4 = 0; var4 < var2.length; ++var4) {
                URI var5;
                try {
                    var5 = var2[var4].toURI();
                } catch (URISyntaxException var9) {
                    continue;
                }

                File var6 = Paths.get(var5).toFile().getParentFile();
                if (var6 != null && !var6.equals(var3)) {
                    String var7 = VM.getSavedProperty("os.arch");
                    File var8;
                    if (var7 != null) {
                        var8 = new File(new File(var6, var7), var1);
                        if (var8.exists()) {
                            return var8.getAbsolutePath();
                        }
                    }

                    var8 = new File(var6, var1);
                    if (var8.exists()) {
                        return var8.getAbsolutePath();
                    }
                }

                var3 = var6;
            }

            return null;
        }

        private static AccessControlContext getContext(File[] var0) throws IOException {
            PathPermissions var1 = new PathPermissions(var0);
            ProtectionDomain var2 = new ProtectionDomain(new CodeSource(var1.getCodeBase(), (Certificate[])null), var1);
            AccessControlContext var3 = new AccessControlContext(new ProtectionDomain[]{var2});
            return var3;
        }

        static {
            ClassLoader.registerAsParallelCapable();
            instance = null;
        }
    }

App ClassLoader源码

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<AppClassLoader>() {
                public AppClassLoader run() {
                    URL[] var1x = var1 == null ? new URL[0] : Launcher.pathToURLs(var2);
                    return new AppClassLoader(var1x, var0);
                }
            });
        }

        AppClassLoader(URL[] var1, ClassLoader var2) {
            super(var1, var2, Launcher.factory);
            this.ucp.initLookupCache(this);
        }

        public Class<?> loadClass(String var1, boolean var2) throws ClassNotFoundException {
            int var3 = var1.lastIndexOf(46);
            if (var3 != -1) {
                SecurityManager var4 = System.getSecurityManager();
                if (var4 != null) {
                    var4.checkPackageAccess(var1.substring(0, var3));
                }
            }

            if (this.ucp.knownToNotExist(var1)) {
                Class var5 = this.findLoadedClass(var1);
                if (var5 != null) {
                    if (var2) {
                        this.resolveClass(var5);
                    }

                    return var5;
                } else {
                    throw new ClassNotFoundException(var1);
                }
            } else {
                return super.loadClass(var1, var2);
            }
        }

        protected PermissionCollection getPermissions(CodeSource var1) {
            PermissionCollection var2 = super.getPermissions(var1);
            var2.add(new RuntimePermission("exitVM"));
            return var2;
        }

        private void appendToClassPathForInstrumentation(String var1) {
            assert Thread.holdsLock(this);

            super.addURL(Launcher.getFileURL(new File(var1)));
        }

        private static AccessControlContext getContext(File[] var0) throws MalformedURLException {
            PathPermissions var1 = new PathPermissions(var0);
            ProtectionDomain var2 = new ProtectionDomain(new CodeSource(var1.getCodeBase(), (Certificate[])null), var1);
            AccessControlContext var3 = new AccessControlContext(new ProtectionDomain[]{var2});
            return var3;
        }

        static {
            ClassLoader.registerAsParallelCapable();
        }
    }

上面是ExtClassLoader和AppClassLoader的源码,可以看到他们都是通过继承URLClassLoader来间接继承ClassLoader,我们知道在类加载器加载过程中最重要的就是loadClass(),ExtClassLoader没有重写这个方法,AppClassLoader虽然重写了这个方法但是本质还是直接调用super.loadClass()。也就是他们在尝试调用这个方法加载类的时候其实都是调用父类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;
        }
    }

从上面代码中所有类加载器使用loadClass()进行类加载时都会先判断自己的父类加载器是否存在,如果存在则使用父类类加载器进行加载直到父类类加载器为null,因为最顶层的父类加载器BootStrapClassLoader是C++编写的,在我们Java程序中就是null,此刻就使用BootStrapClassLoader的native方法进行加载,当发现无法加载时则会退出由子加载器进行尝试加载,整个过程与栈的方式类似

上面说了每个加载器加载的时候会判断自己的父类加载器是否存在,现在我们来看下这个父类加载器是怎么赋值的

首先,他们都是继承自java.lang.ClassLoader中的变量private final ClassLoader parent,当ExtClassLoader和AppClassLoader在构造方法中将自己的parent传入到构造方法中,从而确定了这个类加载器的父类加载器是什么,这里我们要注意,我们自定义的类加载器为什么是AppClassLoader,这是因为当我们自定义一个类加载器同时没有重写构造方法时,会调用父类的构造方法,在我们的ClassLoader中存在一个构造方法设置父类加载器

    protected ClassLoader() {
        this(checkCreateClassLoader(), getSystemClassLoader());
    }

第二个参数就是parent,被设置为SystemClassLoader,在上面代码中我们已经知道这个值就是AppClassLoader,我们可以通过重写构造方法的方式来设置父类加载器,下面的代码就是一个实例

public class ClassLoaderDemo extends ClassLoader{

    ClassLoaderDemo(ClassLoader parent){
        super(parent);
    }

    public static void main(String[] args) {
        ClassLoaderDemo classLoaderDemo = new ClassLoaderDemo(ClassLoader.getSystemClassLoader().getParent());

        System.out.println("User ClassLoader is :" + classLoaderDemo);
        System.out.println("User ClassLoader parent ClassLoader is :" + classLoaderDemo.getParent());
        System.out.println("Java Default ClassLoader is :" + ClassLoader.getSystemClassLoader());
        System.out.println("Java Default ClassLoader parent ClassLoader is :" + ClassLoader.getSystemClassLoader().getParent());
        System.out.println("Ext ClassLoader parent ClassLoader is :" + ClassLoader.getSystemClassLoader().getParent().getParent());
    }
}

这个代码和上面的实例唯一的不同就是我们重写了构造方法,并且手动设置了父类加载器,下面就是打印的结果

User ClassLoader is :com.wjx.atlantis.prometheus.jvm.ClassLoaderDemo@46f7f36a
User ClassLoader parent ClassLoader is :sun.misc.Launcher$ExtClassLoader@421faab1
Java Default ClassLoader is :sun.misc.Launcher$AppClassLoader@18b4aac2
Java Default ClassLoader parent ClassLoader is:sun.misc.Launcher$ExtClassLoader@421faab1
Ext ClassLoader parent ClassLoader is :null

从这个结果可以侧面验证我们的结论,我们可以通过重写构造方法来设置我们的自定义类加载器的父类加载器

双亲委派机制的作用

  1. 防止重复加载同一个.class。通过委托去向上面问一问,加载过了,就不用再加载一遍。保证数据安全
  2. 保证核心.class不能被篡改。通过委托方式,不会去篡改核心.class,即使篡改也不会去加载,即使加载也不会是同一个.class对象了。不同的加载器加载同一个.class也不是同一个Class对象。这样保证了Class执行安全

双亲委派机制的打破

双亲委派一共有3种类型的模式打破

  1. 重写loadClass()
  2. JDBC等SPI的加载方式
  3. Tomcat热部署的方式

双亲委派打破方式分析

重写loadClass()

由于双亲委派的原理就是所有的类加载器都执行java.ClassLoader中loadClass()这个方法中就写了双亲委派的逻辑,一旦我们重写了这个方法就不会执行java.ClassLoader中loadClass()的双亲委派逻辑

JDBC的SPI加载方式打破双亲委派分析

在JDBC4.0之前我们要加载JDBC驱动一般是通过class.forName或者System.setProperty来设置驱动

//传统加载方式1
Class.forName("com.mysql.jdbc.Driver");
Connection connection = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306","root","123456")

//传统加载方式2
System.setProperty("jdbc.drivers","com.mysql.jdbc.Driver")
Connection connection = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306","root","123456")

但是JDBC4.0之后就不需要再调用Class.forName来加载驱动程序了,我们只需要把驱动的jar包放到工程的类加载路径里,那么驱动就会被自动加载,这个自动加载采用的技术叫做SPI(service provider interface),数据库驱动厂商也都做了更新。可以看一下jar包里面的META-INF/services目录,里面有一个java.sql.Driver的文件,文件里面包含了驱动的全路径名

使用上,我们只需要通过下面一句就可以创建数据库的连接:

Connection con = DriverManager.getConnection(url , username , password );   

因为类加载器受到加载范围的限制,在某些情况下父类加载器无法加载到需要的文件,这时候就需要委托子类加载器去加载class文件

JDBC的Driver接口定义在JDK中,其实现由各个数据库的服务商来提供,比如MySQL驱动包。DriverManager 类中要加载各个实现了Driver接口的类,然后进行管理,但是DriverManager位于 JAVA_HOME中jre/lib/rt.jar 包,由BootStrap类加载器加载,而其Driver接口的实现类是位于服务商提供的 Jar 包,根据类加载机制,当被装载的类引用了另外一个类的时候,虚拟机就会使用装载第一个类的类装载器装载被引用的类。 也就是说BootStrap类加载器还要去加载jar包中的Driver接口的实现类。我们知道,BootStrap类加载器默认只负责加载 JAVA_HOME中jre/lib/rt.jar 里所有的class,所以需要由子类加载器去加载Driver实现,这就破坏了双亲委派模型

查看DriverManager类的源码,看到在使用DriverManager的时候会触发其静态代码块,调用 loadInitialDrivers() 方法,并调用ServiceLoader.load(Driver.class) 加载所有在META-INF/services/java.sql.Driver 文件里边的类到JVM内存,完成驱动的自动加载

     static {
        loadInitialDrivers();
        println("JDBC DriverManager initialized");
    }

    private static void loadInitialDrivers() {
        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {
                ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
                Iterator<Driver> driversIterator = loadedDrivers.iterator();

                try{
                    while(driversIterator.hasNext()) {
                        driversIterator.next();
                    }
                } catch(Throwable t) {
                // Do nothing
                }
                return null;
            }
        });

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

这个子类加载器是通过 Thread.currentThread().getContextClassLoader() 得到的线程上下文加载器

public Launcher() {
    ...
    try {
        this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
    } catch (IOException var9) {
        throw new InternalError("Could not create application class loader", var9);
    }
    Thread.currentThread().setContextClassLoader(this.loader);
    ...
}

可以看到,在 sun.misc.Launcher 初始化的时候,会获取AppClassLoader,然后将其设置为上下文类加载器,所以线程上下文类加载器默认情况下就是Application ClassLoader

Tomcat破坏双亲委派

Tomcat对于双亲委派的破坏是由于用户对程序动态性的追求导致的,动态性是指代码热替换、模块热部署等。OSGI实现模块化热部署的关键就是它自定义的类加载器机制的实现。每一个程序模块(Bundle)都有一个自己的类加载器,当需要更换一个模块时,就把模块连同类加载器一起换掉以实现代码的热替换。在OSGI环境中,类加载器不再是双亲委派模型中的树状结构,而是进一步发展为更加复杂的网状结构,当受到类加载请求时,OSGI将会按照下面的顺序进行类搜索

  1. 将java.*开头的类委派给父类加载器加载
  2. 否则,将委派列表名单内的类委派给父类加载器加载
  3. 否则,将import列表中的类委派给export这个类的Bundle的类加载器加载
  4. 否则,查找当前Bundle的ClassPath,使用自己的类加载器加载
  5. 否则,查找类是否在自己的Fragment Bundle,如果在,则委派给Fragment Bundle的类加载器加载
  6. 否则,查抄Dynamic import列表的Bundle,委派给对应Bundle的类加载器加载
  7. 否则,类加载器失败

image.png

Tomcat如何破坏双亲委派模型的呢?

每个Tomcat的webappClassLoader加载自己的目录下的class文件,不会传递给父类加载器。

事实上,tomcat之所以造了一堆自己的classloader,大致是出于下面三类目的:

  1. 对于各个 webapp中的 class和 lib,需要相互隔离,不能出现一个应用中加载的类库会影响另一个应用的情况,而对于许多应用,需要有共享的lib以便不浪费资源。
  2. 与 jvm一样的安全性问题。使用单独的 classloader去装载 tomcat自身的类库,以免其他恶意或无意的破坏;
  3. 热部署。相信大家一定为 tomcat修改文件不用重启就自动重新装载类库而惊叹吧

SPI

SPI (Service Provider Interface),主要是用来在框架中使用的,最常见和莫过于我们在访问数据库时候用到的java.sql.Driver接口了。

你想一下首先市面上的数据库五花八门,不同的数据库底层协议的大不相同,所以首先需要定制一个接口,来约束一下这些数据库,使得 Java 语言的使用者在调用数据库的时候可以方便、统一的面向接口编程。

数据库厂商们需要根据接口来开发他们对应的实现,那么问题来了,真正使用的时候到底用哪个实现呢?从哪里找到实现类呢?

这时候 Java SPI 机制就派上用场了,不知道到底用哪个实现类和找不到实现类,我们告诉它不就完事了呗。

大家都约定好将实现类的配置写在一个地方,然后到时候都去哪个地方查一下不就知道了吗?

Java SPI 就是这样做的,约定在 Classpath 下的 META-INF/services/ 目录里创建一个以服务接口命名的文件,然后文件里面记录的是此 jar 包提供的具体实现类的全限定名

这样当我们引用了某个 jar 包的时候就可以去找这个 jar 包的 META-INF/services/ 目录,再根据接口名找到文件,然后读取文件里面的内容去进行实现类的加载与实例化。

比如我们看下 MySQL 是怎么做的

image.png

再来看一下文件里面的内容

image.png

总结

JVM的类加载器主要BootStrap、Ext、App和自定义类加载器,他们分别有负责加载的目录,JVM的类加载器之间存在父子关系但不是我们Java理解的继承关系而是一种组合,双亲委派机制就依赖于此。但是随着SPI等技术的出现双亲委派也出现被打破的情况,我们在开发中如果需要自定义类加载器的话是不建议重写loadClass()方法,因为这样双亲委派就被打破了