JVM性能优化------类加载器实现原理(二)

项目地址
jvm_01

前面提到了双亲委派原则,是为了保证类的唯一,那么我们怎么去破坏这个机制呢?
思路:直接绕开loadClass方法
那么我们要怎么去绕开loadClass方法呢?
有两种办法:

  1. 前面我们提到了自定义加载器,所以我们可以自定义加载器
  2. Spi机制绕开loadClass方法,使用当前线程设定关联类加载器

我们先来了解一下Spi机制。

Spi机制
Spi机制加载第三方扩展的jar包类初始化,对我们接口的类实现初始化。
那么有哪些jar使用过Spi机制呢?我们很熟悉的mysql连接池就使用了,我们先看看这个jar包的结构。
在这里插入图片描述
我们可以看到这里又是一个奇怪的名字。。。没错,我们今天看的就是他。
为什么要这么命名,都后面就揭晓了。先看看这个文件
在这里插入图片描述
只有这一个信息,这个信息如果写代码写多了,那么一定会熟悉,这就是一个对象的完整路径。我们进去看看。
在这里插入图片描述
这时候我们就知道了,他是一个实现的了java.sql.Driver(这个名字也就是service文件夹下的文件名)接口。

好了,现在我们来看看怎么使用Spi机制。
在这里插入图片描述
现在我们可以看到,代码已经完成。并且创建了文件。
Main.java

package com.lxq;

import java.util.ServiceLoader;

/**
 * @author 龙小虬
 * @date 2021/4/12 11:33
 */
public class Main {
    public static void main(String[] args) {
        ServiceLoader<SpiService> load = ServiceLoader.load(SpiService.class);
        load.forEach((k)->{
            System.out.println(k);
        });
    }
}

resources/META-INF/services/com.lxq.SpiService

com.lxq.impl.SpiService01
com.lxq.impl.SpiService02

运行结果:
在这里插入图片描述
现在可以发现程序运行成功了。那他到底是怎么实现的呢?

查看ServiceLoader的load()方法
在这里插入图片描述

  1. ClassLoader cl = Thread.currentThread().getContextClassLoader();
    获取当前线程的ClassLoad()
  2. ServiceLoader.load(service, cl);
    传参,并执行重载的方法。

在这里插入图片描述
new 出新对象。

private ServiceLoader(Class<S> svc, ClassLoader cl) {
    service = Objects.requireNonNull(svc, "Service interface cannot be null");
    loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
    acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
    reload();
}
  1. Objects.requireNonNull(svc, "Service interface cannot be null");
    判断service是否为空
  2. loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
    如果当前线程的加载器为空,那么就使用系统加载器。对于系统加载器具体是是什么,后面我们再来看。

再调用reload();

private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
public void reload() {
    providers.clear();
    lookupIterator = new LazyIterator(service, loader);
}
  1. providers.clear();
    对providers集合进行清空
  2. lookupIterator = new LazyIterator(service, loader);
    对对象进行初始化。

我们在看到hasNextService()方法

private boolean hasNextService() {
    if (nextName != null) {
        return true;
    }
    if (configs == null) {
        try {
            String fullName = PREFIX + service.getName();
            if (loader == null)
                configs = ClassLoader.getSystemResources(fullName);
            else
                configs = loader.getResources(fullName);
        } catch (IOException x) {
            fail(service, "Error locating configuration files", x);
        }
    }
    while ((pending == null) || !pending.hasNext()) {
        if (!configs.hasMoreElements()) {
            return false;
        }
        pending = parse(service, configs.nextElement());
    }
    nextName = pending.next();
    return true;
}
  1. String fullName = PREFIX + service.getName();
    获取service变量的名字,也就是我们前面传入的SpiService的路径+包名。
    在这里插入图片描述
    通过debug也可以看到是这样的。这个字符串熟悉吗?就是我们在resources文件下的目录文件吧?这也就是为什么文件名要这样命名的原因。
  2. if (loader == null)
    如果当前线程的类加载器是null也就是启动类加载器。那么就使用系统加载器,这个后面会提到。如果不为null,那么获取目录,然后就读取文件结束了。

可能到这里还有很多人不理解为什么使用当前线程的加载器呢?
我们看看,当前加载器是什么。
在这里插入图片描述
可以看到当前线程的加载器是应用类加载器。如果我们需要破坏双亲委派原则,是不是就要打乱他的读取顺序?那么就很好理解了,只要我们不从启动类加载器开始读取,就开始破坏了。

也可以这样,假如我们更改目前线程的加载器

package com.lxq;

import java.util.ServiceLoader;

/**
 * @author 龙小虬
 * @date 2021/4/12 11:33
 */
public class Main {
    public static void main(String[] args) {
        Thread.currentThread().setContextClassLoader(Thread.currentThread().getContextClassLoader().getParent());
        System.out.println(Thread.currentThread().getContextClassLoader());
        ServiceLoader<SpiService> load = ServiceLoader.load(SpiService.class);
        load.forEach((k)->{
            System.out.println(k);
        });
    }
}

运行结果:
在这里插入图片描述
这里可以看到,Spi机制执行的没有将两个实现类加载进去。因为我们使用的是应用类加载器的父类加载器(扩展类加载器)
那么,如果改为启动类加载器呢?
在这里插入图片描述
诶?竟然加载出来了???
前面提到了,如果当前加载器为null,那么会使用系统加载器,看看系统加载器吧。
在这里插入图片描述
initSystemClassLoader();初始化系统加载器
在这里插入图片描述
我们可以看到scl变量是从这两个地方获取到的。先看到sun.misc.Launcher.getLauncher();,这个Launcher应该很熟悉了,第一篇文章提到过,
在这里插入图片描述
到这里就可以看到了,原来我们的系统加载器就是应用类加载器。这一切就已经说明了,当我们的当前线程为启动类加载器的时候,会自动改变成使用应用类加载器。

目前使用Spi机制破坏双亲委派原则,就结束了。那么我们来自定义实现吧。

自定义加载器
应用场景:
需要自己自定义class文件夹目录

  1. 网络上的class文件
  2. 本地硬盘

继承ClassLoader,并且覆写findClass()方法。

package com.lxq;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.channels.WritableByteChannel;

/**
 * @author 龙小虬
 * @date 2021/4/12 12:21
 */
public class MyLoadClass extends ClassLoader{

    private String fileName;

    public MyLoadClass(String fileName){
        this.fileName = fileName;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        try {
            byte[] data = getClassFileBytes(new File(fileName));
            return defineClass(name,data,0,data.length);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    //此方法不需要理解
    private byte[] getClassFileBytes(File file) throws Exception {
        FileInputStream fileInputStream = new FileInputStream(file);
        FileChannel fileChannel = fileInputStream.getChannel();
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        WritableByteChannel writableByteChannel = Channels.newChannel(byteArrayOutputStream);
        ByteBuffer byteBuffer = ByteBuffer.allocateDirect(1024);
        while (true){
            int i = fileChannel.read(byteBuffer);
            if(i==0 || i==-1){
                break;
            }
            byteBuffer.flip();
            writableByteChannel.write(byteBuffer);
            byteBuffer.clear();
        }
        fileInputStream.close();
        return byteArrayOutputStream.toByteArray();
    }
}

运行代码

package com.lxq;

import java.util.ServiceLoader;

/**
 * @author 龙小虬
 * @date 2021/4/12 11:33
 */
public class Main02 {
    public static void main(String[] args) throws ClassNotFoundException {
        MyLoadClass myLoadClass = new MyLoadClass("D:\\SpiService03.class");
        Class<?> aClass = myLoadClass.findClass("com.lxq.impl.SpiService03");
        System.out.println(aClass.getClassLoader());
    }
}

运行结果:
在这里插入图片描述
现在我们的自定义加载器就可用了。这样看起来还没怎么发现他的作用在哪。那我们再来个热部署吧。在项目部署到服务器上的时候,我们都需要进行测试,那么在测试环境的情况下我们就需要用到热部署,这样会比重启快很多。

手写热部署
我们知道热部署主要用于测试环境,因为测试环境只更改部分代码,那么热部署具体是怎样的呢?首先他肯定是时时刻刻注意着文件是否发生改变。所以我们需要创建一个线程来对文件更改进行监控。
那么,怎么知道文件被更改了呢?
其实记录文件的最后修改时间就行了,一旦最后修改时间发生变化,那么文件就被更改了。就需要我们重新初始化对象。
所以,我们首先在原加载器中,添加文件的最后修改时间。

public long getLastModified(){
   return file.lastModified();
}

再获取多个编译后的class文件,注意文件内容需要修改。

public class SpiService03 implements SpiService {
    @Override
    public String output() {
        return "热部署进行中";
    }
}
public class SpiService03 implements SpiService {
    @Override
    public String output() {
        return "热部署进行1";
    }
}
public class SpiService03 implements SpiService {
    @Override
    public String output() {
        return "热部署进行2";
    }
}

最后创建线程。

new Thread(()->{
        while (true){
            try{
                Thread.sleep(2000);

            }catch (Exception e){

            }
            end = myLoadClass1.getLastModified();
            if(end!=start) {
                MyLoadClass myLoadClass2 = new MyLoadClass("D:\\SpiService03.class");
                try {
                    Class<?> aClass2 = myLoadClass2.findClass("com.lxq.impl.SpiService03");
                    SpiService spiService2 = getSpiService(aClass2);
                    System.out.println(spiService2.output());
                    start = end;
                } catch (ClassNotFoundException e) {
                    e.printStackTrace();
                }
            }
        }
    }).start();
}

最终代码:

package com.lxq;

import java.util.ServiceLoader;

/**
 * @author 龙小虬
 * @date 2021/4/12 11:33
 */
public class Main02 {
    static long start;
    static long end;
    public static void main(String[] args) throws ClassNotFoundException {
        MyLoadClass myLoadClass1 = new MyLoadClass("D:\\SpiService03.class");
        Class<?> aClass1 = myLoadClass1.findClass("com.lxq.impl.SpiService03");
        SpiService spiService1 = getSpiService(aClass1);
        System.out.println(spiService1.output());
        start = myLoadClass1.getLastModified();
        new Thread(()->{
            while (true){
                try{
                    Thread.sleep(2000);

                }catch (Exception e){

                }
                end = myLoadClass1.getLastModified();
                if(end!=start) {
                    MyLoadClass myLoadClass2 = new MyLoadClass("D:\\SpiService03.class");
                    try {
                        Class<?> aClass2 = myLoadClass2.findClass("com.lxq.impl.SpiService03");
                        SpiService spiService2 = getSpiService(aClass2);
                        System.out.println(spiService2.output());
                        start = end;
                    } catch (ClassNotFoundException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();
    }

    static SpiService getSpiService(Class<?> aClass){
        try {
            return (SpiService)aClass.newInstance();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
        return null;
    }
}

运行结果:
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

龙小虬

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值