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 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
很奇怪的是,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
可以看到,现在确实是由我们所定义的类加载器加载的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 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 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 --------- [Unloading class com.leolin.jvm.MyTest1] findClass invoked:com.leolin.jvm.MyTest1 class loader name:loader1 class:1142817658 Process finished with exit code 0
可以看到当我们将MyTest1所对应Class对象不再被引用时,执行GC,虚拟机会卸载MyTest1对应的Class对象。还要一个办法是在GC后休眠足够长的一段时间,然后用用工具jvisualvm->查看java进程->监视,里面可以看到被卸载的类。