Classloader
JVM加载class文件到内存有两种方式:
- 隐式加载:不通过在代码里调用ClassLoader来加载需要的类,而是通过JVM来自动加载需要的类到内存,例如:当类中继承或者引用某个类时,JVM在解析当前这个类不在内存中时,就会自动将这些类加载到内存中。
- 显式加载:在代码中通过ClassLoader类来加载一个类,例如调用
this.getClass.getClassLoader().loadClass()
或者Class.forName()
ClassLoader工作机制
注意:
程序在启动的时候,并不会一次性加载程序所要用的所有class文件,而是根据程序的需要,通过Java的类加载机制来动态加载某个class文件到内存中。
ClassLoader工作机制
双亲委派模型
双亲委派模式是在Java 1.2后引入的,其工作原理的是:
如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求
委托给父类的加载器去执行
,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回
,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载
,这就是双亲委派模式,即每个儿子都很懒,每次有活就丢给父亲去干,直到父亲说这件事我也干不了时,儿子自己想办法去完成
双亲委派模式优势
- 避免类的重复加载
- 安全因素,java核心api中定义类型不会被随意替换
深入理解Java类加载器
用类加载器显式加载类例如
java.lang.Integer
,类加载器只会返回给已加载过的;如果自定义java.lang.Integer
并加载之,会报错。
https://blog.csdn.net/Mint6/article/details/80864788?from=singlemessage
具体解析
一般来说,例如程序hello.jar
执行到:
Demo demo = new Demo();
会按照双亲委派模型进行加载类Demo
。如果Demo
在hello.jar
内,AppClassLoader
就将其加载完成;但是如果例如SPI
这种,既不在应用hello.jar
内又不在系统类路径内,那么就要抛弃双亲委派模型,获取线程上下文类加载器
加载(线程上下文类加载器
默认是AppClassLoader
,此时的线程上下文类加载器
肯定是自定义的类加载器)。
在DriverManager类初始化时执行了loadInitialDrivers()方法,在该方法中通过
ServiceLoader.load(Driver.class);
去加载外部实现的驱动类,ServiceLoader类会去读取mysql的jdbc.jar下META-INF文件的内容这样ServiceLoader会帮助我们处理一切,并最终通过load()方法加载,看看load()方法实现就知道最终是
通过线程上下文类加载器加载
public static ServiceLoader load(Class service) {
//通过线程上下文类加载器加载
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
自定义一个破坏双亲委派模型的类加载器
的方法:
- 父加载器
parent
设置为null- 重写
loadClass()
方法直接调用findClass
(可以参考ClassLoader
代码)
深入理解Java类加载器
加载指定路径的class或jar
这里介绍2种加载方式:
- URLClassLoader直接加载
- ServiceLoader加载
URLClassLoader直接加载
例如要加载类:
package com;
public class Demo {
public Demo() {
System.out.println("\n" + this.getClass().getClassLoader().toString());
}
}
将其编译为class文件,存放在路径/Users/root/Projects/idea/my/com
。
注意!
- 根目录是
/Users/root/Projects/idea/my/
,com
是表示包路径。- 该类里面如果引用根目录以外的类,必须在runtime中能够获取到
这时要加载它:
@Test
public void test() throws Exception {
// 使用根路径
URL url = new URL("file:/Users/root/Projects/idea/my/");
ClassLoader newCL = new URLClassLoader(new URL[]{url}, Thread.currentThread().getContextClassLoader());
// 加载。注意要使用全限定名
Class clazz = Class.forName("com.Demo", false, newCL);
// 或者
clazz = newCL.loadClass("com.Demo");
}
ServiceLoader加载
对于SPI这种,就需要用到ServiceLoader加载。可以参考地址:https://github.com/byamao1/try-plugin
需要注意:
- 放入URLClassLoader的URL,目标是jar必须是到jar文件路径,目标是class可以是class的根文件夹路径
- 自定义类加载器或自己new的URLClassLoader,要在重写的方法
loadClass
中先判断要加载的类是否为非本加载器加载的类(如spi中的类),如果是则用其他类加载器(例如spi加载器)加载,否则才由自己加载- 在idea中
resources
文件夹下不要直接新建META-INFO.services
文件夹,而是要新建文件夹META-INFO
后再在其下新建文件夹services
(虽然这样建idea的显示就是META-INFO.services
,但绝不能按照前面的做,那样只是1个名字叫META-INFO.services
的文件夹)。- 插件类例如
Demo
必须有一个无参构造方法,否则ServiceLoader
无法实例化插件类
知识点
- 放入URLClassLoader的URL的用途就是让该类加载器能加载其应该拥有的jar或class
- URLClassLoader符合双亲委派模型
- 从日志中可以看出:
Demo
中的IDemo
是由AppClassLoader加载的;Demo、OtherClass、Internal
是由插件类加载器加载。
插件化
插件化的一个重要目标就是利用类加载器实现类隔离(比如不同厂商版本的依赖包),其原理在于在类中(例如Demo
)隐式类加载器就是Demo
的类加载器(一般为插件类加载器),对于插件中出现的插件外的类(例如SPI接口类)则不加载。
这里分析Presto的connector插件架构。
Presto的自定义类加载器PluginClassLoader
继承URLClassLoader
类并重写了loadClass
,其类加载逻辑为:
如果类已加载了,就返回它
如果是个SPI接口类,则委托给
spiClassLoader
(就是PluginManager
的类加载器)加载否则交给父方法
super.loadClass
加载。这里是真正加载插件类的地方,会到该加载器的成员URLClassPath
中找该类。要注意的是,PluginManager.parent为空
,实际上就是不会委托父加载器加载,而是只由自己加载(实际上打破了双亲委派模型)。插件类的加载过程是:
注意:
更改当前线程的
ContextClassLoader
,只是为了应对扩展程序中可能出现的如下代码:
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
classLoader.loadClass(...);
Java 自定义 ClassLoader 实现隔离运行不同版本jar包的方式
从上面我们得知,如果采取ServiceLoader的SPI方案,应该在resources/META-INF/services
中存放实现类的全限定名。有意思的是Presto的插件基本都没有这个声明文件,但是编译打包后插件模块的target/classes
中却能找到。如果观察插件的pom.xml
文件,就会发现
。其实在根pom.xml
中使用了presto自己的打包插件presto-maven-plugin
,将该maven插件打开看就能发现ServiceDescriptorGenerator
中会在打包时自动生成了声明文件。
SOFA-Ark
SOFA-Ark
是蚂蚁金服开源的一款基于Java实现的轻量级类隔离加载容器。
具体可以参考博客:sofa-ark类隔离技术分析调研
站在插件的角度看待,我觉得:
SOFA-Ark = SPI接口声明 + 插件间可依赖
Ref
你应该知道的Java Classloader - 知乎