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类种,作为静态内部类呈现。
类加载器的父亲委托机制:
请看如下代码:
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();
自定义类加载器步骤:
看一个具体自定义加载器的实例:
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卸载了。