JVM类加载器子系统与SPI

类加载器

JVM中有两种类型的加载器,一种是由C++编写,一种是由JAVA编写,除了启动类加载器(Bootstrap Class Loader)是由C++编写的以外,其他都是由JAVA编写的,由JAVA编写的加载器都继承与java.lang.ClassLoader,JVM还支持自定义加载器,对于自定义加载器的典型用法就是可以破坏JVM的双亲委派,为什么要破坏双亲委派,想要做到破坏双亲委派的项目必有这个需求,比如像我们的web服务器tomcat,就自定义了类加载器,不仅仅是web服务器,像目前主流的技术框架都会自定义类加载器,自定义类加载器可以加载自己想要加载的class,不仅仅是加载本地的,还可以加载来自于网络的class文件。
JVM的类加载器有:启动类加载器、扩展类加载器、应用类加载器、自定义类加载器;JVM的类加载器是属于双亲委派的模式,就是当我的类加载器需要加载一个类时,它自己不会去完成这个操作,它会把请求向上委派,直到启动类加载器去加载,如果最后都没有加载成功,也就是没有找到全限定名的class时,这时候JVM会抛出一个ClassNotFound的异常,下图为我们的类加载器的结构:
在这里插入图片描述
上图为类加载器的结构图,从图中可以看出我们要加载自己的类是向上委派给自己的父类加载器去完成的;其中最上层的启动类加载器是C++编写的,我们无法去查看它的实现,其他都是java实现的类加载器,因此,启动类加载器无法被Java程序调用
启动类加载器不像其他类加载器有实体,它是没有实体的,JVM将C++处理类加载的一套逻辑定义为启动类加载器。

启动类加载器

查看启动类加载器的加载路径

URL[] urLs = Launcher.getBootstrapClassPath().getURLs();
for (URL urL : urLs) {
    System.out.println(urL);
}

输出:
file:/D:/tools/java8_212/jre/lib/resources.jar
file:/D:/tools/java8_212/jre/lib/rt.jar
file:/D:/tools/java8_212/jre/lib/sunrsasign.jar
file:/D:/tools/java8_212/jre/lib/jsse.jar
file:/D:/tools/java8_212/jre/lib/jce.jar
file:/D:/tools/java8_212/jre/lib/charsets.jar
file:/D:/tools/java8_212/jre/lib/jfr.jar
file:/D:/tools/java8_212/jre/classes
可以看到启动类加载器主要加载了我们的jre下的核心包

扩展类加载器

  ClassLoader classLoader = ClassLoader.getSystemClassLoader().getParent();

        URLClassLoader urlClassLoader = (URLClassLoader) classLoader;

        URL[] urls = urlClassLoader.getURLs();
        for (URL url : urls) {
            System.out.println(url);
        }

file:/D:/tools/java8_212/jre/lib/ext/access-bridge-64.jar
file:/D:/tools/java8_212/jre/lib/ext/cldrdata.jar
file:/D:/tools/java8_212/jre/lib/ext/dnsns.jar
file:/D:/tools/java8_212/jre/lib/ext/jaccess.jar
file:/D:/tools/java8_212/jre/lib/ext/jfxrt.jar
file:/D:/tools/java8_212/jre/lib/ext/localedata.jar
file:/D:/tools/java8_212/jre/lib/ext/nashorn.jar
file:/D:/tools/java8_212/jre/lib/ext/sunec.jar
file:/D:/tools/java8_212/jre/lib/ext/sunjce_provider.jar
file:/D:/tools/java8_212/jre/lib/ext/sunmscapi.jar
file:/D:/tools/java8_212/jre/lib/ext/sunpkcs11.jar
file:/D:/tools/java8_212/jre/lib/ext/zipfs.jar
加载ext扩展目录的jar

应用类加载器

默认加载用户程序的类加载器

查看类加载器加载的路径

 String[] urls = System.getProperty("java.class.path").split(":");

        for (String url : urls) {
            System.out.println(url);
        }

        System.out.println("================================");

        URLClassLoader classLoader = (URLClassLoader) ClassLoader.getSystemClassLoader();

        URL[] urls1 = classLoader.getURLs();
        for (URL url : urls1) {
            System.out.println(url);
        }

类加载器加载的类如何存储

在这里插入图片描述
从上图可以得到不同的类加载器在加载类到方法区中过后是会在方法区开辟自己的一部分空间,也就是说不同的类加载器在加载同一个类到方法区过后,他们的内存地址是不一样的,因为在方法区中的地址不一样,而JVM的加载过程是双亲委派,为什么要双亲委派??其中的一部分原因就是为了防止我们的同一个类被不同的加载器所加载,那么它在内存中就会存在多份,所以使用双亲委派机制就可以很好避免这种情况的发生。
当我们想要得到一个类的对象的时候,需要从方法区中取得这个类,也就是类加载的过程(经过一系列的过程)然后初始化在堆区创建空间来存储我们创建的对象。

双亲委派

什么是双亲委派,双亲委派就是JVM在加载类时的一种机制,每当有类加载请求时,会把请求委托给自己的父类,让父类帮自己完成类的加载,一层一层网上委托,直到委托给启动类加载,如果最后都没有被加载,则会抛出ClassNotFound一层;下图就是双亲委派的说明:
在这里插入图片描述

打破双亲委派

因为在某些情况下父类加载器需要委托子类加载器去加载class文件。受到加载范围的限制,父类加载器无法加载到需要的文件,以Driver接口为例,由于Driver接口定义在jdk当中的,而其实现由各个数据库的服务商来提供,比如mysql的就写了MySQL Connector,那么问题就来了,DriverManager(也由jdk提供)要加载各个实现了Driver接口的实现类,然后进行管理,但是DriverManager由启动类加载器加载,只能记载JAVA_HOME的lib下文件,而其实现是由服务商提供的,由系统类加载器加载,这个时候就需要启动类加载器来委托子类来加载Driver实现,从而破坏了双亲委派。

类似这样的情况就需要打破双亲委派。

打破双亲委派的意思其实就是不委派、向下委派

SPI机制

打破双亲委派的还有一个机制是SPI机制
什么是SPI SPI ,全称为 Service Provider Interface,是一种服务发现机制

它通过在ClassPath路径下的META-INF/services文件夹查找文件,自动加载文件里所定义的类。
这一机制为很多框架扩展提供了可能,比如在Dubbo、JDBC中都使用到了SPI机制。我们先通过一个很简单的例子来看下它是怎么用的:

首先我创建4个工程:
xxx-core 创建一个接口PayService
xxx-dev1 创建一个测试类
xxx-dev2 创建一个微信支付类实现PayService
xxx-dev3 创建一个支付宝支付类实现PayService

如下:
xxx-core:
PayService:

public interface PayService {
    void pay();
}

xxx-dev2:
WxPayService:

public class WxPayService implements PayService {
    @Override
    public void pay() {
        System.out.println("Wxpay....");
    }
}

xxx-dev3:
AliPayService

public class AliPayService implements PayService {
    @Override
    public void pay() {
        System.out.println("AliPay....");
    }
}

我们在dev2和dev3工程的resources下面还要建立一个服务文件:
在这里插入图片描述

其中com.xxx.**.PayService就是你的实现类

我们来编写我们的测试类:
xxx-dev1:

public class SpiTest {

    public static void main(String[] args) {
        ServiceLoader<PayService> services = ServiceLoader.load(PayService.class);
        services.forEach(item ->{
            item.pay();
        });
    }
}

通过jdk提供的SPI机制ServiceLoader来实现服务的发现;
这样运行是不能输出任何东西的,但是当我们在dev1工程引入xxx-dev2就会输出Wxpay…,
当我们将dev1工程换成dev2的时候,会输出AliPay…
如果我们把dev1和dev2都引入了,那么就都会被发现,被注册,那么就会输出:
Wxpay…
AliPay…

<think>嗯,用户之前问过JVM的工作原理,GC算法和回收机制,现在又接着问类加载和双亲委派。看来他们对JVM的底层机制比较感兴趣,可能是在学习Java的高级内容,或者准备面试需要。 首先,我需要确认用户对类加载的基本概念是否了解。类加载是JVM将.class文件加载到内存并生成Class对象的过程,这个过程包括加载、链接、初始化等阶段。用户可能想知道每个阶段的具体作用,以及双亲委派模型是如何工作的。 双亲委派模型是类加载的重要机制,用户可能听过这个词,但不太清楚具体如何实现,或者为什么要有这种机制。需要解释清楚双亲委派的层次结构,比如启动类加载器、扩展类加载器、应用程序类加载器,以及它们之间的委派关系。还要提到双亲委派的优点,比如避免重复加载,保证核心类库的安全,防止恶意替换。 可能用户还有更深层的需求,比如如何打破双亲委派,或者实际应用中的例子,比如Tomcat如何自定义类加载器。需要准备这些扩展内容,但不要过于深入,避免让回答变得冗长。 另外,用户可能对类加载的实践问题感兴趣,比如如何自定义类加载器,或者在什么情况下需要这样做。比如热部署、模块化加载等场景。这时候可以举一个简单的代码示例,展示如何继承ClassLoader并重写findClass方法。 需要注意的是,之前的回答中已经提到了双亲委派机制,所以这次可能需要更详细地展开,避免重复。同时,要确保语言通俗易懂,结构清晰,分点说明,帮助用户逐步理解。 总结下来,回答的结构应该是:先介绍类加载的过程,分阶段讲解,然后详细说明双亲委派模型,包括各个类加载器的层次结构、工作流程、优点,以及如何打破双亲委派,最后结合实际应用场景,比如Tomcat的例子,让用户更好地理解其实际意义。</think>以下是对类加载机制双亲委派模型的深度解析: --- ### 一、类加载全流程剖析 类加载是JVM将字节码转换为运行时类结构的核心过程,主要分为三个阶段: #### 1. 加载(Loading) - **核心任务**:查找.class文件并读入字节流 - **数据来源**: - 本地文件系统 - 网络资源(如Applet) - JAR/ZIP归档文件 - 动态生成的字节码(代理类) - **重要产物**:在堆中生成`java.lang.Class`对象 #### 2. 链接(Linking) | 子阶段 | 关键操作 | |-------------|--------------------------------------------------------------------------| | **验证** | 检查魔数、版本号、字节码合法性(防止篡改.class文件) | | **准备** | 为类变量分配内存并设置初始值(如int→0,对象→null) | | **解析** | 将符号引用转为直接引用(如将`java.lang.String`转化为内存地址) | #### 3. 初始化(Initialization) - 执行`<clinit>`方法(编译器自动收集类变量的赋值操作和静态代码块) - **线程安全保证**:JVM通过加锁确保只有一个线程执行初始化 ![类加载流程图](https://via.placeholder.com/600x300?text=Class+Loading+Process) --- ### 二、双亲委派机制详解 #### 1. 四层类加载器体系 | 加载器类型 | 加载路径 | 代码实现类 | |-------------------------|-----------------------------------------|-----------------------------| | **Bootstrap ClassLoader** | $JAVA_HOME/jre/lib/*.jar | C++实现,Java中显示为null | | **Extension ClassLoader** | $JAVA_HOME/jre/lib/ext/*.jar | sun.misc.Launcher$ExtClassLoader | | **Application ClassLoader** | CLASSPATH指定路径 | sun.misc.Launcher$AppClassLoader | | **Custom ClassLoader** | 开发者自定义路径 | 继承java.lang.ClassLoader的子类 | #### 2. 工作流程(源码级解析) ```java // ClassLoader.loadClass()关键逻辑 protected Class<?> loadClass(String name, boolean resolve) { synchronized (getClassLoadingLock(name)) { // 1.检查是否已加载 Class<?> c = findLoadedClass(name); if (c == null) { try { // 2.委派父加载器 if (parent != null) { c = parent.loadClass(name, false); } else { c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) {} // 3.父类无法加载时自己处理 if (c == null) { c = findClass(name); // 触发自定义加载逻辑 } } return c; } } ``` #### 3. 设计优势 - **安全防护**:防止核心API被篡改(如自定义java.lang.String类) - **避免重复**:保证类的唯一性(相同类名不同加载器加载视为不同类) - **灵活扩展**:开发者可通过自定义加载器实现特殊需求 --- ### 三、打破双亲委派的典型场景 #### 1. SPI机制(Service Provider Interface) - **问题场景**:JDBC驱动由Bootstrap加载器加载,但需要加载厂商实现类 - **解决方案**:使用线程上下文类加载器(Thread Context ClassLoader) ```java // JDBC驱动加载示例 Class.forName("com.mysql.jdbc.Driver"); Connection conn = DriverManager.getConnection(url); // DriverManager通过ContextClassLoader加载驱动实现 ``` #### 2. OSGi模块化系统 - 每个Bundle使用独立类加载器 - 采用网状委派模型替代双亲委派 #### 3. 热部署实现 - 自定义类加载器加载新版本类 - 通过不断创建新ClassLoader实例实现代码热替换 --- ### 四、实战:自定义类加载器 ```java public class CustomClassLoader extends ClassLoader { private String classPath; public CustomClassLoader(String classPath) { this.classPath = classPath; } @Override protected Class<?> findClass(String name) throws ClassNotFoundException { byte[] data = loadClassData(name); return defineClass(name, data, 0, data.length); } private byte[] loadClassData(String className) { // 自定义加载逻辑(如从网络/加密文件加载) try (InputStream ins = new FileInputStream(classPath + className.replace(".", "/") + ".class"); ByteArrayOutputStream baos = new ByteArrayOutputStream()) { int len; byte[] buffer = new byte[4096]; while ((len = ins.read(buffer)) != -1) { baos.write(buffer, 0, len); } return baos.toByteArray(); } catch (IOException e) { throw new RuntimeException(e); } } } // 使用示例 ClassLoader loader = new CustomClassLoader("/custom_classes/"); Class<?> clazz = loader.loadClass("com.example.MyClass"); ``` --- ### 五、类加载器常见问题排查 1. **ClassNotFoundException** - 检查类路径配置 - 确认是否被正确加载器加载 2. **NoClassDefFoundError** - 类文件存在但依赖缺失 - 类初始化失败导致后续加载被拒绝 3. **LinkageError** - 不同类加载器加载了同名类 - 版本冲突导致类不兼容 --- ### 六、新型模块化系统影响(JPMS) - **模块路径** vs 类路径 - 新增`jrt:/`协议访问JDK模块 - 通过`module-info.java`声明依赖关系 掌握类加载机制是理解Java动态扩展、热部署等高级特性的基础,也是排查类冲突问题的关键切入点。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值