【Java虚拟机8】自定义类加载器、类加载器命名空间、类的卸载

前言

学习类加载器就一定要自己实现一个类加载器,今天就从一个简单的自定义类加载器说起。

自定义类加载器

例1

一个简单的类加载器,从一个给定的二进制名字读取一个字节码文件的内容,然后生成对应的class对象。

package com.jamie.jvmstudy;

import java.io.*;

public class CustomizedClassLoader extends ClassLoader {

    private String classLoaderName;

    private String fileExtension = ".class";

    public CustomizedClassLoader(String classLoaderName) {
        super(); //如果调用默认构造器,代表默认的父类加载器是系统类加载器SystemClassLoader
        this.classLoaderName = classLoaderName;
    }

    public CustomizedClassLoader(ClassLoader parent, String classLoaderName) {
        super(parent); //如果指定父类加载器,那么该构造器执行完之后,这个类加载器就有指定的parent了。(默认是系统类加载器)
        this.classLoaderName = classLoaderName;
    }

    @Override
    public Class<?> findClass(String className) throws ClassNotFoundException {
        byte[] data = this.loadClassData(className);

        return this.defineClass(className, data, 0, data.length);
    }

    private byte[] loadClassData(String className) {
        byte[] data = null;
        try(InputStream is = new FileInputStream(new File(className + this.fileExtension));
            ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
            int ch;
            while(-1 != (ch = is.read())) {
                baos.write(ch);
            }
            data = baos.toByteArray();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return data;
    }

    public static void main(String[] args) throws Exception {
        CustomizedClassLoader customizedClassLoader = new CustomizedClassLoader("jamie loader");
        test(customizedClassLoader);
    }

    private static void test(ClassLoader classLoader) throws Exception {
        Class<?> myClass = classLoader.loadClass("com.jamie.jvmstudy.TestClassLoader");
        Object o = myClass.newInstance();
        System.out.println(String.format("classLoader in this method is [%s]", classLoader));
        System.out.println(String.format("object [%s] has been created by [%s].", o, myClass.getClassLoader()));
        System.out.println(String.format("class loader of CustomizedClassLoader is [%s]", classLoader.getClass().getClassLoader()));
    }
}

运行结果如下:

classLoader in this method is [com.jamie.jvmstudy.CustomizedClassLoader@4b67cf4d]
object [com.jamie.jvmstudy.TestClassLoader@7ea987ac] has been created by [sun.misc.Launcher$AppClassLoader@14dad5dc].
class loader of CustomizedClassLoader is [sun.misc.Launcher$AppClassLoader@14dad5dc]

这里重点说明一下:示例中的"com.jamie.jvmstudy.TestClassLoader"Class是被系统类加载器加载的,而不是我们自定义的加载器。
原因:

  1. 自定义的加载器CustomizedClassLoader的父类构造器是系统类加载器。因为我们加载类调用的方法是:classLoader.loadClass("com.jamie.jvmstudy.TestClassLoader")
  2. 默认的loadClass()方法实现就是双亲委派的源码实现,因为系统类加载器会在当前的classpath(类路径)下查找是否存在匹配的"binary name",如果存在,则系统类加载器加载成功。
  3. 所以只要类路径下存在匹配的二进制名字的字节码,就会被系统类加载器成功加载。

例2

基于例1的基础上,自定义加载器获取字节码文件的内容改为从一个指定的路径中读取。并且传入一个非classpath的路径,去加载某个字节码。
因为自定义的类加载器默认的父类加载器是系统类加载器,运行下例的时候,需要在编译之后把类路径下的com.jamie.jvmstudy.TestClassLoader字节码文件删除(防止[双亲委托机制]使[系统类加载器AppClassLoader]把指定类加载进虚拟机),然后运行本例。

package com.jamie.jvmstudy;

import java.io.*;

public class CustomizedClassLoader extends ClassLoader {

    private String classLoaderName;

    private String path;

    private String fileExtension = ".class";

    public CustomizedClassLoader(String classLoaderName) {
        super(); //如果调用默认构造器,代表默认的父类加载器是系统类加载器SystemClassLoader
        this.classLoaderName = classLoaderName;
    }

    public CustomizedClassLoader(ClassLoader parent, String classLoaderName) {
        super(parent);
        this.classLoaderName = classLoaderName;
    }

    @Override
    public Class<?> findClass(String className) throws ClassNotFoundException {
        System.out.println("Self findClass() invoked");
        byte[] data = this.loadClassData(className);
        return this.defineClass(className, data, 0, data.length);
    }

    private byte[] loadClassData(String className) {
        byte[] data = null;
        className = className.replace(".", "/");
        try(InputStream is = new FileInputStream(new File(path + className + this.fileExtension));
            ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
            int ch;
            while(-1 != (ch = is.read())) {
                baos.write(ch);
            }
            data = baos.toByteArray();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return data;
    }

    public static void main(String[] args) throws Exception {
        CustomizedClassLoader loader1 = new CustomizedClassLoader("jamie loader1");
        loader1.setPath("D:/temp/");
        Class<?> myClass1 = loader1.loadClass("com.jamie.jvmstudy.TestClassLoader");
        System.out.println(String.format("Hashcode of myClass1 is [%s].", myClass1.hashCode()));
        System.out.println(String.format("myClass1 is [%s]", myClass1));
        System.out.println();

        CustomizedClassLoader loader2 = new CustomizedClassLoader("jamie loader2");
        loader2.setPath("D:/temp/");
        Class<?> myClass2 = loader2.loadClass("com.jamie.jvmstudy.TestClassLoader");
        System.out.println(String.format("Hashcode of myClass2 is [%s].", myClass2.hashCode()));
        System.out.println(String.format("myClass2 is [%s]", myClass2));

        System.out.println();
        System.out.println("myClass1 == myClass2 ? " + (myClass1 == myClass2));
    }

    public void setPath(String path) {
        this.path = path;
    }
}

运行结果如下:

Self findClass() invoked
Hashcode of myClass1 is [1956725890].
myClass1 is [class com.jamie.jvmstudy.TestClassLoader]

Self findClass() invoked
Hashcode of myClass2 is [21685669].
myClass2 is [class com.jamie.jvmstudy.TestClassLoader]

myClass1 == myClass2 ? false

结论:由运行结果可以看出,此时的虚拟机中出现了两个不一样的TestClassLoader.class对象。
这就引入了类加载器的命名空间的问题
我们之前理解的“Class对象只存在一份”是基于同一个类加载器的命名空间来说的。

类加载器的命名空间

  • 每个类加载器都有自己的命名空间。命名空间由该加载器和所有父加载器所加载的类组成。
  • 在同一个命名空间中,不会出现类的完整名字(包括类的包名)相同的两个类。
  • 在不同的命名空间中,有可能会出现类的完整名字(包括类的包名)相同的两个类。

后续写了一篇文章类加载器之命名空间详解,里面有对【命名空间】更加详细的分析与示例。感兴趣请跳转。

类的卸载

  • 当一个类被加载、连接和初始化之后,它的生命周期就开始了。当代表该类的Class对象不再被引用,既不可达时,Class对象就会结束生命周期,该类在方法区内的数据也会被卸载,从而结束该类的生命周期。
  • 一个类何时结束生命周期,取决于代表它的Class对象何时结束生命周期。
  • 被Java虚拟机自带的ClassLoader加载的类,是不会被卸载的。因为JVM本身会始终引用这些ClassLoader,而这些ClassLoader始终会引用它加载的所有类的Class对象。所以它们永远可达。
  • 只有自定义的类加载器加载的类,才有可能被卸载。

类卸载的证明:

基于上述例2,改造一下main方法,并在执行时,添加JVM运行参数:-XX:+TraceClassUnloading

    public static void main(String[] args) throws Exception {
        CustomizedClassLoader loader1 = new CustomizedClassLoader("jamie loader1");
        loader1.setPath("D:/temp/");
        Class<?> myClass1 = loader1.loadClass("com.jamie.jvmstudy.TestClassLoader");
        System.out.println(String.format("Hashcode of myClass1 is [%s].", myClass1.hashCode()));
        System.out.println(String.format("myClass1 is [%s]", myClass1));
        System.out.println();

        loader1 = null;
        myClass1 = null;
        System.gc();

        CustomizedClassLoader loader2 = new CustomizedClassLoader("jamie loader2");
        loader2.setPath("D:/temp/");
        Class<?> myClass2 = loader2.loadClass("com.jamie.jvmstudy.TestClassLoader");
        System.out.println(String.format("Hashcode of myClass2 is [%s].", myClass2.hashCode()));
        System.out.println(String.format("myClass2 is [%s]", myClass2));

        System.out.println();
        System.out.println("myClass1 == myClass2 ? " + (myClass1 == myClass2));
        Thread.sleep(50000); //为了查看类的卸载情况增加延时
    }

运行结果:可以看到TestClassLoader类被卸载一次。

=====Self findClass() invoked=====
Hashcode of myClass1 is [1956725890].
myClass1 is [class com.jamie.jvmstudy.TestClassLoader]

[Unloading class com.jamie.jvmstudy.TestClassLoader 0x00000007c0061028]
=====Self findClass() invoked=====
Hashcode of myClass2 is [21685669].
myClass2 is [class com.jamie.jvmstudy.TestClassLoader]

myClass1 == myClass2 ? false

增加main方法睡眠时间,使用Java VisualVM可以看到下图:

posted @ 2020-08-05 22:52  可苦可乐  阅读(430)  评论(0编辑  收藏  举报