网上写类加载过程与SPI的文章还是挺多的,但写文章主要目的还是巩固自己的学习,如果能帮助到其他人那再好不过了。所以总结下比如线程上下文类加载器Thread.currentThread().getContextClassLoader()还有JDK SPI与Spring SPI的实现原理。
类的加载过程主要分为加载->连接->初始化。连接过程又可分为三步:验证->准备->解析
1.加载:使用类加载器加载类文件的字节流到方法区,在内存中生成Class对象。
2.验证:文件格式验证->元数据验证->字节码验证->符号引用验证。
文件格式验证就是校验Class文件规范,这几种验证在虚拟机规范里有介绍,这里不详细说明
3.准备:在方法区为类变量(static)分配内存,并初始化默认值。
类变量如果是基本数据类型,引用和实例都在方法区
如果引用数据类型,引用在方法区,实例在堆中
4.解析:将常量池内的符号引用替换成直接引用。
直接引用:直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄
5.初始化:执行类初始化的代码逻辑(按照顺序为:类的静态变量和静态代码块赋值->成员变量和构造代码块赋值->构造器赋值)
类的初始化只有满足触发条件才会被初始化,而虚拟机规范里明确规范了一些触发条件:
1)遇到4类字节码指令(new,getstatic,putstatic,invokestatic )
创建类的实例对象,访问类的静态变量,赋值类的静态变量,调用类的静态方法
2)使用reflect包的方法对类进行反射调用:如Class.forName(“xxx”)等
3)初始化一个类前,会先触发父类的初始化:即super()需要在构造方法第一行
4)虚拟机启动时,会先初始化主类(main方法所在的类)
还有java7与java8中更新的两种情况用的比较少就不说了,方法区即Java8中的元空间,随着JDK版本更新一些信息都有可能会改变
主要触发条件是创建类的实例变量,这又与对象创建有关,所以下面说对象的创建过程
6.卸载:类不再被引用,类的Class对象被GC。(补充:卸载类之前,该类的类加载器实例需被GC)
对象的创建过程主要分为类加载检查->分配内存->初始化零值->设置对象头(->执行初始化方法)。
1.类加载检查:
比如JVM遇到new指令,会检查指令参数是否能在常量池中定位到这个类的符号引用,并检查这个符号引用代表的类是否已被加载、连接和初始化过。如果没有则执行该类的加载过程。
而JVM遇到static相关指令(比如访问修改ClassA类中的static ClassB varA变量),则检查ClassA的加载-验证,而ClassA的验证过程中如果发现ClassB未被加载,则又触发ClassB的加载-连接-初始化。
2.分配内存
JVM为新生对象分配内存,这个过程涉及到内存分配方式和内存分配并发问题,不了解可以自行学习。
3.初始化零值
JVM将分配到的内存空间都初始化为零值。(不包括对象头,对象分为对象头、实例数据、对齐填充)
4.设置对象头
对象头分为标记字段和类型指针,比如设置标记字段的对象的哈希码,分代年龄和锁标志位信息。
这和对象回收,锁升级有关,这里不详细说,不了解可以自行学习。
5.执行初始化方法(比如Servlet初始化阶段的init()方法):
在JVM层次对象已经创建,在程序代码层次,对象创建完成前还需执行初始化动作。
类的初始化与对象创建紧密相关,比如上文提到的类初始化过程中,静态变量的访问、赋值,调用静态方法以及创建实例对象,都涉及到静态对象或实例对象的创建。因此类加载过程的部分内容,也是对象创建过程的一部分。
Java类加载主要依据双亲委派模型进行加载,大家应该都很熟悉。
bootstrap classloader ------- 对应jvm中某c++写的dll类,
Extenson ClassLoader ---------对应内部类ExtClassLoader,
System ClassLoader ---------对应内部类AppClassLoader,
Custom ClassLoader ----------对应任何URLClassLoader的子类
以上四种classloder按照从上到下的顺序,依次为下一个的parent
抽象类 ClassLoader
|
SecureClassLoader
|
URLClassloader
| |
sun的ExtClassLoader sun的AppClassLoader
(自定义类加载器步骤:
1、编写一个类继承自ClassLoader抽象类。
2、复写它的findClass()方法。
3、在findClass()方法中调用defineClass()。
自定义类加载器也可以继承SecureClassLoader或ClassLoader)
类加载步骤:
1、 一个AppClassLoader查找资源时,先看看缓存是否有,缓存有从缓存中获取,否则委托给父加载器。
2.、递归,重复第1部的操作。
3.、如果ExtClassLoader也没有加载过,则由Bootstrap ClassLoader出面,它首先查找缓存,如果没有找到的话,就去找自己的规定的路径下,也就是sun.mic.boot.class下面的路径。找到就返回,没有找到,让子加载器自己去找。
4.、BootstrapClassLoader如果没有查找成功,则ExtClassLoader自己在java.ext.dirs路径中去查找,查找成功就返回,查找不成功,再向下让子加载器找。
5.、ExtClassLoader查找不成功,AppClassLoader就自己查找,在java.class.path路径下查找。找到就返回。如果没有找到就让子类找,如果没有子类会抛出各种异常。
但按照双亲委派模型不能解决所有的类加载问题,Java中定义了许多SPI(服务者提供接口,ServiceProviderInterface的缩写),这些接口并未实现,由第三方进行实现,例如JDBC、JNDI等。依据双亲委派模型,SPI的接口是Java核心库的一部分,由BootStrapClassLoader加载的;SPI实现的Java类一般是由AppClassLoader来加载。
因此JDK定义了线程上下文加载器(Thread.currentThread().getContextClassLoader)这个概念,它并不是真实存在的,可以理解为线程的一个属性,在BootStrap类加载器加载的类中通过它获取到了AppClassLoader,可认为通过它获取下层类加载器,来绕开双亲委托的机制。
以JDBC为例,mysql驱动jar包下META-INF/services目录下有一个以 接口全限定名 (java.sql.Driver)为命名的文件,内容为实现类的全限定名(com.mysql.cj.jdbc.Driver)
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
上文提到过:类不能被重复加载,而卸载类之前,该类的类加载器实例需被GC
因此热部署的原理就是
1)销毁该自定义ClassLoader
2)更新class类文件
3)创建新的ClassLoader去加载更新后的class类文件
Spring中有类似JDK的SPI机制,通过SpringFactoriesLoader代替JDK中的ServiceLoader, 通过META-INF/spring.factories文件代替META-INF/service目录下的描述文件
Spring的SPI也更加灵活,可以根据资源文件的URL,就可以构造相应的文件来读取资源内容
不必要key是接口, 值是实现类。
Spring boot使用此方式来处理自动配置的bean: key是注解,值是被标记@Configuration的类
(如key是org.springframework.boot.autoconfig.EnableAutoConfiguration)
读取资源获取类的全限定名,然后通过反射构造类的实例
# Application Listeners
org.springframework.context.ApplicationListener=\
org.springframework.boot.autoconfigure.BackgroundPreinitializer
.................
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\
.................
public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";
// spring.factories文件的格式为:key=value1,value2,value3
// 从所有的jar包中找到META-INF/spring.factories文件
// 然后从文件中解析出key=factoryClass类名称的所有value值
public static List<String> loadFactoryNames(Class<?> factoryClass, ClassLoader classLoader) {
String factoryClassName = factoryClass.getName();
// 取得资源文件的URL
Enumeration<URL> urls = (classLoader != null ?
classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
List<String> result = new ArrayList<String>();
// 遍历所有的URL
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
// 根据资源文件URL解析properties文件,得到对应的一组@Configuration类
Properties properties = PropertiesLoaderUtils.loadProperties(
new UrlResource(url));
String factoryClassNames = properties.getProperty(factoryClassName);
// 组装数据,并返回
result.addAll(Arrays.asList(
StringUtils.commaDelimitedListToStringArray(factoryClassNames)));
}
return result;
}
Spring中还有converter SPI和formatter SPI:
converter可以用做任意两个类型之间的转换, formatter用做string类型和其他类型之间的转换
有错误帮忙指出,有问题可以探讨