JVM 类加载子系统与 SPI 详解

简介: 本文将详细描述 JVM 类加载子系统,与 SPI 实现核心原理。

类加载器


在Java虚拟机的角度来看,只存在两种不同的类加载器:一种是启动类加载器(Bootstrap ClassLoader),这个类加载器使用C++语言实现,是虚拟机自身的一部分;另外一种就是其他所有 的类加载器,这些类加载器都由Java语言实现,独立存在于虚拟机外部,并且全都继承自抽象类 java.lang.ClassLoader。 类加载器结构如下图所示:



image.png


1.启动类加载器


启动类加载器(Bootstrap Class Loader):这个类加载器负责加载存放在\lib目录,或者被-Xbootclasspath参数所指定的路径中存放的,而且是Java虚拟机能够识别的(按照文件名识别,如rt.jar、tools.jar,名字不符合的类库即使放在lib目录中也不会被加载)类 库加载到虚拟机的内存中。启动类加载器无法被Java程序直接引用,用户在编写自定义类加载器时, 如果需要把加载请求委派给引导类加载器去处理,那直接使用null代替即可,下面是java.lang.Class#getClassLoader()方法的代码片段,其中的注释和代码实现都明确地说明了以null值来代表引导类加载器的约定规则。


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


2.拓展类加载器


扩展类加载器(Extension Class Loader):这个类加载器是在类sun.misc.Launcher$ExtClassLoader 中以Java代码的形式实现的。它负责加载\lib\ext目录中,或者被java.ext.dirs系统变量所 指定的路径中所有的类库。根据“扩展类加载器”这个名称,就可以推断出这是一种Java系统类库的扩 展机制,JDK的开发团队允许用户将具有通用性的类库放置在ext目录里以扩展Java SE的功能,在JDK 9之后,这种扩展机制被模块化带来的天然的扩展能力所取代。由于扩展类加载器是由Java代码实现的,开发者可以直接在程序中使用扩展类加载器来加载Class文件。 如果是 openjdk 文件位置 jdk/src/share/classes/sun/misc/Launcher.java


3.应用类加载器


应用程序类加载器(Application Class Loader):这个类加载器由 sun.misc.Launcher$AppClassLoader来实现。由于应用程序类加载器是ClassLoader类中的getSystemClassLoader()方法的返回值,所以有些场合中也称它为“系统类加载器”。它负责加载用户类路径 (claspath:)上所有的类库,开发者同样可以直接在代码中使用这个类加载器。如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器,也可以通过 java.class.path 进行指定。


4.自定义加载器


继承类 java.lang.ClassLoader ,下面我给出一个简单的例子


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


什么是双亲委派


如果一个类加载器收到了加载某个类的请求, 则该类加载器并不会去加载该类, 而是把这个请求委派给父类加载器,每一个层次的类加载器都是如此, 因此所有的类加载请求最终都会传送到顶端的启动类加载器; 只有当父类加载器在其搜索范围内无法找到所需的类, 并将该结果反馈给子类加载器, 子类加载器会尝试去自己加载。


总结:在类加载的过程中首先不会自己去加载该类,会尝试让父类去加载,如果父类不能加载再尝试自己加载;这样的目的是保证同一个类能够始终被同一个类加载器去加载。


打破双亲委派


双亲委派的过程,中会存在一个问题就是Java 在rt.jar 包中定义了非常的多的接口信息,需要第三方厂家或者使用者自己去实现,这样就会出现无法找到实现类的问题,如果需要解决这个两个问题,我们就可以采用 spi 方案来处理。


1. 自定义类加载器


继承类 java.lang.ClassLoader ,下面我给出一个简单的例子


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


2. SPI 机制


是一种服务发现机制。它通过在ClassPath路径下的META-INF/services文件夹查找文件,自动加载文件里所定义的类。这一机制为很多框架扩展提供了可能,比如在Dubbo、JDBC中都使用到了SPI机制 以 mysql-connector-java 为例子


  1. services 目录下的文件如下图, 文件名就是 jdk 提供的 jdbc 驱动权限定接口名 java.sql.Driver

image.png


2. 加载 com.mysql.jdb.Driver 的过程是这样的。 首先我们先来看一个简单的调用例子


public class JdbcTest {
    public static void main(String[] args) throws SQLException {
        Connection con = DriverManager.getConnection(
                "jdbc:mysql://127.0.0.1:3306/ssm", "root", "root123");
        PreparedStatement pds = con.prepareStatement("select 1");
        ResultSet rs = pds.executeQuery();
        while (rs.next()) {
            String result = rs.getString(1);
            System.out.println("rs: " + result);
        }
        rs.close();
        con.close();
    }
}


  1. 获取链接的核心类就是 DriverManager 我们在来看它提供了的 spi 操作方法 loadInitialDrivers 该方法核心的过程就是 ServiceLoader.load(Driver.class) 对ServiceLoader.load 方法的调用,它会通过接口类会去所有的依赖 jar 中查找实现了 Driver 类的 spi 定义,最后去遍历 driversList 调用 Class.forName 通过上下文类加载器去加载三方实现类。


private static void loadInitialDrivers() {
        String drivers;
        try {
            drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
                public String run() {
                    return System.getProperty("jdbc.drivers");
                }
            });
        } catch (Exception ex) {
            drivers = null;
        }
        // If the driver is packaged as a Service Provider, load it.
        // Get all the drivers through the classloader
        // exposed as a java.sql.Driver.class service.
        // ServiceLoader.load() replaces the sun.misc.Providers()
        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {
                ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
                Iterator<Driver> driversIterator = loadedDrivers.iterator();
                /* Load these drivers, so that they can be instantiated.
                 * It may be the case that the driver class may not be there
                 * i.e. there may be a packaged driver with the service class
                 * as implementation of java.sql.Driver but the actual class
                 * may be missing. In that case a java.util.ServiceConfigurationError
                 * will be thrown at runtime by the VM trying to locate
                 * and load the service.
                 *
                 * Adding a try catch block to catch those runtime errors
                 * if driver not available in classpath but it's
                 * packaged as service and that service is there in classpath.
                 */
                try{
                    while(driversIterator.hasNext()) {
                        driversIterator.next();
                    }
                } catch(Throwable t) {
                // Do nothing
                }
                return null;
            }
        });
        println("DriverManager.initialize: jdbc.drivers = " + drivers);
        if (drivers == null || drivers.equals("")) {
            return;
        }
        String[] driversList = drivers.split(":");
        println("number of Drivers:" + driversList.length);
        for (String aDriver : driversList) {
            try {
                println("DriverManager.Initialize: loading " + aDriver);
                Class.forName(aDriver, true,
                        ClassLoader.getSystemClassLoader());
            } catch (Exception ex) {
                println("DriverManager.Initialize: load failed: " + ex);
            }
        }
    }


线程上下文类加载器


线程上下文类加载(Thread Context ClassLoader)。这个类加载器可以通过 java.lang.Thread 类的 setContextClassLoader() 方法进行设置,如果当前线尚未设置,它将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那这个类加载器就默认是应用程序类加载器。 线程上下文类加载器可以用来去加载所需要的 spi 服务代码, 这是一种父类加载器请求子类加载器完成类加载的行为,这种行为实际上是打破了双亲委派模型的层次结构来逆向使用类加载器。在 jdk 1.6 之后提供了 java.util.ServiceLoader 类, 以 META-INF/services 中的配置信息,通过责任链的方式提供一种 spi 类加载模型。



相关文章
|
1月前
|
前端开发 安全 Java
聊聊Java虚拟机(一)—— 类加载子系统
虚拟机就是一款用来执行虚拟计算机指令的计算机软件。它相当于一台虚拟计算机。大体上,虚拟机分为系统虚拟机和程序虚拟机。系统虚拟机就相当于一台物理电脑,里面可以安装操作系统;程序虚拟机是为了执行单个计算机程序而设计出来的虚拟机。其中 Java 虚拟机就是**执行 Java 字节码指令的虚拟机**。
37 2
|
5月前
|
前端开发 安全 Java
JVM类加载和双亲委派机制
JVM类加载和双亲委派机制
102 0
|
28天前
|
安全 Java 程序员
深入理解jvm - 类加载过程
深入理解jvm - 类加载过程
35 0
|
2月前
|
存储 前端开发 安全
浅谈 JVM 类加载过程
浅谈 JVM 类加载过程
32 0
|
2月前
|
存储 安全 Java
JVM类加载(类加载过程、双亲委派模型)
JVM类加载(类加载过程、双亲委派模型)
|
3月前
|
存储 算法 安全
面试~jvm(JVM内存结构、类加载、双亲委派机制、对象分配,了解垃圾回收)
面试~jvm(JVM内存结构、类加载、双亲委派机制、对象分配,了解垃圾回收)
47 0
|
5月前
|
安全 前端开发 Java
JVM概述和类加载子系统
我记得当年学java的时候,就很好奇,为什么我在IDEA上写一些代码(其实就是一堆我们人能知道的英文单词的组合加一些运算符),为什么就可以在windows上运行后执行我们的指令,而且还可以打成jar包去linux系统跑起来,为什么一份代码可以在不同平台运行呢?类是如何加载的?对象如何创建的以及都有哪些信息?我创建的对象被分配到哪个内存去了?java是怎么和我们操作系统打交道的又是怎么调用CPU为我们计算的?创建了对象分配了内存,为什么可以不用手动回收就可以自动清理内存等等等,相信你也同样有过这些困惑。
33 0
|
6月前
|
存储 安全 前端开发
【jvm系列-02】jvm的类加载子系统以及jclasslib的基本使用
【jvm系列-02】jvm的类加载子系统以及jclasslib的基本使用
49 0
|
7月前
|
存储 Java Linux
Java类加载过程、为什么会出现JVM?
也就是说Java程序可以在windows操作系统上运行,不做任何修改,同样的java程序可以在Linux操作系统上运行,跨平台。
|
7月前
|
存储 安全 前端开发
JVM- 第二章-类加载子系统
JVM- 第二章-类加载子系统
46 0