JVM学习笔记3_类加载器

JVM类加载器分为四种:

  • 根类加载器(Bootstrap ClassLoader): 加载 JRE/lib/rt.jar 或者 Xbootclasspath选项指定的jar包,由C++实现,不是ClassLoader子类

  • 扩展类加载器(Extension ClassLoader): 加载JRE/lib/ext/*.jar 或者 -Djava.ext.dirs 指定目录下的jar包

  • 系统类加载器(App ClassLoader): 加载ClASSPATH或者-Djava.class.path指定目录下的类和jar包

  • 用户自定义类加载器(Custom ClassLoader): 通过java.lang.ClassLoader的子类自定义加载class

系统类加载器和扩展类加载器都定义在sun.misc.Launcher类种,作为静态内部类呈现。

类加载器的父亲委托机制:

  1. 自底向上检查类是否已经加载
  2. 自顶向下尝试加载类

类加载器与初始化

请看如下代码:

package classloader1;

public class ClassLoaderTest {

    public static void main(String[] args) throws ClassNotFoundException {

        ClassLoader classLoader = ClassLoader.getSystemClassLoader();
        Class clazz1 = classLoader.loadClass("classloader1.C");

        System.out.println("-----------------------------");

        Class clazz = Class.forName("classloader1.C");

        System.out.println(clazz1 == clazz);

    }
}
class C {
    static {
        System.out.println("class C static block");
    }
}

输出结果:

-----------------------------
class C static block
true

结论: 调用classLoader.loadCLass方法加载一个类,并不是对类的主动使用,不会导致类的初始化。而反射Class.forName(“…”) 是对类主动使用的一种方式,会导致类的初始化。

获取ClassLoader的方式

  • 获得当前类的classLoader: clazz.getCLassLoader()

  • 获得当前线程上下文的classLoader: Thread.currentThread().getContextClassLoader();

  • 获得系统的classLoader: ClassLoader.getSystemClassLoader();

如何自定义类加载器

自定义类加载器步骤:

  1. 继承ClassLoader抽象类,重写findClass方法,该方法调用defineClass方法返回class对象
  2. 自定义loadClassData方法,该方法将类名字符串转换为字节数组
  3. 在真正去加载类的时候,应该调用loadClass方法,返回class对象

看一个具体自定义加载器的实例:

public class MyClassLoader extends ClassLoader {

    public MyClassLoader() {
        super(); //将系统类加载器作为该类加载器的父类加载器
    }

    public MyClassLoader(ClassLoader parent) {
        super(parent); //将传入的类加载器作为该类加载器的父类加载器
    }

    /**
     * 一定要重写ClassLoader抽象类的findClass方法
     *
     * @param name
     * @return
     */
    @Override
    protected Class findClass(String name) {
        byte[] bytes;
        try {
            bytes = loadClassData(name);
            //通过调用ClassLoader类的defineClass方法,将类的字节数组传入
            return this.defineClass(name, bytes, 0, bytes.length);
        } catch (IOException e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * 自定义私有方法,将磁盘上的class文件转换为字节数组
     *
     * @param className
     * @return
     * @throws IOException
     */
    private byte[] loadClassData(String className) throws IOException {

        className = className.replace(".", File.separator);

        FileInputStream fis = new FileInputStream(new File(className));
        ByteArrayOutputStream baos = new ByteArrayOutputStream();

        byte[] buffer = new byte[1000];
        int len;

        while (-1 != (len = fis.read(buffer, 0, buffer.length))) {
            baos.write(buffer, 0, len);
        }

        baos.close();
        fis.close();

        return baos.toByteArray();

    }

    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {

        MyClassLoader myClassLoader = new MyClassLoader();
        Class clazz = myClassLoader.loadClass("classloader1.ClassLoaderTest");

        Object object = clazz.newInstance();

        System.out.println(object);
        System.out.println(object.getClass().getClassLoader());

    }
}

输出结果如下:

classloader1.ClassLoaderTest@4554617c
sun.misc.Launcher$AppClassLoader@18b4aac2

 可以看到,虽然自定义了类加载器,但是最后的Object实例的类加载器是AppClassLoader, 而不是我们自定义的类加载器。

 什么原因呢?本质跟ClassLoader抽象类的loadClass(String name, boolean resolve)方法有关:

protected Class loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            Class c = findLoadedClass(name);
            if (c == null) {
                try {
                    if (parent != null) {
                        //只要父加载器不为空,递归调用该方法,委托父加载器去加载
                        c = parent.loadClass(name, false);
                    } else {
                        //父加载器为空,那么当前类加载器为根类加载器
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                }

                if (c == null) {
                    //如果还没加载成功,那么才会通过用户自定义的类加载器去加载
                    c = findClass(name);
                    //...
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }

 该方法简短的代码,已经将类加载器的双亲委托机制说明的很清楚了。

结果分析:
自定义类加载器MyClassLoader的默认父加载器是AppClassLoader, classloader1.ClassLoaderTest是自定义类,在classPath下,最终会被AppClassLoader加载成功, 根本轮不到MyClassLoader去加载该类。

问题来了, 如何才能让自己定义的类加载器去加载一个类呢?

打破规则就行,即让AppClassLoader加载失败。假如我们加载类A, 首先,我们将需要加载的类的包路径目录和类的class文件挪到classpath路径之外,并且删除classpath路径下类A产生的class文件(想想为啥)。

将代码稍稍改造一下:

public class MyClassLoader extends ClassLoader {

    private String basePath;

    private String extension = ".class";

    public MyClassLoader() {
        super(); //将系统类加载器作为该类加载器的父类加载器
    }

    public MyClassLoader(String basePath) {
        this.basePath = basePath;
    }


    public MyClassLoader(ClassLoader parent) {
        super(parent); //将传入的类加载器作为该类加载器的父类加载器
    }

    @Override
    protected Class findClass(String name) {
        System.out.println("MyClassLoader findClass invoked");
        byte[] bytes;
        try {
            bytes = loadClassData(name);
            return this.defineClass(name, bytes, 0, bytes.length);
        } catch (IOException e) {
            e.printStackTrace();
            return null;
        }
    }

    private byte[] loadClassData(String className) throws IOException {
        //通过绝对路径指定一个class文件的位置
        className = basePath + className.replace(".", File.separator) + extension;

        FileInputStream fis = new FileInputStream(new File(className));
        ByteArrayOutputStream baos = new ByteArrayOutputStream();

        byte[] buffer = new byte[1000];
        int len;

        while (-1 != (len = fis.read(buffer, 0, buffer.length))) {
            baos.write(buffer, 0, len);
        }

        baos.close();
        fis.close();

        return baos.toByteArray();

    }

    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {

        MyClassLoader myClassLoader = new MyClassLoader("C:\\Users\\Administrator\\Desktop\\classes\\");
        Class clazz = myClassLoader.loadClass("classloader1.ClassLoaderTest");
        System.out.println(clazz);

        Object object = clazz.newInstance();
        System.out.println(object.getClass().getClassLoader());
    }
}

运行结果如下:

MyClassLoader findClass invoked
class classloader1.ClassLoaderTest
classloader2.MyClassLoader@4554617c

嗯,自定义类加载器终于用上了。

稍微改下main方法的代码:

public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {

        MyClassLoader myClassLoader = new MyClassLoader("C:\\Users\\Administrator\\Desktop\\classes\\");
        Class clazz = myClassLoader.loadClass("classloader1.ClassLoaderTest");
        System.out.println(clazz.hashCode());

        //再定义一个自定义的类加载器
        MyClassLoader myClassLoader1 = new MyClassLoader("C:\\Users\\Administrator\\Desktop\\classes\\");
        Class clazz1 = myClassLoader1.loadClass("classloader1.ClassLoaderTest");
        System.out.println(clazz1.hashCode());
    }

这里的执行结果分两种情况:
1. 当ClassPath目录下存在classloader1.ClassLoaderTest的class文件时,输出的两个class对象的hashCode值相同, 因为本质上是由AppClassLoader去加载的, 而且只会加载一次;
2. 当ClassPath目录下不存在classloader1.ClassLoaderTest的class文件时,输出的两个class对象的hashCode值不相同, 因为他们是通过自定义类加载器加载的。这也说明了一个问题:同一个类被加载了两次。如果myClassLoader1 的父类加载器是myClassLoader ,那么输出结果又会不一样,因为类加载器在同一个命名空间。

命名空间

每个类加载器都有自己的命名空间,命名空间由该类加载器及父类加载器所加载的类组成。

同一个命名空间下,不会出现相同的两个类(包名类名都相同)。
不同命名空间下,有可能会出现相同的两个类(包名类名都相同)。

类加载器的双亲委托机制本质上是一种包含关系。

类的卸载

  • 由JVM自带的类加载器(前面提到的三种)所加载的类,在JVM生命周期中,始终不会被卸载。

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

JVM参数: -XX:+TraceClassUnloading 可以看出程序运行哪些类被JVM卸载了。

你可能感兴趣的:(JVM)