Java程序启动时并不是把所有的类一次性都加载到内存中,而是当需要的时候才加载,比如说调用到类的静态方法、变量,实例化类等等,ClassLoader即类加载器,就是负责在需要的时候将类加载到JVM中。
在Java 8及以前,Java中有三个内置的加载器,分别是BootstrapClassLoader、ExtensionClassLoader和AppClassLoader,它们负责加载不同的类。
$JAVA_HOME/jre/lib
目录下的类,这里都是一些JDK内部比较核心的类,比如java.lang.*
,java.util.*
等等,它和其它类加载器不同的是它是由native实现,特别的是另外两个类加载器也是由它加载的。$JAVA_HOME/lib/ext
目录或者java.ext.dirs
系统变量指定目录下的类,主要是一些扩展类。除了BootstrapClassLoader以外,每个类加载器都有自己的父加载器,保存在parent成员变量中,比如AppClassLoader的父加载器是ExtensionClassLoader,而ExtensionClassLoader的父加载器是BootstrapClassLoader,不过这里比较特殊的是由于BootstrapClassLoader是native实现的,它并没有保存在BootstrapClassLoader的parent成员变量中,当需要通过parent查找class的时候是通过findBootstrapClass方法来实现的。
当调用类加载器的loadClass方法加载类时,首先检查该类是否已经加载过,加载过则直接返回加载好的Class对象,没有加载过则调用父加载器的loadClass方法来加载类,父加载器同样是先检查是否加载过该类,加载过则直接返回加载好的Class对象,没有则继续调用父加载器的父加载器的loadClass方法,就这样一层一层调用下去,直到调用到BootstrapClassLoader加载器也没有加载该类,则返回一层一层调用类加载器findClass来加载类,如果加载成功则返回Class对象,都没有加载成功则抛出ClassNotFoundException,这种加载机制就是常说的双亲委派机制。
遵循双亲委派机制可以避免不同的类加载器重复加载同一个类,同时也可以避免Java库中的类不被替换,比如说我们自定义一个java.lang.String
类,在该类加载时会发现BootstrapClassLoader已经加载过了,所以不会再加载我们自己实现的java.lang.String
类。
注:当父加载器是BootstrapClassLoader时,不是调用loadClass方法,而是通过findBootstrapClass来加载。
注释虽然双亲委派机制有一定的好处,但是由于单向委派的原则,使得它失去一些灵活性,首先我们要明白的是当我们要加载一个类时,默认是使用调用者的类加载器去加载该类,比如下面这段代码
public class Course {
String name;
}
public class Main {
public static void main(String[] args) {
Course course = new Course();
}
}
当加载Course类时,会使用Main类的类加载器去加载Course类,对于大部分情况,这种逻辑都没有问题,但是对于Java核心包里面的类来说,它们的类加载器是BootstrapClassLoader,而BootstrapClassLoader只能加载$JAVA_HOME/jre/lib
目录下的类,如果在Java核心包里面需要加载第三方库下的类,就只能用Thread的里面的Context ClassLoader了,它是Thread类里面的一个成员变量,可以根据需求设置,默认是父线程的Context ClassLoader,而main线程的Context ClassLoader是上面提到的AppClassLoader。
public class Thread implements Runnable {
...
private ClassLoader contextClassLoader;
...
public void setContextClassLoader(ClassLoader cl) {
...
}
}
比如说Java的服务提供者接口(Service Provider Interface,SPI),它们都是定义在Java核心包下,而一般实现都是在第三方jar包中,为了统一管理,简化操作,加载这些第三方服务的实现类的逻辑是放在java.util.ServiceLoader
中,而java.util.ServiceLoader
本身的类加载器是BootstrapClassLoader,如果直接使用它的类加载器去加载第三方库是加载不了的,所以就需要用当前线程中的Context ClassLoader去加载,如果不显式设置的话也就是AppClassLoader。
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
Java 9引入了模块化,JDK自带的一些核心类也不存放在$JAVA_HOME/jre/lib/rt.jar
中,而是根据不同模块分别放在了$JAVA_HOME/jmods
下面的jmod文件中,为了安全性和兼容之前的逻辑,Java 9仍然保留了之前这三个内置的类加载器,但是为了实现模块系统,还是有一些改动。
$JAVA_HOME/lib/ext
来进行扩展,ExtensionClassLoader改名为PlatformClassLoader。java.base
模块的BuiltinClassLoader。java.base
、java.rmi
等等,这些模块都是拥有全部的权限,而PlatformClassLoader加载一些Java库中不需要全部权限的模块,比如java.sql
、jdk.charsets
等等。