1. 什么是类加载器?
类加载的实际过程为:通过一个类的全限定名来获取描述此类的二进制字节流。我们把实现这个动作的代码模块成为“类加载器”。
2. 怎么比较两个类"相等"?
我们知道使用关键字instanceof,可以判断某个对象是否是某个Class的实例对象,但是一旦涉及到类加载器ClassLoader之后,就会出现很多令人迷惑的现象。
我们来先看个具体例子:
public class Test {
public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
Test test = new Test();
System.out.println(test instanceof Test);
ClassLoader classLoader = new ClassLoader() {
public Class<?> loadClass(String name) throws ClassNotFoundException {
String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
InputStream is = getClass().getResourceAsStream(fileName);
if (is == null) {
return super.loadClass(name);
}
try {
byte[] b = new byte[is.available()];
is.read(b);
return defineClass(name, b, 0, b.length);
} catch (IOException e) {
e.printStackTrace();
throw new ClassNotFoundException(name);
}
}
};
Object obj = classLoader.loadClass("Test").newInstance();
System.out.println(obj.getClass());
System.out.println(obj.getClass() == Test.class);
System.out.println(test.getClass() == Test.class);
System.out.println(obj instanceof Test);
}
}
这段代码的运行结果为:
true
class Test
false
true
false
从结果中可以看到,obj对象的class也为Test,但是与Test.class确是“不相等”的,而test对象的class与Test.class是“相等”的。它们两者之间的区别是,前者是由我们自定义的ClassLoader加载出来的,而后者是由虚拟机默认的ClassLoader加载出来的。虽然两者都是同一份class文件,但是加载的ClassLoader确不同,这说明要判断两个类是否“相等”,是由2个因素来决定的:
- 一是class信息是否“相等”,这里的“相等”指的是描述类的class信息是一致的,包括包名一致、类名一致、类里的信息一致等;
- 另一个就是加载该class的ClassLoader是否是同一个。
3. ClassLoader的双亲委派模型
双亲委派模型要求所有的类加载器都有一个父加载器,除了最顶层的启动类加载器之外。它的执行逻辑是:当一个类加载器收到加载类的请求时,它不会自己去尝试加载类,而是委托给其父类来加载,每一个层级都是如此,直至到达启动类加载器为止,如果父类加载器反馈自己无法加载时,子类才会自己尝试去加载类。
由此可见,所有的类最终都是由顶层的启动类加载器来加载完成。前面一节中描述了怎么判断class是否“相等”,而双亲委派模型保证了同一个类都是由同一个ClassLoader来加载的,避免了class类型的不一致。
我们可以通过一个实例来看看不同的类加载时的ClassLoader有什么不同:
public class Test {
public static void main(String[] args) {
System.out.println(Test.class.getClassLoader());
Object obj = new Object();
System.out.println(obj.getClass().getClassLoader());
List list = new ArrayList();
System.out.println(list.getClass().getClassLoader());
}
}
执行结果为:
sun.misc.Launcher$AppClassLoader@338bd37a
null
null
可以看到,我们自定义的类Test是通过AppClassLoader来加载的,而Object、List的ClassLoader确是null,这是因为这些都是由启动类加载器来加载的,启动类加载器是采用c++写的,在java环境里无法获取到该类的实例,因此为null。
同样,我们依次打印下每个ClassLoader的父ClassLoader:
public class Test {
public static void main(String[] args) {
ClassLoader loader = Test.class.getClassLoader();
while (loader != null) {
System.out.println(loader);
loader = loader.getParent();
}
}
}
结果为:
sun.misc.Launcher$AppClassLoader@20e90906
sun.misc.Launcher$ExtClassLoader@234f79cb
这里也与双亲委派模型里ClassLoader层次结构是一致的,这里需要注意的是,AppClassLoader并不是直接继承自ExtClassLoader的,它们是通过组合的方式来实现父子关系的。
4. Class.forName()加载类
Class类有个静态方法名为forName,可以通过类的字符串名加载返回代表该类的Class对象,它有两个重载的方法,一个只有一个参数,一个有三个参数,我们来先看看有3个参数的方法定义:
public static Class<?> forName(String name, boolean initialize, ClassLoader loader)
这三个参数的含义如下:
name: 类或接口的全限定名
initialize:前面介绍类加载机制时有讲过,共有加载、验证、准备、解析、初始化、使用、卸载等步骤,该参数为true表示加载该类时会进行类的初始化,false表示不会进行类的初始化。
loader:表示采用哪个ClassLoader来加载该类
我们通过一个例子来看看,类加载时不同的参数会有什么不同的结果。
public class Test {
public static int COUNT = 0;
static {
System.out.println("Test init...");
}
public static void printCount() {
System.out.println("COUNT: " + COUNT);
COUNT++;
}
public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, SecurityException, IllegalArgumentException, InvocationTargetException {
ClassLoader classLoader = new ClassLoader() {
public Class<?> loadClass(String name) throws ClassNotFoundException {
String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
InputStream is = getClass().getResourceAsStream(fileName);
if (is == null) {
return super.loadClass(name);
}
try {
byte[] b = new byte[is.available()];
is.read(b);
return defineClass(name, b, 0, b.length);
} catch (IOException e) {
e.printStackTrace();
throw new ClassNotFoundException(name);
}
}
};
Test.printCount();
Test.printCount();
Test.printCount();
Class clazz = Class.forName("Test", false, classLoader);
System.out.println("==========");
Method m = clazz.getMethod("printCount", null);
m.invoke(null, null);
}
}
执行结果如下:
Test init...
COUNT: 0
COUNT: 1
COUNT: 2
==========
Test init...
COUNT: 0
在该例子中,执行Test.printCount()时,首先会触发Test类的初始化,然后连续共执行了3次,COUNT的值应该为3。接着我们使用自定义ClassLoader又加载了Test类,并且initialize参数设置为false,所以并没有触发类的初始化。然后我们通过反射调用了刚加载的Test类的printCount()方法,发现这个时候触发了类的初始化,并且打印出COUNT的值为0,这都说明采用自定义ClassLoader加载的Test类,与虚拟机默认加载的Test类压根是不同的对象。
如果把加载类的代码改为Class clazz = Class.forName("Test", true, classLoader),结果会是什么样呢?
Test init...
COUNT: 0
COUNT: 1
COUNT: 2
Test init...
==========
COUNT: 0
这就很明显的看出initialize为true或者false时,其加载过程的不同了。
那么另外一个方法的执行逻辑是什么呢?
public static Class<?> forName(String className)
其实相当于Class.forName(className, true, appClassLoader),也即采用默认的ClassLoader来加载类,并且在加载时会进行类初始化。
5. 为什么要自定义类加载器?
大部分情况下,我们都不需要自定义类加载器。但是默认的类加载器有一个局限性,就是它只能加载特定目录下的class文件,但是如果我们想要加载远程服务器上的class文件,或者就是一个符合class规范的二进制字节流,那么就需要自定义类加载器来实现了。
现在流行的热修复、热部署技术,其实都是利用了自定义类加载器来实现的。以Android应用中的热修复技术为例,一般情况下安卓应用发布到应用市场后,用户下载安装应用软件,如果应用软件出了比较致命的bug,通常必须由用户重新下载更新新的安装包才能解决问题。这些都要求用户升级软件,但是热修复技术可以不用升级软件就能动态解决原有软件的致命bug。其核心原理就是原本发布的软件里,通常是采用自定义ClassLoader来加载执行代码的,当某些代码出现问题后,发布修复问题的补丁包代码,客户端获取到补丁包代码后,采用自定义来加载器来加载补丁包里的类,而不是加载原来有问题的类,这样就达到了不升级软件就能解决问题的目标。
java类加载机制系列文章: