java中类加载器ClassLoader,双亲加载机制,启动类加载器,应用类加载器,线程上下文类加载器

我们知道,在java中一般我们都是以jar包或者war包的形式发布应用,而这些里面的class文件是需要在JVM虚拟机中运行,那么这些class类文件怎么加载到jvm中的呢 ?
一个类从加载到虚拟机内存中一直到卸载出内存为止,要经历如下几个阶段

  • 加载(Loading),通过类的全名获取类的字节流,然后将其转换为方法区的运行时数据结构,同时在内存中生成一个代表该类的java.lang.Class对象,作为方法区该类的的各种数据的访问入口
  • 验证(Vertification),确保加载的Class信息符合java虚拟机规范的全部要求
  • 准备(Preparation),为类中定义的静态变量分配内存空间并设置初始值
  • 解析(Resolution),将常量池中的符号引用替换为直接引用,包含类、接口、字段、类方法、接口方法、方法类型、方法句柄、调用点限定符这7类符号引用替换为类信息在内存中的直接引用
  • 初始化(Initialization),初始化类变量和其他资源
  • 使用(Using)
  • 卸载(Unloading)
    而验证、准备、解析三个阶段称为连接(Linking)
    在这里插入图片描述
    这个就是大家看类加载基本上都能看到的内容。

类加载器

在java中类加载器用来加载类二进制文件到JVM中,一个类在JVM中类型是否相同除了比较类的全名是否一致,还需要判断是否由同一个类加载器加载到JVM中,即判断类的类型是否一样前提条件是必须是同一个类加载器,类加载器有如下三个特性:

  • 委托机制,子类加载一个类首先委托父类去加载,如果父类加载不了才会自己去加载
  • 可见性,子类可以看见并访问父类加载的所有类,但是父类加载器加载的类看不到子类加载器加载的类
  • 单一性,类加载器仅加载一个类一次,委托机制确保子类不会在加载父类已经加载的类

在java中,如果classA中有classB的引用,那么当加载classA的时候发现需要加载classB,就会用加载classA的类加载器去加载classB

双亲委派模型

jvm中类加载器加载类的时候采用的是双亲委派模型:当一个类加载器需要加载一个类的时候,并不会立马自己去记载,而是首先委派给父类加载器去加载,父类加载器加载不了在给父类的父类去加载,一层一层往上委托,直到顶层加载器(Bootstrap Classloader),如果父类加载器反馈无法加载那么累加器才会自己去加载。

需要注意的是,双亲委派模型中,子类是可以看到并且访问父类加载的类,反之不行,即父类无法看到访问子类加载的类
JVM默认提供了三种类加载器:

  • 启动类加载器(BootStrap ClassLoader),加载在JAVA_HOME/lib目录下或-Xbootclasspath参数指定的路径且能够被jvm识别的(按照文件名识别,如rt.jar、tools.jar,名字不符合即使放在lib目录下也不会被加载)
  • 扩展类加载器(Extension ClassLoader),加载在JAVA_HOME/lib/ext目录下,或者被java.ext.dirs系统变量所指定的的路径中的类,可以直接将常用的类放在JAVA_HOME/lib/ext目录下,让扩展类加载器加载
  • 应用程序类加载器(Applicaiton ClassLoader),加载用户类路径下的所有类库classpath,也被称为系统类加载器,如果没有自定义过类加载器,这就是程序中默认的类加载器
    在这里插入图片描述

双亲委派机制保证了一个类只会被加载一次,同时java核心类库不会被篡改,即使篡改了也不会被加载

线程上下文加载器

但是java中也有破坏双亲模型的机制,例如SPI(Service Provider Interface),但是SPI的接口是java核心类库(如ServiceLoader),是由启动类加载器加载的,但是实现都是第三方实现,一般都是由应用程序类加载器加载的(例如mysql驱动: com.mysql.jdbc.Driver)。我们看看JDBC的加载机制:

 Connection connection = DriverManager.getConnection(url,username,password);
        String sql = "select count(1) from test ";
        Statement statement = connection.prepareStatement(sql);
        ResultSet rs = statement.executeQuery(sql);
        while(rs.next()){
    
    
            int result = rs.getInt(1);
            System.out.println(result);
        }

上面这段不用Classs.forName也是可以的,就是用到了SPI机制:

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

ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
                Iterator<Driver> driversIterator = loadedDrivers.iterator();
                try{
    
    
                    while(driversIterator.hasNext()) {
    
    
                    // load class
                        driversIterator.next();
                    }
                } catch(Throwable t) {
    
    
                // Do nothing
                }
                return null;
            }

最后实际上是加载META-INF/services/java.sql.Driver文件里面的类:
在这里插入图片描述
最后需要加载如下两个类:

com.mysql.jdbc.Driver
com.mysql.fabric.jdbc.FabricMySQLDriver

也就是说,需要在在加载核心类库中的类ServiceLoader加载的时候加载上述两个类。按照上面描述的:
在java中,如果classA中有classB的引用,那么当加载classA的时候发现需要加载classB,就会用加载classA的类加载器去加载classB

这时候,加载ServiceLoader用的是启动类加载器,按照双亲委托机制是无法加载第三方类的。这时候java提供了线程上下文类加载器,相当于是破坏了双亲加载机制。
那么线程上下文类加载器怎么使用的呢 ?

 public static <S> ServiceLoader<S> load(Class<S> service) {
    
    
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        return ServiceLoader.load(service, cl);
    }
public static <S> ServiceLoader<S> load(Class<S> service,
                                            ClassLoader loader)
    {
    
    
        return new ServiceLoader<>(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 void reload() {
    
    
        providers.clear();
        lookupIterator = new LazyIterator(service, loader);
    }
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
        }

可以看到,这里通过ClassLoader cl = Thread.currentThread().getContextClassLoader();获取到了线程上线文类加载器,然后用这个去加载第三方类库,这样启动类加载器加载的类就能够看到线程上下文类加载器加载的类了,打破了双亲委派机制。

我们在看看tomcat的类加载器的应用:
请添加图片描述

tomcat中自己定义了几个类加载器,主要分为如下几个:

  • common类加载器,这个类加载器加载的类能被所有的程序共享,包含tomcat和tomcat下的web程序
  • catalina类加载器,这个类加载器加载的类只能被tomcat使用,对web程序不可见
  • shared类加载器,可以被所有的web程序公用,但是tomcat无法使用

可以看到,通过自定义的类加载器和双亲委派机制,tomcat实现了不同层次类之间的隔离。

在spring中,用的大部分也都是线程上下文类加载器。

猜你喜欢

转载自blog.csdn.net/LeoHan163/article/details/118379542