学习java不可不知的类加载器

我们都知道JDBC规范定义在java的核心包rt.jar中,rt.jar是由启动类加载器去加载的,jdbc的驱动管理器是由具体的数据库厂商提供的,比如mysql和oracle的jar包。实际开发中一般是把驱动包放到classpath中,但是classpath路径是由系统类加载器去加载的,正常来说启动类加载器是无法加载classpath中的类文件的,那么jdbc是如何获取到驱动的呢?想要弄清楚这个问题,需要先了解java类加载的机制。

一 类加载器的介绍

java代码通过javac编译成class文件,而类加载器就是把class文件装进虚拟机。java中类加载器分为四类,分别是启动类加载器、扩展类加载器、系统类加载器和自定义类加载器。为什么需要这么多类加载器呢?因为java虚拟机启动的时候,并不会一次性加载所有的class文件,而是根据需要去动态加载,避免很大程度的内存浪费。

  1. 启动类加载器
    顾名思义,是在虚拟机启动时加载,是由C++编写,主要扫描rt.jar这个包,但并不仅仅是这个包,可以通过系统参数System.getProperty("sun.boot.class.path")获取扫描的路径。可以看到,启动类加载器加载的是jre和jre/lib目录下的核心库。

    //启动类加载器加载的路径
    String bootstrapPaths = System.getProperty("sun.boot.class.path");
    Arrays.stream(bootstrapPaths.split(":")).forEach(path -> System.out.println(path));
    
    //打印结果如下
    /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/jre/lib/resources.jar
    /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/jre/lib/rt.jar
    /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/jre/lib/sunrsasign.jar
    /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/jre/lib/jsse.jar
    /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/jre/lib/jce.jar
    /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/jre/lib/charsets.jar
    /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/jre/lib/jfr.jar
    /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/jre/classes
    
  2. 扩展类加载器
    通过System.getProperty("java.ext.dirs")可以看到它主要负责加载Java的扩展类库,默认加载JAVA_HOME/jre/lib/ext/目录下的所有jar包或者由java.ext.dirs系统属性指定的jar包。

    //扩展类加载器加载的路径:
    String extPaths = System.getProperty("java.ext.dirs");
    Arrays.stream(extPaths.split(":")).forEach(path -> System.out.println(path));
    
    //打印结果如下
    /Users/liyefei6/Library/Java/Extensions
    /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/jre/lib/ext
    /Library/Java/Extensions
    /Network/Library/Java/Extensions
    /System/Library/Java/Extensions
    /usr/lib/java
    
  3. 系统类加载器
    负责在JVM启动时,加载来自在命令java中的classpath或者java.class.path系统属性或者CLASSPATH操作系统属性所指定的JAR类包和类路径.

    //系统类加载器加载的路径:
    String appPaths = System.getProperty("java.class.path");
    Arrays.stream(appPaths.split(":")).forEach(path -> System.out.println(path));
    
    //打印结果如下
    系统类加载器加载的路径:
    /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/jre/lib/charsets.jar
    /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/jre/lib/deploy.jar
    /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/jre/lib/ext/cldrdata.jar
    /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/jre/lib/ext/dnsns.jar
    /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/jre/lib/ext/jaccess.jar
    /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/jre/lib/ext/jfxrt.jar
    /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/jre/lib/ext/localedata.jar
    /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/jre/lib/ext/nashorn.jar
    /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/jre/lib/ext/sunec.jar
    /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/jre/lib/ext/sunjce_provider.jar
    /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/jre/lib/ext/sunpkcs11.jar
    /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/jre/lib/ext/zipfs.jar
    /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/jre/lib/javaws.jar
    /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/jre/lib/jce.jar
    /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/jre/lib/jfr.jar
    /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/jre/lib/jfxswt.jar
    /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/jre/lib/jsse.jar
    /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/jre/lib/management-agent.jar
    /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/jre/lib/plugin.jar
    /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/jre/lib/resources.jar
    /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/jre/lib/rt.jar
    /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/lib/ant-javafx.jar
    /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/lib/dt.jar
    /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/lib/javafx-mx.jar
    /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/lib/jconsole.jar
    /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/lib/packager.jar
    /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/lib/sa-jdi.jar
    /Library/Java/JavaVirtualMachines/jdk1.8.0_261.jdk/Contents/Home/lib/tools.jar
    /Users/liyefei6/code/demo-parent/demo-classloader/target/classes
    /Users/liyefei6/.m2/repository/org/apache/commons/commons-lang3/3.11/commons-lang3-3.11.jar
    ......
    
    public class Test {
        public static void main(String[] args) {
            //下面代码输出结果为:sun.misc.Launcher$AppClassLoader@18b4aac2
            System.out.println(Test.class.getClassLoader());
        }
    }
    //通过上面代码打印的结果验证了Test类使用的类加载器为AppClassLoader,即系统类加载器。
    
  4. 自定义类加载器
    如果系统提供的类加载器满足不了需求,比如本地磁盘或者网络下载的.class文件,那么就可以自定义一个类加载器。下面是官方doc上给出的实现自定义加载器的例子:

     *     class NetworkClassLoader extends ClassLoader {
     *         public Class findClass(String name) {
     *             byte[] b = loadClassData(name);
     *             return defineClass(name, b, 0, b.length);
     *         }
     *
     *         private byte[] loadClassData(String name) {
     *             // load the class data from the connection
     *         }
     *     }
    

    从代码示例中可以看出,实现自定义类加载器只需要三步:写一个loadClassData把class文件变为byte数组、重写findClass方法、调用defineClass方法。下面是自定义类加载器的实现:

    //自定义类加载器
    public class MyClassLoader extends ClassLoader {
    
        private String loadPath;
    
        public MyClassLoader(String loadPath) {
            super();
            this.loadPath = loadPath;
        }
    
        @Override
        public Class findClass(String name) {
            byte[] b = loadClassData(name);
            return defineClass(name, b, 0, b.length);
        }
    
        private byte[] loadClassData(String name) {
            String fullPath = loadPath + name.replaceAll("\\.", "/") + ".class";
            File file = new File(fullPath);
            if (file.exists()) {
                FileInputStream in = null;
                ByteArrayOutputStream out = null;
                try {
                    in = new FileInputStream(file);
                    out = new ByteArrayOutputStream();
                    byte[] buffer = new byte[1024];
                    int size = 0;
                    while ((size = in.read(buffer)) != -1) {
                        out.write(buffer, 0, size);
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                } finally {
                    try {
                        in.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
                return out.toByteArray();
            } else {
                return null;
            }
        }
    }
    
    //测试类
    public class Test {
        public static void main(String[] args) throws Exception {
            
          	//使用默认的类加载器
            System.out.println(Test.class.getClassLoader());  
          
            //使用自定义的类加载器
            MyClassLoader myClassLoader = new MyClassLoader("/Users/liyefei6/classes/");
            Class<?> person1Class = myClassLoader.loadClass("com.demo.classloader.Person1");
            System.out.println(person1Class.getClassLoader());
        }
    }
    
    //打印结果:
    sun.misc.Launcher$AppClassLoader@18b4aac2//使用的是系统类加载器AppClassLoader
    com.demo.classloader.MyClassLoader@1fb3ebeb//使用的是自定义类加载器MyClassLoader
    

    从系统提供的类加载器扫描的路径分析到/Users/liyefei6/classes/路径是不会被扫描到的,就不会被类加载器所加载,所以打印出来的结果是自定义类加载器,而Test类是在classpath下,是会被系统类加载器所加载到的,所以打印的结果是AppClassLoader。有意思的是Person1类中有静态代码块但并未执行,java中严格规定了只有以下五种情况会触发类的初始化:

    • 遇到new、getstatic、putstatic、invokestatic这四条字节码指令
    • 使用Java.lang.refect包的方法对类进行反射调用
    • 当初始化一个类的时候,如果发现其父类还没有进行初始化,则需要先触发其父类的初始化
    • 当虚拟机启动时,用户需要指定一个要执行的主类,虚拟机会先执行该主类
    • JDK1.5中方法句柄所对应的类没有进行过初始化,则需要先触发其初始化

    简单总结就是类加载不一定会初始化,初始化一定需要类加载,Person1类如下所示:

    public class Person1 {
        private String name;
        static {
            System.out.println("静态代码块执行 -> Person1");
        }
    
        public Person1() {
            System.out.println("构造方法执行 -> Person1");
        }
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    }
    
    

二 类加载器执行过程

双亲委派模型是一种很好的设计思想,它不仅可以避免重复加载,还可以避免核心类被篡改。比如自己重写个java.lang.Object并放到Classpath中,如果没有双亲委派的话直接自己执行了,那不安全。双亲委派可以保证这个类只能被顶层Bootstrap Classloader类加载器加载,从而确保只有JVM中有且仅有一份正常的java核心类。如果有多个的话,那么就乱套了。比如相同的类instance of可能返回false,因为可能父类不是同一个类加载器加载的Object。借鉴比较经典的两幅图来介绍类加载的过程。

如果一个类加载器收到了类加载的请求,他首先会从自己缓存里查找是否之前加载过这个class,如果加载过直接返回,如果没加载过的话他不会自己亲自去加载,他会把这个请求委派给父类加载器去完成,每一层都是如此,类似递归,一直递归到顶层父类。也就是Bootstrap ClassLoader,只要加载完成就会返回结果,如果顶层父类加载器无法加载此class,则会返回去交给子类加载器去尝试加载,若最底层的子类加载器也没找到,则会抛出ClassNotFoundException
但是在有些场景下需要打破双亲委派的思想,比如Tomcat、热部署等等。

Tomcat为什么要破坏双亲委派模型?
因为一个Tomcat可以部署N个web应用,但是每个web应用都需要有自己的classloader,才能互不干扰。比如web1里面有com.test.A.class,web2里面也有com.test.A.class,如果只有一套classloader,出现了两个重复的类路径,会相互影响和冲突,所以tomcat打破了,他是线程级别的,不同web应用是不同的classloader。
热部署打破双亲委派是因为jvm会通过默认的loadClass()方法先找缓存,如果加载过就不会再次加载,你改了class字节码也不会热加载,所以自定义ClassLoader,去掉找缓存那部分,直接就去加载,也就是每次都重新加载。

三 SPI介绍

「SPI」 全称为 (Service Provider Interface) ,是JDK内置的一种服务提供发现机制。 目前有不少框架用它来做服务的扩展实现, 简单来说,它就是一种动态替换发现的机制。

按照双亲委派模型的思想,JDBC规范中定义的类是由启动类加载器去加载的,它是是无法使用mysql或者oracle驱动包里由系统类加载器加载出来的类的。为了解决这个问题,又引入了SPI的规范,使启动类加载器可以调用系统类加载器加载出的类。

下面举例说明SPI的使用,首先创建一个仅包含扩展接口的maven工程spi-interface,然后创建一个接口SPIService。

//SPIService.java
package com.liyefei.service;

public interface SPIService {
    void println(String str);
}

<!-- pom.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
 
    <groupId>com.liyefei.interface</groupId>
    <artifactId>spi-interface</artifactId>
    <version>1.0.0</version>
</project>

接下来创建一个maven工程spi-impl作为扩展的实现,工程的资源文件下需要创建META-INF/services目录,目录中需要创建一个用扩展接口全路径名称的文件,文件内容为接口实现类的全路径名,代码如下:

//OneServiceImpl.java
package com.liyefei.one;
import com.liyefei.service.SPIService;

public class OneServiceImpl implements SPIService {
    @Override
    public void println(String str) {
        System.out.println("impl service:" + str);
    }
}
<!-- pom.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
   
    <groupId>com.liyefei.impl</groupId>
    <artifactId>spi-impl</artifactId>
    <version>1.0.0</version>

    <dependencies>
        <dependency>
            <groupId>com.liyefei.interface</groupId>
            <artifactId>spi-interface</artifactId>
            <version>1.0.0</version>
        </dependency>
    </dependencies>
</project>
文件目录如下:
├── spi-impl
		│		├──src
		│		│		├──main
		│		│				├──java
		│		│						└──OneServiceImpl
		│		│				├──resources
		│		│						├──META-INF
		│		│								├──services
		│		│										├──com.liyefei.service
		└──pom.xml

最后创建一个spi-test的maven工程,用于测试,工程依赖扩展的实现,代码如下:

//Test.java
package com.liyefei.test;
import com.liyefei.service.SPIService;

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

public class Test {
    public static void main(String[] args) {
        ServiceLoader<SPIService> spiServices = ServiceLoader.load(SPIService.class);
        Iterator<SPIService> iterator = spiServices.iterator();
        while (iterator.hasNext()) {
            iterator.next().println("run");
        }
    }
}
//打印结果:impl service:run
<!-- pom.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  
    <dependencies>
        <dependency>
            <groupId>com.liyefei.impl</groupId>
            <artifactId>spi-impl</artifactId>
            <version>1.0.0</version>
        </dependency>
    </dependencies>
</project>

从打印结果可以看出,接口SPIService的实现类ONEServiceImpl成功执行,想要深究原因只能查看源码,进入load方法发现使用了上下文类加载器,源码如下:

//ServiceLoader.class
public static <S> ServiceLoader<S> load(Class<S> service) {
  			//上下文类加载器
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        return ServiceLoader.load(service, cl);
    }

通过源码一路跟进可以看到,fullName就是由接口扩展实现工程中资源文件夹下"META-INF/services/" + 接口全路径名称组成,最后通过Class.forName使用上下文类加载器反射出所需要的类。

//ServiceLoader.class
private static final String PREFIX = "META-INF/services/";
String fullName = PREFIX + service.getName();
if (loader == null)
	configs = ClassLoader.getSystemResources(fullName);
else
	configs = loader.getResources(fullName);
//ServiceLoader.class
loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
Class<?> c = null;
try {
  c = Class.forName(cn, false, loader);
} catch (ClassNotFoundException x) {
  fail(service, "Provider " + cn + " not found");
}

那么上下文类加载器中存放的是那个类加载器呢?通过类加载器的主要类Launcher.class源码中可以看到线程上下文类加载器中默认存放的类加载器为系统类加载器。通过Thread.currentThread().setContextClassLoader(this.loader)这行代码打破了双亲委派模型。

//Launcher.class
public class Launcher {
    private static URLStreamHandlerFactory factory = new Launcher.Factory();
    private static Launcher launcher = new Launcher();
    private static String bootClassPath = System.getProperty("sun.boot.class.path");
    private ClassLoader loader;
    private static URLStreamHandler fileHandler;

    public static Launcher getLauncher() {
        return launcher;
    }

    public Launcher() {
        Launcher.ExtClassLoader var1;
        try {
            //扩展类加载器
            var1 = Launcher.ExtClassLoader.getExtClassLoader();
        } catch (IOException var10) {
            throw new InternalError("Could not create extension class loader", var10);
        }

        try {
            //系统类加载器
            this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
        } catch (IOException var9) {
            throw new InternalError("Could not create application class loader", var9);
        }

        //此处即为jvm启动时设置的上下文类加载器,即系统类加载器
        Thread.currentThread().setContextClassLoader(this.loader);
        .....

SPI打破了双亲委派模型,具体的底层实现可以分离出来,将每组实现打包成不同的jar,在具体使用时根据需要使用不同的jar即可,方便了应用的扩展,JDBC规范中加载各个供应商的驱动就是通过这种方式去实现的。

四 总结与展望

工作和生活中只要肯思考,就会有收获。通过一个类加载器可以挖掘出很多的知识点,慢慢就会明白很多应用的实现原理,想要弄清楚原理必须要通过源码去发掘、去实践,也希望自己可以通过多发掘别人的源码、学习别人的设计思想,有朝一日可以在技术的道路上越走越远。

posted @ 2021-03-20 13:05  温故方能知新  阅读(42)  评论(0)    收藏  举报