JAVA类加载机制以及SPI

一、JAVA类加载机制

一个完整的JAVA程序执行过程大约经过几个步骤:

1、编写Java代码;

2、编译成.class二进制文件(字节码);

3、类加载到JVM

4、虚拟机执行。

可以看到,将.class文件加载到JAVA虚拟机是关键一个环节。该步骤主要是将字节码文件装载到JVM中,并对数据进行验证,解析和初始化,最终形成了可以供JAVA虚拟机执行的JAVA类型。

下面的示意图是完整的类加载过程:

类加载主要经过:

1、加载

2、连接

  • 验证
  • 准备
  • 解析

3、初始化

JVM物理结构:

1、加载

也称装载,这个过程就是查找所有的class文件。可以是本地系统的class文件,也可以是通过网络下载的class文件,也可以是所有jar包,war包的class文件,从专有数据库加载,Class.forName()加载,ClassLoader.loadClass()方法动态加载等等。该过程主要完成的事情就是将类的class加载到内存,在JAVA堆中生成一个表示该类的java.lang.Class对象。

2、连接

1)验证

验证就是确保被加载的类的准确性,符合JAVA虚拟即的规范。这个就是一个标准的验证过程,主要包括文件名,元数据,字节码和符号引用等验证。

2)准备

该阶段主要是为了类的静态变量分配内存,并初始化默认值。即类中static声明的变量。不过这里还是要分成两种情况,即是否被final修饰

private static final int s=100;   //在该阶段初始化值为100

private static int s=100;    //在该阶段初始化值为0.赋值100的指令是在后续初始化阶段做的。

3)解析

解析阶段是将常量池中的符号引用解析为直接引用。

符号引用:一种可以唯一表示类的字符串,此时它并没有在内存中。在Class文件中它以CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info等类型的常量出现。

直接引用:能够直接指向目标的指针或者间接指向目标的引用。

通过该过程,目标已经被叫加载到JVM中。

3、初始化

该过程才会开始执行Java的代码。该步骤是对类静态变量进行初始化,以及类的初始化。但类执行初始化是有条件的。

  1. 用new创建对象时;
  2. get或者set类的静态字段或者静态方法时;
  3. 使用JAVA反射进行调用的时侯,即java.lang.reflect调用;
  4. 初始化一个类时,如果该类的父类还没有初始化,要先初始化父类;
  5. 虚拟机启动时,需要初始化main函数的类;

下面的情况不会初始化:

  1. 通过子类调用父类的静态字段,只会初始化父类,不会初始化子类;
  2. 通过数组引用来引用类,不会初始化;
  3. 常量在编译时被存储到常量池中,也不会初始化;
  4. 通过类名获取 Class 对象,不会触发类的初始化;
  5. 通过 Class.forName 加载指定类时,如果指定参数 initialize 为 false 时,也不会触发类初
    始化,其实这个参数是告诉虚拟机,是否要对类进行初始化;
  6. 通过 ClassLoader 默认的 loadClass 方法,也不会触发初始化动作。

在进行类的初始化时还是有一定顺序的,一般都是先初始化父类,再执行子类的初始化。

上面提到的类加载机制都是由类加载器实现的,ClassLoader.从开发的角度来看,加载器主要是分三种:

1、启动类加载器;

2、扩展类加载器;

3、应用类加载器;

三者的区别和联系可见下图:

该图中非常清晰地描述了每种加载器的作用以及加载过程。加载过程就是自下向上,当加载器收到类加载的请求时,自己不会加载该类,而是将请求转发给父类加载器去加载,一层一层,最终都会到达启动类加载器。当父类无法加载的时侯,子类才会去尝试加载。这种加载过程,在Java中叫做双亲委派模型。

该模型的优点是对于基础类,可以保证各个加载器加载的都是同一个,而不会出现混乱的现象。也就是说,它主要是保证Java核心类的安全。

但其也有一样的弊端,即委派是单向的,子类加载器可以访问父类加载器的加载类,但反过来,父类加载器无法访问子类加载的。这个在SPI机制是不可以的,比如JAVA核心定一个SPI,实现类都是应用类加载器加载,那么 BootstrapClassLoader是无法找到实现类的。

对于上面的缺陷,有两种方式解决:

1、通过上下文加载器ContextClassLoader;

比如在加在JDBC驱动时,就使用了该loader。

DriverManager的static:

   static {
        loadInitialDrivers();
        println("JDBC DriverManager initialized");
    }

loadInitialDriver加载驱动,其会调用:

ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);

由于DriverManager的加载器是启动类加载器,是无法加载驱动实现的。其最终会调用:

 public static <S> ServiceLoader<S> load(Class<S> service) {
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        return ServiceLoader.load(service, cl);
    }

2、使用类加载器加载资源,比如jar包。

public Enumeration<URL> getResources(String name) throws IOException {
        @SuppressWarnings("unchecked")
        Enumeration<URL>[] tmp = (Enumeration<URL>[]) new Enumeration<?>[2];
        if (parent != null) {
            tmp[0] = parent.getResources(name);
        } else {
            tmp[0] = getBootstrapResources(name);
        }
        tmp[1] = findResources(name);

        return new CompoundEnumeration<>(tmp);
    }

二、SPI机制

SPI,Service Loader Interface,不同模块之间不应该通过具体实现类这种硬编码的方式,而是采用接口类实现,从而可以灵活插拔和扩展。目前在JAVA领域,SPI机制应用的也非常广泛,比如在SPring中以及Dubbo中都是大量的应用,而Dubbo对JAVA SPI进行了扩展。

SPI的约定:

Java SPI 的具体约定为:当服务的提供者提供了服务接口的一种实现之后,在 jar 包的 META-INF/services/ 目录里同时创建一个以服务接口命名的文件。该文件里就是实现该服务接口的具体实现类。而当外部程序装配这个模块的时候,就能通过该 jar 包 META-INF/services/ 里的配置文件找到具体的实现类名,并装载实例化,完成模块的注入。基于这样一个约定就能很好的找到服务接口的实现类。JDK 提供服务实现查找的一个工具类:java.util.ServiceLoader。

比如 Mysql-connect

JAVA主要通过ServiceLoader加载META-INF/services下的所有实现类,并装载实例化。

看下源码:

public final class ServiceLoader<S>
    implements Iterable<S>
{

    private static final String PREFIX = "META-INF/services/";

    // The class or interface representing the service being loaded
    private final Class<S> service;

    // The class loader used to locate, load, and instantiate providers
    private final ClassLoader loader;

    // The access control context taken when the ServiceLoader is created
    private final AccessControlContext acc;

    // Cached providers, in instantiation order
    private LinkedHashMap<String,S> providers = new LinkedHashMap<>();

    // The current lazy-lookup iterator
    private LazyIterator lookupIterator;

    ......

}

它的特点就是一次性加载所有实例。如果加载失败了,可能名称也拿不到,因此Dubbo基于自身的应用,进行了一定的扩展。

Dubbo SPI

之前在Dubbo相关的文章中,有提到过Dubbo SPI机制,该机制在Dubbo应用的非常广泛,也是非常重要的一部分。Dubbo之所以自己实现,主要是考虑自身的需求,它并不需要原生Java SPI那样,一次把所有实现类都加载出来,而是按需去选择性地加载某个实现类。

Dubbo需要在META-INF/dubbo中描述所要实现的类。看下Dubbo loadclass的源码:

   private Map<String, Class<?>> loadExtensionClasses() {
        this.cacheDefaultExtensionName();
        Map<String, Class<?>> extensionClasses = new HashMap();
        this.loadDirectory(extensionClasses, "META-INF/dubbo/internal/", this.type.getName());
        this.loadDirectory(extensionClasses, "META-INF/dubbo/internal/", this.type.getName().replace("org.apache", "com.alibaba"));
        this.loadDirectory(extensionClasses, "META-INF/dubbo/", this.type.getName());
        this.loadDirectory(extensionClasses, "META-INF/dubbo/", this.type.getName().replace("org.apache", "com.alibaba"));
        this.loadDirectory(extensionClasses, "META-INF/services/", this.type.getName());
        this.loadDirectory(extensionClasses, "META-INF/services/", this.type.getName().replace("org.apache", "com.alibaba"));
        return extensionClasses;
    }
private void cacheDefaultExtensionName() {
//SPI注解
        SPI defaultAnnotation = (SPI)this.type.getAnnotation(SPI.class);
        if (defaultAnnotation != null) {
            String value = defaultAnnotation.value();
            if ((value = value.trim()).length() > 0) {
                String[] names = NAME_SEPARATOR.split(value);
                if (names.length > 1) {
                    throw new IllegalStateException("More than 1 default extension name on extension " + this.type.getName() + ": " + Arrays.toString(names));
                }

                if (names.length == 1) {
                    this.cachedDefaultName = names[0];
                }
            }
        }

    }

看上面的代码,可看到Dubbo会找接口有SPI注解的,类似下面:

@SPI("dubbo")
public interface Protocol {
    int getDefaultPort();

    @Adaptive
    <T> Exporter<T> export(Invoker<T> var1) throws RpcException;

    @Adaptive
    <T> Invoker<T> refer(Class<T> var1, URL var2) throws RpcException;

    void destroy();
}

上面方法上用的Adaptive注解是一种实现自适应扩展的机制,通过该注解,可以生成一种代理类,并在调用接口方法时,会调用代理类的方法,动态获取实现类的方法。

拿protocol举例

private static final Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension();

上面方法最终会调用Dubbo Compile生成一个Adaptive类。具体地实现过程可以参考下面的参考资料,说得很详细。

Dubbo会生成一个代理类:

public class Protocol$Adaptive implements org.apache.dubbo.rpc.Protocol {
    public void destroy() {
        throw new UnsupportedOperationException("The method public abstract void org.apache.dubbo.rpc.Protocol.destroy() of interface org.apache.dubbo.rpc.Protocol is not adaptive method!");
    }
 
    public int getDefaultPort() {
        throw new UnsupportedOperationException("The method public abstract int org.apache.dubbo.rpc.Protocol.getDefaultPort() of interface org.apache.dubbo.rpc.Protocol is not adaptive method!");
 
    }
 
    public org.apache.dubbo.rpc.Exporter export(org.apache.dubbo.rpc.Invoker arg0) throws org.apache.dubbo.rpc.RpcException {
        if (arg0 == null) 
            throw new IllegalArgumentException("org.apache.dubbo.rpc.Invoker argument == null");
        
        if (arg0.getUrl() == null)
            throw new IllegalArgumentException("org.apache.dubbo.rpc.Invoker argument getUrl() == null");
        
        org.apache.dubbo.common.URL url = arg0.getUrl();
        //根据url获取具体的protocol ,url类似:http:*****?protocol=dubbo
        String extName = (url.getProtocol() == null ? "dubbo" : url.getProtocol());
        
        if (extName == null)
            throw new IllegalStateException("Failed to get extension (org.apache.dubbo.rpc.Protocol) name from url (" + url.toString() + ") use keys([protocol])");
        org.apache.dubbo.rpc.Protocol extension = 
                (org.apache.dubbo.rpc.Protocol) ExtensionLoader.getExtensionLoader(org.apache.dubbo.rpc.Protocol.class).getExtension(extName);
        return extension.export(arg0);
    }
 
    public org.apache.dubbo.rpc.Invoker refer(java.lang.Class arg0, org.apache.dubbo.common.URL arg1) throws org.apache.dubbo.rpc.RpcException {
        if (arg1 == null) 
            throw new IllegalArgumentException("url == null");
        
        org.apache.dubbo.common.URL url = arg1;
        
        String extName = (url.getProtocol() == null ? "dubbo" : url.getProtocol());
        
        if (extName == null)
            throw new IllegalStateException("Failed to get extension (org.apache.dubbo.rpc.Protocol) name from url (" + url.toString() + ") use keys([protocol])");
        
        org.apache.dubbo.rpc.Protocol extension = 
                (org.apache.dubbo.rpc.Protocol) ExtensionLoader.getExtensionLoader(org.apache.dubbo.rpc.Protocol.class).getExtension(extName);
        return extension.refer(arg0, arg1);
    }

}

上面代码可看到,当要调用refer方法的时侯,才会去实例化实现类。

随后在应用中可通过ExtensionLoader获取需要的实例,该类类似于JAVA的ServiceLoader,负责类的加载和整个类生命周期的维护。具体源码直接看官网更清楚: Dubbo SPI机制 以及 Dubbo SPI 自适应扩展

参考资料:

JVM之符号引用和直接引用

Java类加载机制(全套)

JAVA类加载机制-知乎

Dubbo SPI自适应扩展原理

--------EOF---------
微信分享/微信扫码阅读