jvm系列-类加载机制

一、类加载基础

1.1.类加载理论介绍

1.1.1、什么是类加载

虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型.

1.1.2、类加载过程

类加载机制过程分为以下几步

  1. 加载类文件
  2. 连接:验证、准备、解析
  3. 初始化
  4. 使用
  5. 卸载
1.1.2.1、加载类文件

加载阶段完成以下内容

  1. 根据一个类的全限定名称来获取该类的二进制字节流
  2. 将这个字节流代表的静态存储结构转换为方法区运行时数据结构
  3. 在内存中生成代表这个类的java.lang.Class对象,作为方法区这个类的信息的访问入口
1.1.2.2、连接
1.1.2.1.1、验证阶段

验证字节流的正确性

  1. 文件格式验证
  2. 元数据验证
  3. 字节码验证
  4. 符号引用验证
1.1.2.1.2、准备阶段

该阶段主要为类变量分配内存和设置类变量的初始值

1.1.2.1.3、解析阶段

将符号引用解析为直接引用.

主要包括:

  1. 类或接口解析
  2. 字段解析
  3. 类方法解析
  4. 接口方法解析
1.1.2.3、初始化

执行方法.

  1. 方法收集了类变量的赋值和静态代码块,执行顺序,按代码书写顺序
  2. 方法,在类加载时执行,方法在构造方法执行时执行
  3. 父类的方法先执行,也就是父类的静态变量赋值、静态代码块执行优于子类
  4. 接口不会有static代码块,但是也会有静态变量赋值,也会生成方法,但是只有在用到该变量时才会初始化,并且不会去先初始化父接口的静态变量
  5. 在多线程环境下,虚拟机会保证一个类的方法被正确的加锁、同步,只会执行一次,如果执行很耗时,会造成阻塞

对与接口的例子:

public interface SonInterfaceInit {

     String s = StringDefine.define("son a");

}


public class SonInterfaceImpl implements SonInterfaceInit {
    private static  String son= StringDefine.define("son impl var");
}

类加载过程和初始化过程:

[Loaded jdk.net.ExtendedSocketOptions$PlatformSocketOptions from /Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded jdk.net.ExtendedSocketOptions$PlatformSocketOptions$1 from /Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded jdk.net.MacOSXSocketOptions from /Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/rt.jar]
//加载了接口
[Loaded com.source.jvm.data.SonInterfaceInit from file:/Users/dev/workspace-study/source-study/source-jvm/target/classes/]
[Loaded com.source.jvm.data.SonInterfaceImpl from file:/Users/dev/workspace-study/source-study/source-jvm/target/classes/]
[Loaded java.util.Collections$UnmodifiableSet from /Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded jdk.net.ExtendedSocketOptions$1 from /Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded java.util.Collections$UnmodifiableCollection$1 from /Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded com.source.jvm.data.StringDefine from file:/Users/dev/workspace-study/source-study/source-jvm/target/classes/]
[Loaded java.util.HashMap$KeySet from /Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded java.util.HashMap$HashIterator from /Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded java.util.HashMap$KeyIterator from /Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/rt.jar]
//初始化类里面的
接口初始化:son impl var
-----------------------
//初始化接口
接口初始化:son a
[Loaded java.net.StandardSocketOptions from /Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded java.net.StandardSocketOptions$StdSocketOption from /Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded java.lang.Shutdown from /Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded java.net.NetworkInterface from /Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded java.lang.Shutdown$Lock from /Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/rt.jar]

从上面的例子可以看出,当类初始化时,不会去执行接口的方法,也不会去执行父接口的方法.

1.1.3、何时触发类加载

  1. 当遇到new (new 一个对象)、getstatic(使用类的静态变量)、putstatic(为类的静态变量赋值)、invokestatic(执行类的静态方法)这个四个字节码指令时,会触发
  2. 当使用反射调用时
  3. 初始化一个类,发现其父类没有初始化
  4. 虚拟机启动时,执行指定的主类
  5. 当使用动态语言支持时

1.1.4、ClassLoader源码

1.1.4.1、双亲委派机制

在这里插入图片描述

双亲委派机制:

  1. 当加载一个类时,首先自己不去尝试加载该类,把加载类饿请求委派给父类加载器去完成
  2. 当父类加载器无法加载时,才自己尝试去加载该类,如果都无法加载,jvm虚拟机会报java.lang.ClassNotFoundException异常
1.1.4.2、ClassLoader源码
1.1.4.2.1、loadClass
  public Class<?> loadClass(String name) throws ClassNotFoundException {
        return loadClass(name, false);
    }
    
    
    protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
         //获取锁
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            //检查当前class是否已经加载
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        //去父加载器加载
                        c = parent.loadClass(name, false);
                    } else {
                       //在BootstrapClassLoader 中尝试加载
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    //当前classloader尝试加载
                    c = findClass(name);

                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

从上面看出加载一个类的流程:

  1. 获取加载当前类的锁,保证多线程情况下,加载类是线程安全的
  2. 检查当前classloader是否已经加载过类
  3. 如果有父加载器,委托父加载器加载
  4. 如果没有父加载器,说明当前已经是ExtClassLoader,尝试在BootstrapClassloader下加载
  5. 如果父加载器没有加载成功,执行findClass方法,当前加载器尝试加载,如果加载不成功抛出java.lang.ClassNotFoundException异常
  6. 如果resolve为true,解析该类

加载锁,如果parallelLockMap不为null,可以并行加载.否者锁就是当前classloader

    protected Object getClassLoadingLock(String className) {
        Object lock = this;
        if (parallelLockMap != null) {
            Object newLock = new Object();
            lock = parallelLockMap.putIfAbsent(className, newLock);
            if (lock == null) {
                lock = newLock;
            }
        }
        return lock;
    }
1.1.4.2.2、findLoadedClass
    protected final Class<?> findLoadedClass(String name) {
        if (!checkName(name))
            return null;
        return findLoadedClass0(name);
    }

    private native final Class<?> findLoadedClass0(String name);

查看是否已经加载过该类,最终调用native方法

1.1.4.2.3、findClass方法
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        throw new ClassNotFoundException(name);
    }

UrlClassLoader实现:

protected Class<?> findClass(final String name)
        throws ClassNotFoundException
    {
        final Class<?> result;
        try {
            result = AccessController.doPrivileged(
                new PrivilegedExceptionAction<Class<?>>() {
                    public Class<?> run() throws ClassNotFoundException {
                        String path = name.replace('.', '/').concat(".class");
                        Resource res = ucp.getResource(path, false);
                        if (res != null) {
                            try {
                                //根据二进制流获取class
                                return defineClass(name, res);
                            } catch (IOException e) {
                                throw new ClassNotFoundException(name, e);
                            }
                        } else {
                            return null;
                        }
                    }
                }, acc);
        } catch (java.security.PrivilegedActionException pae) {
            throw (ClassNotFoundException) pae.getException();
        }
        if (result == null) {
            throw new ClassNotFoundException(name);
        }
        return result;
    }
1.1.4.3、打破双亲委派模型
1.1.4.3.1、双亲委派模型三次大规模破坏
  1. jdk1.2 定义了findClass(),自己实现的类加载器,可以通过实现该方法来加载指定位置的类
  2. JNDI服务的需求,需要加载JNDI接口提供者(SPI)的代码,添加了通过线程Thread类来设置classLoader的方法setContextClassLoader
  3. 用户对程序动态性的追求而导致,如代码热替换、模块热部署
1.1.4.3.2、破坏双亲委派模型实战
  1. tomcat classloader设计
  2. spi原理

接下来详细说明下这两种情况.

二、类加载使用

2.1.SPI机制

SPI是Service Provider Interface 的简称,即服务提供者接口的意思。SPI说白了就是一种扩展机制,我们在相应配置文件中定义好某个接口的实现类,然后再根据这个接口去这个配置文件中加载这个实例类并实例化

2.1.1.Java对SPI的实现

java自带的java.util.ServiceLoader实现了SPI机制.

2.1.1.1.ServiceLoader使用

定义接口

package com.study.jvm.spi;

public interface PayInterface {
    /**
     * 支付接口
     * @param uid
     * @param money
     * @return
     */
    boolean pay(Long uid,long money);
}

定义实现类:

package com.study.jvm.spi;

public class WxPayInterface implements  PayInterface {
    @Override
    public boolean pay(Long uid, long money) {
        System.out.println("通过微信支付");
        return true;
    }
}

package com.study.jvm.spi;

public class ZfbPayInterface implements  PayInterface {
    @Override
    public boolean pay(Long uid, long money) {
        System.out.println("通过支付宝支付");
        return true;
    }
}

配置文件:

路径:META-INF/services/com.study.jvm.spi.PayInterface

com.study.jvm.spi.WxPayInterface
com.study.jvm.spi.ZfbPayInterface

测试:

package com.study.jvm.spi;

import java.util.Iterator;
import java.util.ServiceLoader;

public class SpiTest {

    public static void main(String[] args) {
        ServiceLoader<PayInterface> serviceLoader = ServiceLoader.load(PayInterface.class);

        Iterator<PayInterface>  iterator = serviceLoader.iterator();
        while (iterator.hasNext()){
          PayInterface payInterface =   iterator.next();
          payInterface.pay(1L,1L);
        }

    }
}

使用总结:

  1. 定义需要通过spi实现的接口
  2. 一般来说实现类与当前接口不在同一个jar,那么在实现类的jar下创建:META-INF/services/目录,并且以接口类权限定名创建文件
    1. 文件内是当前实现类的权限定名
  3. 当使用时,需要在当前jar中引用接口所在的jar和实现类所在的jar,然后通过ServiceLoader.load(类)加载实现类到当前classloader中.
2.1.1.2.ServiceLoader原理

先看下ServiceLoader源码

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;

public void reload() {
        providers.clear();
        lookupIterator = new LazyIterator(service, loader);
    }

    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();
    }
  
  public Iterator<S> iterator() {
        return new Iterator<S>() {

            Iterator<Map.Entry<String,S>> knownProviders
                = providers.entrySet().iterator();

            public boolean hasNext() {
                if (knownProviders.hasNext())
                    return true;
                return lookupIterator.hasNext();
            }

            public S next() {
                if (knownProviders.hasNext())
                    return knownProviders.next().getValue();
                return lookupIterator.next();
            }

            public void remove() {
                throw new UnsupportedOperationException();
            }

        };
    }
  
  private class LazyIterator
        implements Iterator<S>
    {

        Class<S> service;
        ClassLoader loader;
        Enumeration<URL> configs = null;
        Iterator<String> pending = null;
        String nextName = null;

        private LazyIterator(Class<S> service, ClassLoader loader) {
            this.service = service;
            this.loader = loader;
        }

        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;
        }

        private S nextService() {
            if (!hasNextService())
                throw new NoSuchElementException();
            String cn = nextName;
            nextName = null;
            Class<?> c = null;
            try {
                c = Class.forName(cn, false, loader);
            } catch (ClassNotFoundException x) {
                fail(service,
                     "Provider " + cn + " not found");
            }
            if (!service.isAssignableFrom(c)) {
                fail(service,
                     "Provider " + cn  + " not a subtype");
            }
            try {
                S p = service.cast(c.newInstance());
                providers.put(cn, p);
                return p;
            } catch (Throwable x) {
                fail(service,
                     "Provider " + cn + " could not be instantiated",
                     x);
            }
            throw new Error();          // This cannot happen
        }

        public boolean hasNext() {
            if (acc == null) {
                return hasNextService();
            } else {
                PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {
                    public Boolean run() { return hasNextService(); }
                };
                return AccessController.doPrivileged(action, acc);
            }
        }

        public S next() {
            if (acc == null) {
                return nextService();
            } else {
                PrivilegedAction<S> action = new PrivilegedAction<S>() {
                    public S run() { return nextService(); }
                };
                return AccessController.doPrivileged(action, acc);
            }
        }

        public void remove() {
            throw new UnsupportedOperationException();
        }

    }
}

从源码可以看出我们可以通过ServiceLoader ,将META-INF/services/目录下对应接口的实现,加载到给定的ClassLoader中.

为什么要设置ClassLoader.

因为在jvm中,判断两个类是否相同的条件是:

  1. 全限定名相同
  2. 由同一个类加载器加载

所以ClassLoader 配合 SPI可以得到以下功能:

在这里插入图片描述

我们想象一下下面这个场景:

存在一个接口,但是由于版本更替,存在两个实现类,但是实现类名称相同,某一个时刻,线上需要根据不同的参数,调用其中一个实现类.如何实现.

还以上面的例子,现在加一层代理.具体实现如下:
在这里插入图片描述

实现逻辑如下:

获取接口实现类的工厂类

package com.study.jvm.spi;

import java.net.MalformedURLException;
import java.net.URL;
import java.util.concurrent.ConcurrentHashMap;

public class ZfbPayFactory implements PayInterface{


    private final ConcurrentHashMap<String,ZfbPayClassLoader> loaderMap = new ConcurrentHashMap<>(8);

    private final static String ZFB_PAY_VERSION_1 = "ZFB_PAY_VERSION_1_CLASS_LOADER";



    private final static String ZFB_PAY_VERSION_2 = "ZFB_PAY_VERSION_2_CLASS_LOADER";


    @Override
    public boolean pay(Long uid, long money) {
        String loaderName = uid>10? ZFB_PAY_VERSION_2 : ZFB_PAY_VERSION_1;
        ZfbPayClassLoader loader =  loaderMap.get(loaderName);
        if(loader == null){
            synchronized (ZfbPayFactory.class){
                if(loaderMap.get(loaderName) == null){
                    URL[] urls = new URL[1];
                    String location = getLocation(uid);
                    try {
                        urls[0] = new URL("file", "",location);
                        ZfbPayClassLoader newLoader = new ZfbPayClassLoader(urls,Thread.currentThread().getContextClassLoader());
                        loaderMap.put(loaderName,newLoader);
                    } catch (MalformedURLException e) {
                        e.printStackTrace();
                    }
                }
            }
            loader = loaderMap.get(loaderName);
        }
        try {
          Class clazz=  loader.loadClass("com.study.jvm.spi.ZfbPayInterface");

          PayInterface payInterface = (PayInterface) clazz.newInstance();
          payInterface.pay(uid,money);
          return true;
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        }
        return false;
    }

    public String getLocation(Long uid) {
        String baseLocation = "/Users/dev/workspace-study/study/jvm/src/main/java/com/study/jvm/spi/";
        return baseLocation + (uid>10 ? "version2/" :"version1/");
    }

    public static void main(String[] args) {
        ZfbPayFactory factory = new ZfbPayFactory();
       factory.pay(11L,1L);
       factory.pay(1L,1L);


    }
}

自定义ClassLoader

package com.study.jvm.spi;

import sun.misc.Resource;
import sun.misc.URLClassPath;

import java.io.*;
import java.net.URL;
import java.net.URLClassLoader;
import java.security.AccessController;

public class ZfbPayClassLoader extends URLClassLoader {


    private String[] locations ;



    public ZfbPayClassLoader(URL[] urls, ClassLoader parent) {
        super(urls, parent);
        this.locations = new String[urls.length];
        for(int i=0;i<urls.length;i++){
            locations[i] = urls[i].getPath();
            super.addURL(urls[i]);
        }


    }


    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] data = loadMyClass(name);
        if(data == null){
            throw new ClassNotFoundException(name);
        }
       return defineClass(name,data, 0, data.length);
    }

    private byte[] loadMyClass(String name) {
        for(String location  : locations){
            byte[]  data  =  loadByPath(location,name);
            if(data!=null){
                return data;
            }
        }
       return null;
    }

    private byte[] loadByPath(String location, String name) {
        InputStream ins = null;
        try {
            String path =location+name.replace('.', '/').concat(".class");;
            ins = new FileInputStream(new File(path));
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            int bufferSize = 4096;
            byte[] buffer = new byte[bufferSize];
            int bytesNumRead = 0;
            while ((bytesNumRead = ins.read(buffer)) != -1) {
                baos.write(buffer, 0, bytesNumRead);
            }
            return baos.toByteArray();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }


}

几个实现细节,需要注意:

  1. 代码中有一些变量是写死的,可以放到配置中心
    1. 如存放实现类的文件夹地址
    2. 实现类的全限定名
  2. 根据参数获取ClassLoader名称的转换可以抽象出具体的转换规则.

2.1.2.SpringBoot 对SPI机制的扩展

在SpringBoot中如果想自动引入其他jar的配置文件,并根据这些配置扫描指定路径下的包,将包下的bean注册到当前容器,可以使用SpringBoot提供的SPI机制.

在这里插入图片描述

步骤:

  1. 在你自定义的jar的classpath下创建META-INF目录,并创建spring.factories文件
  2. 你可以在spring.factories文件中定义org.springframework.boot.autoconfigure.EnableAutoConfiguration=\自定义的@Configuration类

这样spring就会自动加载这些@Configuration类,并加载里面定义的bean,当然你也可以在@Configuration类里面,定义自己的扫描路径,进行扫描Bean.

2.1.3.Tomcat的SPI机制

在Tomcat中提供了一个Common ClassLoader,它主要负责加载Tomcat使用的类和Jar包以及应用通用的一些类和Jar包,例如CATALINA_HOME/lib目录下的所有类和Jar包。

Tomcat会为每个部署的应用创建一个唯一的类加载器,也就是WebApp ClassLoader,它负责加载该应用的WEB-INF/lib目录下的Jar文件以及WEB-INF/classes目录下的Class文件。由于每个应用都有自己的WebApp ClassLoader,这样就可以使不同的Web应用之间相互隔离,彼此之间看不到对方使用的类文件。即使不同项目下的类全限定名有可能相等,也能正常工作。
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

程序猿老徐

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

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

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

打赏作者

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

抵扣说明:

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

余额充值