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进程->监视,里面可以看到被卸载的类。
浙公网安备 33010602011771号