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 类加载模型。



相关文章
|
5月前
|
前端开发 安全 Java
聊聊Java虚拟机(一)—— 类加载子系统
虚拟机就是一款用来执行虚拟计算机指令的计算机软件。它相当于一台虚拟计算机。大体上,虚拟机分为系统虚拟机和程序虚拟机。系统虚拟机就相当于一台物理电脑,里面可以安装操作系统;程序虚拟机是为了执行单个计算机程序而设计出来的虚拟机。其中 Java 虚拟机就是**执行 Java 字节码指令的虚拟机**。
65 2
|
6天前
|
存储 算法 Java
深入解析 Java 虚拟机:内存区域、类加载与垃圾回收机制
本文介绍了 JVM 的内存区域划分、类加载过程及垃圾回收机制。内存区域包括程序计数器、堆、栈和元数据区,每个区域存储不同类型的数据。类加载过程涉及加载、验证、准备、解析和初始化五个步骤。垃圾回收机制主要在堆内存进行,通过可达性分析识别垃圾对象,并采用标记-清除、复制和标记-整理等算法进行回收。此外,还介绍了 CMS 和 G1 等垃圾回收器的特点。
19 0
深入解析 Java 虚拟机:内存区域、类加载与垃圾回收机制
|
2月前
|
存储 算法 Java
JVM组成结构详解:类加载、运行时数据区、执行引擎与垃圾收集器的协同工作
【8月更文挑战第25天】Java虚拟机(JVM)是Java平台的核心,它使Java程序能在任何支持JVM的平台上运行。JVM包含复杂的结构,如类加载子系统、运行时数据区、执行引擎、本地库接口和垃圾收集器。例如,当运行含有第三方库的程序时,类加载子系统会加载必要的.class文件;运行时数据区管理程序数据,如对象实例存储在堆中;执行引擎执行字节码;本地库接口允许Java调用本地应用程序;垃圾收集器则负责清理不再使用的对象,防止内存泄漏。这些组件协同工作,确保了Java程序的高效运行。
19 3
|
2月前
|
C# UED 开发者
WPF动画大揭秘:掌握动画技巧,让你的界面动起来,告别枯燥与乏味!
【8月更文挑战第31天】在WPF应用开发中,动画能显著提升用户体验,使其更加生动有趣。本文将介绍WPF动画的基础知识和实现方法,包括平移、缩放、旋转等常见类型,并通过示例代码展示如何使用`DoubleAnimation`创建平移动画。此外,还将介绍动画触发器的使用,帮助开发者更好地控制动画效果,提升应用的吸引力。
64 0
|
3月前
|
存储 缓存 自然语言处理
(三)JVM成神路之全面详解执行引擎子系统、JIT即时编译原理与分派实现
执行引擎子系统是JVM的重要组成部分之一,在JVM系列的开篇曾提到:JVM是一个架构在平台上的平台,虚拟机是一个相似于“物理机”的概念,与物理机一样,都具备代码执行的能力。
|
3月前
|
存储 前端开发 Java
(二)JVM成神路之剖析Java类加载子系统、双亲委派机制及线程上下文类加载器
上篇《初识Java虚拟机》文章中曾提及到:我们所编写的Java代码经过编译之后,会生成对应的class字节码文件,而在程序启动时会通过类加载子系统将这些字节码文件先装载进内存,然后再交由执行引擎执行。本文中则会对Java虚拟机的类加载机制以及执行引擎进行全面分析。
|
3月前
|
Java Perl
JVM内存问题之如何统计在JVM的类加载中,每一个类的实例数量,并按照数量降序排列
JVM内存问题之如何统计在JVM的类加载中,每一个类的实例数量,并按照数量降序排列
|
3月前
|
存储 安全 Java
开发与运维引用问题之JVM类加载过程如何解决
开发与运维引用问题之JVM类加载过程如何解决
22 0
|
3月前
|
存储 算法 Java
JAVA程序运行问题之Java类加载到JVM中加载类时,实际上加载的是什么如何解决
JAVA程序运行问题之Java类加载到JVM中加载类时,实际上加载的是什么如何解决
|
4月前
|
安全 前端开发 Java
《JVM由浅入深学习【一】 》JVM由简入深学习提升(类加载过程+父子类加载过程+类加载器+双亲委派机制)
《JVM由浅入深学习【一】 》JVM由简入深学习提升(类加载过程+父子类加载过程+类加载器+双亲委派机制)
33 0