JVM类加载器(二)

一个类加载器对象主要用于负责加载类,当我们将一个字符串形式的类名作为参数,传给类加载器的方法去加载类的时候,类名必须满足Java所规定的二进制名字。什么是二进制名字呢?比如下面几个例子:

  • java.lang.String
  • javax.swing.JSpinner$DefaultEditor
  • java.security.KeyStore$Builder$FileBuilder$1
  • java.net.URLClassLoader$3$1

其中,上面的第三和第四个例子可能比较难懂。第三个例子代表KeyStore内部类的Builder的第一个匿名内部类(KeyStore$Builder$FileBuilder$1);同理第四个例子代表URLClassLoader类的第三个匿名内部类的第一个内部类(URLClassLoader$3$1)。

每个Class对象都会包含对定义它的类加载器对象的引用,但数组的Class对象并不是类加载器创建的,而是Java虚拟机运行期根据需要动态生成的。对于数组类型的加载器,与数组中元素的类型是一样的,如果元素是原生类型,那么这个元素及其对应的数组类型,都没有类加载器。

我们来看下面这个例子:

package com.leolin.jvm;

public class MyTest15 {
    public static void main(String[] args) {
        String[] strings = new String[2];
        System.out.println(strings.getClass().getClassLoader());
        MyTest15[] myTest15s = new MyTest15[2];
        System.out.println(myTest15s.getClass().getClassLoader());
        MyTest15[][] myTest15s_1 = new MyTest15[2][2];
        System.out.println(myTest15s_1.getClass().getClassLoader());
        int[] ints = new int[2];
        System.out.println(ints.getClass().getClassLoader());
    }
}

  

下面的输出证明我们之前的论述:

null
sun.misc.Launcher$AppClassLoader@7b7035c6
sun.misc.Launcher$AppClassLoader@7b7035c6
null

  

有一点我们需要注意,即便数组的类型有时候可以获取到类加载器,但这个类型并不是加载进来的,而是JVM生成的。

下面,我们尝试去编写一个类加载器MyTest16:

package com.leolin.jvm;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;

public class MyTest16 extends ClassLoader {
    private String classLoaderName;//加载器名字,仅仅是标识性作用
    private String fileExtension = ".class";

    public MyTest16(String classLoaderName) {
        super();//将系统类加载器当做该类加载器的父加载器
        this.classLoaderName = classLoaderName;
    }

    public MyTest16(ClassLoader parent, String classLoaderName) {
        super(parent);//显式指定该类加载器的父加载器
        this.classLoaderName = classLoaderName;
    }


    @Override
    protected Class<?> findClass(String className) throws ClassNotFoundException {
        //读取class文件的字节数组
        System.out.println("findClass invoked:" + className);
        System.out.println("class loader name:" + this.classLoaderName);
        byte[] data = this.loadClassData(className);
        return super.defineClass(className, data, 0, data.length);
    }

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

    public static void main(String[] args) throws Exception {
        MyTest16 loader1 = new MyTest16("loader1");
        Class<?> clazz = loader1.loadClass("com.leolin.jvm.MyTest1");
        System.out.println("class:" + clazz.hashCode());
        Object object = clazz.newInstance();
        System.out.println(object);
    }


}

  

上面的代码,我们重写了findClass方法,这个方法会打印我们要求加载的类名,以及本身的类加载器名字 ,然后会调用loadClassData获取对应的class文件的字节数组。然后我们在main方法中又调用加载器的loadClass方法,并传入类名,这个方法就是帮我们加载类的方法,我们加载的是很早之前所编写的MyTest1类。于是我们执行上面的代码,得到如下的输出:

class:576153008
com.leolin.jvm.MyTest1@3a0aaa10

  

很奇怪的是,MyTest1加载成功了,但是我们在findClass方法里所编写的打印却没有输出。这是因为当loader1要去加载MyTest1时,会先将加载任务委托给它的父加载器,也就是系统加载器。系统加载器加载MyTest1成功,也就无须loader1执行findClass方法,将class文件转换成字节数组,将通过defineClass生成对应的Class对象。从这个意义上来说,系统类加载器是MyTest1的定义类加载器,而系统类加载器和MyTest16是MyTest1的初始类加载器。

为了让MyTest16真正去加载MyTest1,我们要稍作改动,我们给MyTest16新增一个path的成员变量,可以指定一个路径,去读取路径下的类文件:

package com.leolin.jvm;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;

public class MyTest16 extends ClassLoader {
    private String classLoaderName;//加载器名字,仅仅是标识性作用
    private String path;
    private String fileExtension = ".class";

    public MyTest16(String classLoaderName) {
        super();//将系统类加载器当做该类加载器的父加载器
        this.classLoaderName = classLoaderName;
    }

    public MyTest16(ClassLoader parent, String classLoaderName) {
        super(parent);//显式指定该类加载器的父加载器
        this.classLoaderName = classLoaderName;
    }

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

    @Override
    protected Class<?> findClass(String className) throws ClassNotFoundException {
        //读取class文件的字节数组
        System.out.println("findClass invoked:" + className);
        System.out.println("class loader name:" + this.classLoaderName);
        byte[] data = this.loadClassData(className);
        return super.defineClass(className, data, 0, data.length);
    }

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

    public static void main(String[] args) throws Exception {
        MyTest16 loader1 = new MyTest16("loader1");
        loader1.setPath("C:\\Users\\admin\\Desktop\\");
        Class<?> clazz = loader1.loadClass("com.leolin.jvm.MyTest1");
        System.out.println("class:" + clazz.hashCode());
        Object object = clazz.newInstance();
        System.out.println(object);
    }
}

  

我们让loader1去桌面读取类文件,同时,我们也要复制我们的类路径到桌面,并将我们工程底下的MyTest1的class文件删除。这样当loader1委托系统加载器加载MyTest1时,系统加载器无法在当前工程的classpath下找到MyTest1,才会将任务回传给loader1,由loader1调用重写的findClass方法去寻找。

运行上面的代码,得到如下输出:

findClass invoked:com.leolin.jvm.MyTest1
class loader name:loader1
class:1796456726
com.leolin.jvm.MyTest1@5de9c245

  

可以看到,现在确实是由我们所定义的类加载器加载的MyTest1。但是如果我们重新编译整个项目,MyTest1又会由系统加载器去加载,我们重写的findClass依旧不会执行。

我们删除MyTest1的class文件,并将main方法修改如下:

public static void main(String[] args) throws Exception {
	MyTest16 loader1 = new MyTest16("loader1");
	loader1.setPath("C:\\Users\\admin\\Desktop\\");
	Class<?> clazz = loader1.loadClass("com.leolin.jvm.MyTest1");
	System.out.println("class:" + clazz.hashCode());
	Object object = clazz.newInstance();
	System.out.println(object);

	MyTest16 loader2 = new MyTest16("loader2");
	loader2.setPath("C:\\Users\\admin\\Desktop\\");
	Class<?> clazz2 = loader2.loadClass("com.leolin.jvm.MyTest1");
	System.out.println("class:" + clazz2.hashCode());
}

  

运行结果如下:

findClass invoked:com.leolin.jvm.MyTest1
class loader name:loader1
class:942306880
com.leolin.jvm.MyTest1@6bb9808e
findClass invoked:com.leolin.jvm.MyTest1
class loader name:loader2
class:1770329462

  

可以看到,MyTest1分别在loader1和loader2加载了两次,而且clazz和clazz2的hashCode也不一样。这似乎和我们之前提到的一个类只能加载一次互相矛盾了,其实这里还涉及到一个类加载器命名空间的问题。

命名空间

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

在同一个命名空间中,不会出现类的完整名字(包括类的包名)相同的两个类。

在不同的命名空间中,有可能出现类的完整名字相同的两个类。

之前的loader1和loader2分别为两个不同的命名空间,所以允许MyTest1分别出现在这两个命名空间中。

我们将loader2的声明改为:

MyTest16 loader2 = new MyTest16(loader1, "loader2");

  

让loader1作为loader2的父加载器,重新执行程序,得到如下输出:

findClass invoked:com.leolin.jvm.MyTest1
class loader name:loader1
class:500265006
com.leolin.jvm.MyTest1@211beb4d
class:500265006

  

类的卸载

当MySample被加载、连接和初始化之后,它的生命周期就开始了。当MySample类的Class对象不再被引用,即不可触及时,Class对象就会结束生命周期,MySample类在方法区内的数据也会被卸载,从而结束MySample类的生命周期。
一个类何时结束生命周期,取决于代表它的Class对象何时结束生命周期。
由Java虚拟机自带的类加载器所加载的类,在虚拟机的生命周期中,始终不会被卸载。Java虚拟机本身会始终引用这些加载器,而这些类加载器则会始终引用他们所加载的类的Class对象,因此这些Class对象是可触及的。
由用户自定义的类加载器所加载的类是可以被卸载的。

现在,我们来展示一下类的卸载,这里要认识一个JVM参数,-XX:+TraceClassUnloading,用来追踪被卸载的类,配置好参数后,我们修改MyTest16的main方法如下:

public static void main(String[] args) throws Exception {
	MyTest16 loader1 = new MyTest16("loader1");
	loader1.setPath("C:\\Users\\admin\\Desktop\\");
	Class<?> clazz = loader1.loadClass("com.leolin.jvm.MyTest1");
	System.out.println("class:" + clazz.hashCode());
	Object object = clazz.newInstance();
	System.out.println(object);
	System.out.println("---------");
	loader1 = null;
	clazz = null;
	object = null;
	System.gc();
	loader1 = new MyTest16("loader1");
	loader1.setPath("C:\\Users\\admin\\Desktop\\");
	clazz = loader1.loadClass("com.leolin.jvm.MyTest1");
	System.out.println("class:" + clazz.hashCode());
	object = clazz.newInstance();
	System.out.println(object);
}

  

运行代码,得到如下输出:

findClass invoked:com.leolin.jvm.MyTest1
class loader name:loader1
class:1807319182
com.leolin.jvm.MyTest1@5fa721e2
---------
[Unloading class com.leolin.jvm.MyTest1]
findClass invoked:com.leolin.jvm.MyTest1
class loader name:loader1
class:1142817658
com.leolin.jvm.MyTest1@e766186

Process finished with exit code 0

  

可以看到当我们将MyTest1所对应Class对象不再被引用时,执行GC,虚拟机会卸载MyTest1对应的Class对象。还要一个办法是在GC后休眠足够长的一段时间,然后用用工具jvisualvm->查看java进程->监视,里面可以看到被卸载的类。

posted @ 2020-05-03 17:26  北洛  阅读(199)  评论(0编辑  收藏  举报