目录

ClassLoader

定义

根据一个类的全限定名来读取此类的二进制字节流到JVM中,然后转换为一个与目标类对应的java.lang.Class对象实例。实现这个动作的代码模块称为“类加载器”。

类加载的种类、职责、双亲委派

类加载器的结构

https://gitee.com/lienhui68/picStore/raw/master/null/20200801222728.png

  • BootstrapClassLoader:启动类类加载器,它用来加载<JAVA_HOME>/jre/lib路径,-Xbootclasspath参数指定的路径以<JAVA_HOME>/jre/classes中的类。BootStrapClassLoader是由c++实现的。

  • ExtClassLoader:拓展类类加载器,它用来加载<JAVA_HOME>/jre/lib/ext路径以及java.ext.dirs系统变量指定的类路径下的类。

  • AppClassLoader:应用程序类类加载器,它主要加载应用程序ClassPath下的类(包含jar包中的类)。它是java应用程序默认的类加载器。

  • 用户自定义类加载器:用户根据自定义需求,自由的定制加载的逻辑,继承AppClassLoader,仅仅覆盖findClass()即将继续遵守双亲委派模型。

  • ThreadContextClassLoader:线程上下文加载器,它不是一个新的类型,更像一个类加载器的角色,ThreadContextClassLoader可以是上述类加载器的任意一种,但往往是AppClassLoader,作用我们后面再说。

    上面说的“继承”是指双亲委派中的层次关系

在虚拟机启动的时候会初始化BootstrapClassLoader,然后在Launcher类中去加载ExtClassLoader、AppClassLoader,并将AppClassLoader的parent设置为ExtClassLoader,并设置线程上下文类加载器。

Launcher是JRE中用于启动程序入口main()的类,让我们看下Launcher的代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
public Launcher() {
        Launcher.ExtClassLoader var1;
        try {
            //加载扩展类类加载器
            var1 = Launcher.ExtClassLoader.getExtClassLoader();
        } catch (IOException var10) {
            throw new InternalError("Could not create extension class loader", var10);
        }

        try {
            //加载应用程序类加载器,并设置parent为extClassLoader
            this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
        } catch (IOException var9) {
            throw new InternalError("Could not create application class loader", var9);
        }
        //设置默认的线程上下文类加载器为AppClassLoader
        Thread.currentThread().setContextClassLoader(this.loader);
        //此处删除无关代码。。。
        }

上面画的几种类加载器是遵循双亲委派模型的,其实就是,当一个类加载器去加载类时先尝试让父类加载器去加载,如果父类加载器加载不了再尝试自身加载。这也是我们在自定义ClassLoader时java官方建议遵守的约定。

ExtClassLoader为什么没有设置parent?

让我们看看下面代码的输出结果

1
2
3
4
5
6
 public static void main(String[] args) throws ClassNotFoundException {
        ClassLoader classLoader = Test.class.getClassLoader();
        System.out.println(classLoader);
        System.out.println(classLoader.getParent());
        System.out.println(classLoader.getParent().getParent());
    }

看看结果是啥

1
2
3
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@5a61f5df
null

因为BootstrapClassLoader是由c++实现的,所以并不存在一个Java的类,因此会打印出null,所以在ClassLoader中,null就代表了BootStrapClassLoader(有些片面)。

那么双亲委派的好处是什么呢?

双亲委派模型能保证基础类仅加载一次,不会让jvm中存在重名的类。比如String.class,每次加载都委托给父加载器,最终都是BootstrapClassLoader,都保证java核心类都是BootstrapClassLoader加载的,保证了java的安全与稳定性。

自定义ClassLoader

自己实现ClassLoader时只需要继承ClassLoader类,然后覆盖findClass(String name)方法即可完成一个带有双亲委派模型的类加载器。

自己实现ClassLoader时只需要继承ClassLoader类,然后覆盖findClass(String name)方法即可完成一个带有双亲委派模型的类加载器。

我们看下ClassLoader#loadClass的代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // 查看是否已经加载过该类,是使用native方法实现的
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    //父加载器不为空则先让父加载器加载
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                    //父类是null就是BootstrapClassLoader,使用启动类类加载器加载
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // 父类类加载器不能加载该类
                }

                //如果父类加载器未加载该类
                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    //让当前类加载器加载
                    c = findClass(name);
                }
            }
            return c;
        }
    }

经典的模板方法模式,子类只需要实现findClass,关心从哪里加载即可。还有一点,parent需要自己设置,可以放在构造函数做这个事情。

为什么不继承AppClassLoader呢?

因为它和ExtClassLoader都是Launcher的静态类,都是包访问路径权限的。

类加载为什么是线程安全的

synchronized (getClassLoadingLock(name)) 看到这行代码,我们能知道的是,这是一个同步代码块,那么synchronized的括号中放的应该是一个对象。我们来看getClassLoadingLock(name)方法的作用是什么:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
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;
    }

以上是getClassLoadingLock(name)方法的实现细节,我们看到这里用到变量parallelLockMap,根据这个变量的值进行不同的操作,如果这个变量是Null,那么直接返回this,如果这个属性不为Null,那么就新建一个对象,然后在调用一个putIfAbsent(className, newLock);方法来给刚刚创建好的对象赋值,这个方法的作用我们一会讲。那么这个parallelLockMap变量又是哪来的那,我们发现这个变量是ClassLoader类的成员变量:

1
private final ConcurrentHashMap<String, Object> parallelLockMap;

这个变量的初始化工作在ClassLoader的构造函数中:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
private ClassLoader(Void unused, ClassLoader parent) {
        this.parent = parent;
        if (ParallelLoaders.isRegistered(this.getClass())) {
            parallelLockMap = new ConcurrentHashMap<>();
            package2certs = new ConcurrentHashMap<>();
            domains =
                Collections.synchronizedSet(new HashSet<ProtectionDomain>());
            assertionLock = new Object();
        } else {
            // no finer-grained lock; lock on the classloader instance
            parallelLockMap = null;
            package2certs = new Hashtable<>();
            domains = new HashSet<>();
            assertionLock = this;
        }
    }

这里我们可以看到构造函数根据一个属性ParallelLoadersRegistered状态的不同来给parallelLockMap 赋值。 我去,隐藏的好深,好,我们继续挖,看看这个ParallelLoaders又是在哪赋值的呢?我们发现,在ClassLoader类中包含一个静态内部类private static class ParallelLoaders,在ClassLoader被加载的时候这个静态内部类就被初始化。这个静态内部类的代码我就不贴了,直接告诉大家什么意思,sun公司是这么说的:Encapsulates the set of parallel capable loader types,意识就是说:封装了并行的可装载的类型的集合。

上面这个说的是不是有点乱,那让我们来整理一下:

首先,在ClassLoader类中有一个静态内部类ParallelLoaders,他会指定的类的并行能力,如果当前的加载器被定位为具有并行能力,那么他就给parallelLockMap定义,就是new一个 ConcurrentHashMap<>(),那么这个时候,我们知道如果当前的加载器是具有并行能力的,那么parallelLockMap就不是Null,这个时候,我们判断parallelLockMap是不是Null,如果他是null,说明该加载器没有注册并行能力,那么我们没有必要给他一个加锁的对象,getClassLoadingLock方法直接返回this,就是当前的加载器的一个实例。

如果这个parallelLockMap不是null,那就说明该加载器是有并行能力的,那么就可能有并行情况,那就需要返回一个锁对象。然后就是创建一个新的Object对象,调用parallelLockMapputIfAbsent(className, newLock)方法,这个方法的作用是:首先根据传进来的className,检查该名字是否已经关联了一个value值,如果已经关联过value值,那么直接把他关联的值返回,如果没有关联过值的话,那就把我们传进来的Object对象作为value值,className作为Key值组成一个map返回。然后无论putIfAbsent方法的返回值是什么,都把它赋值给我们刚刚生成的那个Object对象。

这个时候,我们来简单说明下getClassLoadingLock(String className)的作用,就是: 为类的加载操作返回一个锁对象。为了向后兼容,这个方法这样实现:如果当前的classloader对象注册了并行能力,方法返回一个与指定的名字className相关联的特定对象,否则,直接返回当前的ClassLoader对象。

自定义ClassLoader实现代码热部署

代码热替换,在不重启服务器的情况下可以修改类的代码并使之生效。

  1. 首先是自定义一个ClassLoader
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
public class MyClassLoader extends ClassLoader {
    //用于读取.Class文件的路径
    private String swapPath;
    //用于标记这些name的类是先由自身加载的
    private Set<String> useMyClassLoaderLoad;

    public MyClassLoader(String swapPath, Set<String> useMyClassLoaderLoad) {
        this.swapPath = swapPath;
        this.useMyClassLoaderLoad = useMyClassLoaderLoad;
    }

    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        Class<?> c = findLoadedClass(name);
        if (c == null && useMyClassLoaderLoad.contains(name)){
            //特殊的类让我自己加载
            c = findClass(name);
            if (c != null){
                return c;
            }
        }
        return super.loadClass(name);
    }

    @Override
    protected Class<?> findClass(String name) {
        //根据文件系统路径加载class文件,并返回byte数组
        byte[] classBytes = getClassByte(name);
        //调用ClassLoader提供的方法,将二进制数组转换成Class类的实例
        return defineClass(name, classBytes, 0, classBytes.length);
    }

    private byte[] getClassByte(String name) {
        String className = name.substring(name.lastIndexOf('.') + 1, name.length()) + ".class";
        try {
            FileInputStream fileInputStream = new FileInputStream(swapPath + className);
            byte[] buffer = new byte[1024];
            int length = 0;
            ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
            while ((length = fileInputStream.read(buffer)) > 0){
                byteArrayOutputStream.write(buffer, 0, length);
            }
            return byteArrayOutputStream.toByteArray();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return new byte[]{};
    }
}
  1. 自定义一个示例类,用于被我们自己的类加载器加载
1
2
3
4
5
public class Test {
    public void printVersion(){
        System.out.println("当前版本是1哦");
    }
}
  1. 写个定时任务,一直调用printVersion方法,观察输出,看我们是否替换成功。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
 public static void main(String[] args) {
        //创建一个2s执行一次的定时任务
        new Timer().schedule(new TimerTask() {
            @Override
            public void run() {
                String swapPath = MyClassLoader.class.getResource("").getPath() + "swap/";
                String className = "com.example.Test";

                //每次都实例化一个ClassLoader,这里传入swap路径,和需要特殊加载的类名
                MyClassLoader myClassLoader = new MyClassLoader(swapPath, Sets.newHashSet(className));
                try {
                    //使用自定义的ClassLoader加载类,并调用printVersion方法。
                    Object o = myClassLoader.loadClass(className).newInstance();
                    o.getClass().getMethod("printVersion").invoke(o);
                } catch (InstantiationException |
                        IllegalAccessException |
                        ClassNotFoundException |
                        NoSuchMethodException |
                        InvocationTargetException ignored) {
                }
            }
        }, 0,2000);
    }

操作步骤:

  1. 先编译下工程,将Test.class拷贝到swap文件夹下。

  2. 运行main方法,可观察到控制台一直输出“当前版本是1哦”。

  3. 修改Test#pringtVersion方法的源代码,将输出的内容改为"当前版本是2哦",然后编译工程,将新的Test.class拷贝到swap文件件下,并替换之前的Test.class。

    为什么需要o.getClass().getMethod(“printVersion”).invoke(o);这样通过反射获取method调用,不能先强转成Test,然后test.printVersion()吗?

    如果这么写

    1
    2
    
    Test test = (Test)o;
    o.printVersion();
    

    Test.class会隐性的被加载当前类的ClassLoader加载,当前Main方法默认的ClassLoader为AppClassLoader,而不是我们自定义的MyClassLoader。

    会发生什么?

    会抛出ClassCastException,因为一个类,就算包路径完全一致,但是加载他们的ClassLoader不一样,那么这两个类也会被认为是两个不同的类。

参考

改编自:好怕怕的类加载器