【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是被系统类加载器加载的,而不是我们自定义的加载器。
原因:
- 自定义的加载器CustomizedClassLoader的父类构造器是系统类加载器。因为我们加载类调用的方法是:
classLoader.loadClass("com.jamie.jvmstudy.TestClassLoader")
- 默认的loadClass()方法实现就是双亲委派的源码实现,因为系统类加载器会在当前的classpath(类路径)下查找是否存在匹配的"binary name",如果存在,则系统类加载器加载成功。
- 所以只要类路径下存在匹配的二进制名字的字节码,就会被系统类加载器成功加载。
例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可以看到下图: