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…

猜你喜欢

转载自blog.csdn.net/scjava/article/details/108249563