基本概念

java的类加载器(class loader),用来将java的字节码文件(.class)读取到内存中,生成一个Java.lang.Class对象。通过Class类的newInstance()方法,可以生成一个被加载类的对象。

不同的类加载器可以从不同的来源来加载二进制字节码文件,主要包括:

  • 本地文件
  • 通过网路加载class文件
  • 动态在内存中生成,如JSP文件
  • 从JAR包中加载class文件

每一个Class类中包含加载该Class的class loader引用,通过Class.getClassLoader()方法可以得知。对于数组类型,其Class对象不是由类加载器创建的,而是由Java运行时动态创建的,如果数组的元素类型是引用类型,通过getClassLoader()方法将返回该引用类型的类加载器,如果是原生数据类型,则返回空。

Java中类加载器分为系统提供的类加载器和用户自定义的类加载器:

  • 系统提供类加载器,包括根类加载器(Bootstrap)、扩展类加载器(Ext)和应用类加载器 (SystemClassLoader)
  • 用户自定义的类加载器,通过继承抽象类java.lang.ClassLoader类实现,必须覆写findClass()方法,父类中该类的默认实现是抛出一个异常。

java的类加载器负责从不同的路径来源来加载二进制字节码文件,类加载器之前存在父子关系,加载类时通过双亲委托模型,将加载类的优先权层次上抛,只有当父类加载器无法加载时,才转交当前类加载器加载。

重要方法

  1. 默认构造方法
1protected ClassLoader() {
2        this(checkCreateClassLoader(), getSystemClassLoader());
3    }

可见,默认的父类加载器是应用类加载器

  1. 可以设置父类加载器的构造方法
1 protected ClassLoader(ClassLoader parent) {
2        this(checkCreateClassLoader(), parent);
3    }

设置父类加载器用于委托加载类。

  1. loadClass(String name) : 加载名称为name的类,包含加载类的总体流程
  2. findClass(String name):查找二进制clas文件并生成Class对象,被loadClass方法调用
  3. Class defineClass(String name, byte[] b, int off, int len) :将二进制数据转换成Class对象,被findClass方法调用

双亲委托模型

image

如上图所示,一部了然了。系统提供的类加载器主要有下面三个:

  • 引导类加载器(bootstrap class loader):也叫做启动类加载器用于加载JRE/lib/rt.jar中核心类。原生实现的,不继承自java.lang.ClassLoader类
  • 扩展类加载器(extensions class loader):用于加载JRE/lib/ext目录下的类
  • 系统类加载器:也叫做应用类加载器,用于当前项目CLASSPATH下的类

开发人员可以通过继承 java.lang.ClassLoader类的方式实现自己的类加载器,以满足一些特殊的需求。各个加载器根据父子关系形成层级结构,除了根类加载器之外,其余的类加载器都有且只有一个父类加载器。在加载类时,一层一层向上委托,直到没有父类加载器为止,也就是到根加载器,然后从根类加载器开始向下逐层尝试加载,如果在某一层加载成功就宣告加载结束。这样保证了高层次的类加载器有加载类的优先权。

loadClass关键源码:

 1protected Class<?> loadClass(String name, boolean resolve)
 2        throws ClassNotFoundException
 3    {
 4        synchronized (getClassLoadingLock(name)) {
 5            // First, check if the class has already been loaded
 6            // 检查是否已经加载过,如果已经加载过,直接返回
 7            Class<?> c = findLoadedClass(name);
 8            if (c == null) {
 9                long t0 = System.nanoTime();
10                try {
11                    if (parent != null) {
12                    // 委托给父类加载器加载
13                        c = parent.loadClass(name, false);
14                    } else {
15                    // 如果父类加载器为空,说明当前是根类加载器
16                        c = findBootstrapClassOrNull(name);
17                    }
18                } catch (ClassNotFoundException e) {
19                    // ClassNotFoundException thrown if class not found
20                    // from the non-null parent class loader
21                }
22                // 父类加载器加载不成功,则自己亲自去加载
23                if (c == null) {
24                    // If still not found, then invoke findClass in order
25                    // to find the class.
26                    long t1 = System.nanoTime();
27                    // 加载类并调用defineClass方法生成Class对象
28                    c = findClass(name);
29
30                    // this is the defining class loader; record the stats
31                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
32                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
33                    sun.misc.PerfCounter.getFindClasses().increment();
34                }
35            }
36            if (resolve) {
37            // 解析类
38                resolveClass(c);
39            }
40            return c;
41        }
42    }

自定义一个类加载器

集成ClassLoader自定义一个类加载器,我们设定加载的路径是:D:\dongzhi\Desktop

 1package io.dongzhi;
 2import java.io.*;
 3public class MyClassLoader extends ClassLoader {
 4    private final String ext = ".class";
 5    private String path;
 6
 7    public MyClassLoader(String path) {
 8        this.path = path;
 9    }
10
11    @Override
12    public String toString() {
13        return super.toString();
14    }
15
16    @Override
17    protected Class<?> findClass(String name) {
18        String fullPath = this.path + "/" + name.replace(".", "/") + ext;
19        try (ByteArrayOutputStream bos = new ByteArrayOutputStream();
20             FileInputStream fis = new FileInputStream(new File(fullPath));) {
21            byte[] buffer = new byte[512];
22            int count = 0;
23            while ((count = fis.read(buffer)) != -1) {
24                bos.write(buffer, 0, count);
25            }
26            return super.defineClass(name, bos.toByteArray(), 0, bos.size());
27        } catch (FileNotFoundException e) {
28            e.printStackTrace();
29        } catch (IOException e) {
30            e.printStackTrace();
31        }
32        return null;
33    }
34
35    public static void main(String[] args) throws ClassNotFoundException {
36        MyClassLoader loader = new MyClassLoader("D:\\dongzhi\\Desktop"); // 自定义路径
37        Class<?> clazz = loader.loadClass("io.dongzhi.Person");
38        System.out.println("class:" + clazz);
39        System.out.println("calss loader:" + clazz.getClassLoader());
40    }
41}
  • 当前项目中有Person这个类时,打印结果为:
1class:class io.dongzhi.Person
2calss loader:sun.misc.Launcher$AppClassLoader@18b4aac2
3
4Process finished with exit code 0
  • 当前项目中没有Person这个类时,打印结果:
java.io.FileNotFoundException: D:\dongzhi\Desktop\io\dongzhi\Person.class (系统找不到指定的路径。)
	at java.io.FileInputStream.open0(Native Method)
	at java.io.FileInputStream.open(FileInputStream.java:195)
	at java.io.FileInputStream.(FileInputStream.java:138)
	at io.dongzhi.MyClassLoader.findClass(MyClassLoader.java:21)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
	at io.dongzhi.MyClassLoader.main(MyClassLoader.java:38)
class:null
Exception in thread "main" java.lang.NullPointerException
	at io.dongzhi.MyClassLoader.main(MyClassLoader.java:40)

Process finished with exit code 1

  • 当前项目中没有Person这个类,但是D:/dongzhi/Desktop这个目录有Person.class这个类,打印结果如下:
 1java.io.FileNotFoundException: D:\dongzhi\Desktop\io\dongzhi\Person.class (系统找不到指定的路径。)
 2	at java.io.FileInputStream.open0(Native Method)
 3	at java.io.FileInputStream.open(FileInputStream.java:195)
 4	at java.io.FileInputStream.<init>(FileInputStream.java:138)
 5	at io.dongzhi.MyClassLoader.findClass(MyClassLoader.java:21)
 6	at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
 7	at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
 8	at io.dongzhi.MyClassLoader.main(MyClassLoader.java:38)
 9class:null
10Exception in thread "main" java.lang.NullPointerException
11	at io.dongzhi.MyClassLoader.main(MyClassLoader.java:40)
12
13Process finished with exit code 1
  • 当前项目中没有Person这个类,把Person.class放置到D:/dongzhi/Desktop/io/dongzhi/Person.class,打印结果如下:
1class:class io.dongzhi.Person
2calss loader:io.dongzhi.MyClassLoader@1540e19d
3
4Process finished with exit code 0

以上几个测试中,根据双亲委托机制,当MyClassLoader加载Person.class类时,先委托给父类加载器AppClassLoader,AppClassLoader加载的范围是当前项目的类路径,所以第一种情况中能加载成功,第二种情况中,项目类路径中没有Person类,自定义的类加载器就会起作用,会去加载D:/dongzhi/Desktop目录下的类,由于类不存在,所以报异常。第三、四种情况中,MyClassLoader加载类时,把包拆分成路径去加载类,所以类的正确路径应当是:D:/dongzhi/Desktop/io/dongzhi/Person.class。

命名空间

紧接着,修改测试用例,将main方法修改如下,定义2个类加载器:

 1    public static void main(String[] args) throws ClassNotFoundException {
 2        MyClassLoader loader = new MyClassLoader("D:\\dongzhi\\Desktop"); // 自定义loader1
 3        Class<?> clazz = loader.loadClass("io.dongzhi.Person");
 4        System.out.println("class:" + clazz + ",hashcode" + clazz.hashCode());
 5        System.out.println("calss loader:" + clazz.getClassLoader());
 6
 7        MyClassLoader loader2 = new MyClassLoader("D:\\dongzhi\\Desktop"); // 自定义loader2
 8        Class<?> clazz2 = loader2.loadClass("io.dongzhi.Person");
 9        System.out.println("class2:" + clazz2 + ",hashcode" + clazz2.hashCode());
10        System.out.println("calss loader2:" + clazz2.getClassLoader());
11    }

接着测试以下情况:

  1. 当当前项目类路径中存在Person.class时,输出如下:
1class:class io.dongzhi.Person,hashcode356573597
2calss loader:sun.misc.Launcher$AppClassLoader@18b4aac2
3class2:class io.dongzhi.Person,hashcode356573597
4calss loader2:sun.misc.Launcher$AppClassLoader@18b4aac2
5
6Process finished with exit code 0
  1. 当当前项目类路径不存在Person类,而是存在于自定义加载器的类路径中时,即:D:/dongzhi/Desktop/io/dongzhi/Person.class。则输出如下:
1class:class io.dongzhi.Person,hashcode21685669
2calss loader:io.dongzhi.MyClassLoader@1540e19d1577875235796
3class2:class io.dongzhi.Person,hashcode325040804
4calss loader2:io.dongzhi.MyClassLoader@7f31245a1577875235797
5
6Process finished with exit code 0

分析: 定义了两个类加载器loader和loader1,它们的加载路径是一样的,第1种情况中,输出的结果显示,2个类加载器为同一个实例,加载的Class也为同一个实例,而第2种情况中,类加载器loader和loader1为2个不同的实例,加载的Person也是不同的实例。原因是什么呢?

第1种情况中,当前项目类路径存在Person类时,loader委托父类加载器AppClas正常加载成功,loader2委托AppClassLoader加载Person时,父类加载器发现Person类已经由其加载过了,直接从缓存中取出返回被加载的Person类。 第二种情况中,加载类的任务落在了loader和loader1身上,显然它们是不同的实例,类加载器不同,它们加载的类也是不同的实例。

由此可见,类加载器(以及所有的父类加载器)构成了被加载类的命名空间,同一个类加载器的实例,对同一个类只会加载一次,且缓存起来,不同类加载器的实例,加载同一个类时,得到的Class是不同的实例。

类的卸载

一个类的生命周期取决于对应的Class对象的生命周期,当该Class对象不可触及时,这个类在方法区中的数据就会被清除掉。

由JVM自带的类加载器所加载的类,在整个JVM生命周期中都不会被卸载。JVM本身会引用那些自带的类加载器,自带的类加载器又会引用它们所加载的类的Class对象,所以这些Class对象始终是可触及的。

用户自定义的类加载器加载的类是可以卸载的。

加上-XX:+TraceClassUnloading这个参数,在上面的例子中显式调用System.gc(),可以看到类的卸载日志。

参考资料

https://www.ibm.com/developerworks/cn/java/j-lo-classloader/ https://blog.csdn.net/m0_38075425/article/details/81627349