Java的ClassLoader,你都懂了吗
类是Java程序的组成元素,Java中的每个类都有一个Class对象,为了生成这个Class对象,JVM会使用被称为“类加载器”的子系统,这些“类加载器”就是本文将描述的ClassLoader。
Java并不是一开始就加载所有类,而是用到的时候才加载。当类的一个静态成员被引用时,这个类就会被加载。虽然类的构造函数没有用static修饰,但其实它也是类的静态方法。因此使用new操作符创建类的新对象也会被当做对类的静态成员的引用。
类的加载使用的是被称为类加载器(ClassLoader)的子系统,它的形式是一个链条,链条的末端是原生类加载器。这个链条是这样的:
自定义加载器(可选) -> AppClassLoader-> ExtClassLoader -> BootstrapClassLoader
当需要加载一个类时,JVM会从这个类加载器链的头部开始,依次判断该类是否被加载(即是否能找到该类的Class对象),如果所有ClassLoader都没有加载过该类,就会从这个链条的末端往前开始加载这个类,如果某个类加载器加载成功,那它便是这个类的类加载器,即通过getClass().getClassLoader()方法将返回这个类加载器。如果所有加载器都无法找到这个类,那就会抛出ClassNotFoundException异常。这种加载机制我们称之为“双亲委托”。
自定义类加载器不是必须的,应该说一般情况下不会用到,除非有特殊的需求比如Android的插件化,或者支持从网络下载类等,那么另外三个类加载器都是什么用途呢?
-
BootstrapClassLoader是JVM唯一个原生类加载器,它是JVM实现的一部分,原生类加载器加载的是可信类,包括Java API类,他们通常是从本地磁盘加载的。它查找类的路径可以通过
System.getProperty("sun.boot.class.path")
获取,输出结果:
JAVA_HOME\jre\lib\resources.jar
JAVA_HOME\jre\lib\rt.jar
JAVA_HOME\jre\lib\sunrsasign.jar;
...
-
ExtClassLoader是扩展的类加载器,它查找类的路径可以通过
System.getProperty("java.ext.dirs")
获取,输出结果:
JAVA_HOME\jre\lib\ext
C:\Windows\Sun\Java\lib\ext
-
AppClassLoader负责加载应用程序自身的类,它查找类的路径包括应用程序的本地目录和其它目录,可以通过
System.getProperty("java.class.path")
查看:
JAVA_HOME\jre\lib\charsets.jar
JAVA_HOME\jre\lib\deploy.jar
JAVA_HOME\jre\lib\ext\access-bridge-64.jar
JAVA_HOME\jre\lib\ext\cldrdata.jar
JAVA_HOME\jre\lib\resources.jar
JAVA_HOME\jre\lib\rt.jar
D:\Develop\TestRegex\out\production\TestRegex (当前目录)
D:\Program Files\JetBrains\IntelliJ IDEA Community Edition 2017.2.5\lib\idea_rt.jar
...
Android插件化中,我们会用DexClassLoader加载一个插件的入口类,但插件包里有大量的类,需要一个个去调用ClassLoader.loadClass()吗?加载了一个类A,那A引用的其它类是怎么加载的?
如果类A引用了类B,无论是直接引用还是通过class.forName()引用,JVM会找到类A的ClassLoader,再用这个ClassLoader去加载类B,加载的过程同样采用“双亲委托”机制,因此最终加载了类B的不一定是A的ClassLoader,有可能是AppClassLoader、ExtClassLoader或者是BootstrapClassLoader,而在Android系统中,则有可能是java.lang.BootClassLoader。因此,在运行的时候,JVM会动态地使用正确的类加载器逐一加载各个用到的类。
加载类除了用ClassLoader的loadClass()之外,还可以用Class.forName()方法,它们有什么区别呢?
主要有两个区别:
- Class.forName()默认使用当前类的ClassLoader加载,除非调用Class.forName(name, initialize, classlaoder)的方法来指定ClassLoader;
- Class.forName()会初始化对象,而ClassLoader.load()则不会。
比如对于一个类A:
public class A {
static { System.out.println("time = " + System.currentTimeMillis()); }
}
以及以下两种方式加载这个类:
public class Main1 {
public static void main(String... args) throws Throwable {
final Class<?> c = Class.forName("A");
}
}
public class Main2 {
public static void main(String... args) throws Throwable {
ClassLoader.getSystemClassLoader().loadClass("A");
}
}
Main1会输出time = 1313614183558,而Main2不会。除非调用了该类的某个静态方法,否则类不会被初始化。另外,Class.forName(name, initialize, classLoader)的第二个参数可以控制类是否执行初始化。
参考文献
[1] Difference between Loading a class using ClassLoader and Class.forName
本文完。