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类加载机制系列文章:
网友评论